Skip to content

Allow custom documentclass for pgf backend #28167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion galleries/users_explain/text/pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
================= =====================================================
Parameter Documentation
================= =====================================================
pgf.documentclass Line used to define the LaTeX documentclass
pgf.preamble Lines to be included in the LaTeX preamble
pgf.rcfonts Setup fonts from rc params using the fontspec package
pgf.texsystem Either "xelatex" (default), "lualatex" or "pdflatex"
Expand Down Expand Up @@ -182,7 +183,9 @@
* If the font configuration used by Matplotlib differs from the font setting
in yout LaTeX document, the alignment of text elements in imported figures
may be off. Check the header of your ``.pgf`` file if you are unsure about
the fonts Matplotlib used for the layout.
the fonts Matplotlib used for the layout. For best results, you may need to
set `pgf.documentclass` and `pgf.preamble` to match your LaTeX document and
disable `pgf.rcfonts`.

* Vector images and hence ``.pgf`` files can become bloated if there are a lot
of objects in the graph. This can be the case for image processing or very
Expand Down
71 changes: 39 additions & 32 deletions lib/matplotlib/backends/backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,17 @@
# %f/{:f} format rather than %s/{} to avoid triggering scientific notation,
# which is not recognized by TeX.

def _get_preamble():
def _get_documentclass():
if mpl.rcParams["pgf.documentclass"]:
return mpl.rcParams["pgf.documentclass"]
else:
return _DOCUMENTCLASS


def _get_preamble(commands=None):
"""Prepare a LaTeX preamble based on the rcParams configuration."""
if commands is None:
commands = []
font_size_pt = FontProperties(
size=mpl.rcParams["font.size"]
).get_size_in_points()
Expand All @@ -47,18 +56,8 @@ def _get_preamble():
r"\def\mathdefault#1{#1}",
# Use displaystyle for all math.
r"\everymath=\expandafter{\the\everymath\displaystyle}",
# Set up font sizes to match font.size setting.
# If present, use the KOMA package scrextend to adjust the standard
# LaTeX font commands (\tiny, ..., \normalsize, ..., \Huge) accordingly.
# Otherwise, only set \normalsize, manually.
r"\IfFileExists{scrextend.sty}{",
r" \usepackage[fontsize=%fpt]{scrextend}" % font_size_pt,
r"}{",
r" \renewcommand{\normalsize}{\fontsize{%f}{%f}\selectfont}"
% (font_size_pt, 1.2 * font_size_pt),
r" \normalsize",
r"}",
# Allow pgf.preamble to override the above definitions.
# Allow pgf.documentclass and pgf.preamble to override the above definitions.
_get_documentclass(),
mpl.rcParams["pgf.preamble"],
*([
r"\ifdefined\pdftexversion\else % non-pdftex case.",
Expand All @@ -72,6 +71,7 @@ def _get_preamble():
for family in ["serif", "sans\\-serif", "monospace"]]
)
] + [r"\fi"] if mpl.rcParams["pgf.rcfonts"] else []),
*commands,
# Documented as "must come last".
mpl.texmanager._usepackage_if_not_loaded("underscore", option="strings"),
])
Expand Down Expand Up @@ -205,13 +205,13 @@ class LatexManager:
@staticmethod
def _build_latex_header():
latex_header = [
_DOCUMENTCLASS,
# Include TeX program name as a comment for cache invalidation.
# TeX does not allow this to be the first line.
rf"% !TeX program = {mpl.rcParams['pgf.texsystem']}",
# Test whether \includegraphics supports interpolate option.
r"\usepackage{graphicx}",
_get_preamble(),
_get_preamble(commands=[
# Include TeX program name as a comment for cache invalidation.
# TeX does not allow this to be the first line.
rf"% !TeX program = {mpl.rcParams['pgf.texsystem']}",
# Test whether \includegraphics supports interpolate option.
r"\usepackage{graphicx}",
]),
r"\begin{document}",
r"\typeout{pgf_backend_query_start}",
]
Expand Down Expand Up @@ -823,6 +823,7 @@ def print_pgf(self, fname_or_fh, **kwargs):
def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs):
"""Use LaTeX to compile a pgf generated figure to pdf."""
w, h = self.figure.get_size_inches()
geometry_options = r"papersize={%fin,%fin}, margin=0in" % (w, h)

info_dict = _create_pdf_info_dict('pgf', metadata or {})
pdfinfo = ','.join(
Expand All @@ -834,12 +835,14 @@ def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs):
self.print_pgf(tmppath / "figure.pgf", **kwargs)
(tmppath / "figure.tex").write_text(
"\n".join([
_DOCUMENTCLASS,
r"\usepackage[pdfinfo={%s}]{hyperref}" % pdfinfo,
r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}"
% (w, h),
r"\usepackage{pgf}",
_get_preamble(),
r"\PassOptionsToPackage{pdfinfo={%s}}{hyperref}" % pdfinfo,
r"\PassOptionsToPackage{%s}{geometry}" % geometry_options,
_get_preamble(commands=[
r"\usepackage{hyperref}",
r"\usepackage{geometry}",
r"\geometry{reset, %s}" % geometry_options,
r"\usepackage{pgf}",
]),
r"\begin{document}",
r"\centering",
r"\input{figure.pgf}",
Expand Down Expand Up @@ -941,15 +944,19 @@ def __init__(self, filename, *, keep_empty=_UNSET, metadata=None):
keep_empty = _api.deprecate_privatize_attribute("3.8")

def _write_header(self, width_inches, height_inches):
geometry_options = (r"papersize={%fin,%fin}, margin=0in"
% (width_inches, height_inches))
pdfinfo = ','.join(
_metadata_to_str(k, v) for k, v in self._info_dict.items())
latex_header = "\n".join([
_DOCUMENTCLASS,
r"\usepackage[pdfinfo={%s}]{hyperref}" % pdfinfo,
r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}"
% (width_inches, height_inches),
r"\usepackage{pgf}",
_get_preamble(),
r"\PassOptionsToPackage{pdfinfo={%s}}{hyperref}" % pdfinfo,
r"\PassOptionsToPackage{%s}{geometry}" % geometry_options,
_get_preamble(commands=[
r"\usepackage{hyperref}",
r"\usepackage{geometry}",
r"\geometry{reset, %s}" % geometry_options,
r"\usepackage{pgf}",
]),
r"\setlength{\parindent}{0pt}",
r"\begin{document}%",
])
Expand Down
7 changes: 4 additions & 3 deletions lib/matplotlib/mpl-data/matplotlibrc
Original file line number Diff line number Diff line change
Expand Up @@ -738,9 +738,10 @@

### pgf parameter
## See https://matplotlib.org/stable/tutorials/text/pgf.html for more information.
#pgf.rcfonts: True
#pgf.preamble: # See text.latex.preamble for documentation
#pgf.texsystem: xelatex
#pgf.rcfonts: True
#pgf.documentclass: \documentclass{article} # Set the documentclass
#pgf.preamble: # See text.latex.preamble for documentation
#pgf.texsystem: xelatex

### docstring params
#docstring.hardcopy: False # set this when you want to generate hardcopy docstring
Expand Down
7 changes: 4 additions & 3 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,9 +1303,10 @@ def _convert_validator_spec(key, conv):
"pdf.use14corefonts": validate_bool,
"pdf.fonttype": validate_fonttype, # 3 (Type3) or 42 (Truetype)

"pgf.texsystem": ["xelatex", "lualatex", "pdflatex"], # latex variant used
"pgf.rcfonts": validate_bool, # use mpl's rc settings for font config
"pgf.preamble": validate_string, # custom LaTeX preamble
"pgf.texsystem": ["xelatex", "lualatex", "pdflatex"], # latex variant used
"pgf.rcfonts": validate_bool, # use mpl's rc settings for font config
"pgf.documentclass": validate_string, # custom LaTeX documentclass
"pgf.preamble": validate_string, # custom LaTeX preamble

# write raster image data into the svg file
"svg.image_inline": validate_bool,
Expand Down
43 changes: 43 additions & 0 deletions lib/matplotlib/tests/test_backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,46 @@ def test_document_font_size():
label=r'\normalsize the document font size is \the\fontdimen6\font'
)
plt.legend()


# test using a preamble where packages have different options
@needs_pgf_xelatex
@pytest.mark.parametrize('format', ('pgf', 'pdf', 'png'))
@pytest.mark.backend('pgf')
def test_preamble_packages(format, tmp_path):
# A preamble containing all packages matplotlib uses, but with different
# options to trigger an option clash.
preamble = '\n'.join([
r'\usepackage[no-math]{fontspec}',
r'\usepackage[nohyphen]{underscore}',
r'\usepackage[demo]{graphicx}',
r'\usepackage[draft]{hyperref}',
r'\usepackage[margin=1in]{geometry}',
r'\usepackage[draft]{pgf}',
r'\usepackage[ascii]{inputenc}',
r'\usepackage[safe]{textcomp}',
])
mpl.rcParams['pgf.preamble'] = preamble

plt.figure().savefig(BytesIO(), format=format)

path = os.path.join(tmp_path, f'preamble_packages_{format}.pdf')
with PdfPages(path) as pdf:
pdf.savefig(plt.figure())


# test different documentclass types
@needs_pgf_xelatex
@pytest.mark.parametrize('documentclass', (
'',
r'\documentclass{article}',
r'\documentclass[12]{article}',
r'\documentclass[10]{article}',
r'\documentclass[20]{extarticle}',
r'\documentclass{minimal}',
r'\documentclass{beamer}',
))
@pytest.mark.backend('pgf')
def test_documentclass(documentclass):
mpl.rcParams['pgf.documentclass'] = documentclass
plt.figure().savefig(BytesIO())