Skip to content

API reference

This package provides two classes to create and render LaTeX documents:

  • TexDocument: Create and render a LaTeX document given the full LaTeX code
  • TexFragment: Create and render a LaTeX standalone document given a LaTeX fragment or a TikZ Picture.

jupyter_tikz.TexDocument

This class provides functionality to create and render a LaTeX document given the full LaTeX code. It can also constructs LaTeX code using Jinja2 templates.

Source code in jupyter_tikz/jupyter_tikz.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
class TexDocument:
    """This class provides functionality to create and render a LaTeX document given the full LaTeX code. It can also constructs LaTeX code using Jinja2 templates."""

    def __init__(
        self, code: str, no_jinja: bool = False, ns: dict[str, Any] | None = None
    ):
        """Initializes the `TexDocument` class.

        Args:
            code: LaTeX code to render.
            no_jinja: Disable Jinja2 rendering.
            ns: A namespace dictionary with the variables to render the Jinja2 template. It must be provided when `use_jinja` is `True`.

        Raises:
            ValueError: If `use_jinja` is `True` and `ns` is not provided.
        """
        self._code: str = code.strip()
        self._no_jinja: bool = no_jinja
        if not ns:
            ns = {}

        if not self._no_jinja:
            self._render_jinja(ns)

    @property
    def full_latex(self) -> str:
        """Returns the full LaTeX code to render."""
        return self._code

    @property
    def tikz_code(self) -> str | None:
        r"""Returns the TikZ code."""
        pattern = r"^\s*\\begin\{tikzpicture\}.*?\\end\{tikzpicture\}"
        match = re.search(pattern, self.full_latex, re.DOTALL | re.MULTILINE)
        if match:
            return dedent(match.group(0))
        return None

    @staticmethod
    def _arg_head(arg, limit=60) -> str:
        if type(arg) == str:
            arg = arg.strip()
            arg = f"{arg[:limit]}..." if len(arg) > limit else arg
            arg = str(repr(arg.strip()))
        else:
            arg = str(arg)
        return arg

    @property
    def _hex_hash(self) -> str:
        """Returns the md5 hash value of the full LaTeX code."""
        # return f"{abs(hash(self.full_latex)):x}"
        return md5(self.full_latex.encode()).hexdigest()

    def __repr__(self) -> str:
        """Returns a compact string representation of the object."""
        params_dict = self.__dict__
        if "scale" in params_dict.keys():
            if params_dict["scale"] == 1.0:
                del params_dict["scale"]

        params = ", ".join(
            [
                f'{k if k != "_no_jinja" else "no_jinja"}={self._arg_head(v)}'
                for k, v in params_dict.items()
                if k not in ["_code", "full_latex", "tikz_code", "ns"] and v
            ]
        )
        if params:
            params = ", " + params
        return f"{self.__class__.__name__}({self._arg_head(self._code)}{params})"

    def __str__(self) -> str:
        """Returns the LaTeX code string to render."""
        return self._code

    def _clearup_latex_garbage(self, keep_temp) -> None:
        if not (keep_temp):  # F
            files = Path().glob(f"{self._hex_hash}.*")
            for file in files:
                if file.exists():
                    file.unlink()

    def _run_command(self, command: str, full_err: bool = False, **kwargs) -> int:

        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            check=False,
            **kwargs,
        )
        if result.returncode != 0:
            err_msg = result.stderr if result.stderr else result.stdout
            if not full_err:  # tail -n 20
                err_msg = "\n".join(err_msg.splitlines()[-20:])
            print(err_msg, file=sys.stderr)
        return result.returncode

    def _save(
        self, dest: str, ext: Literal["tikz", "tex", "png", "svg", "pdf"]
    ) -> None:
        dest_path = Path(dest)

        if os.environ.get("JUPYTER_TIKZ_SAVEDIR"):
            dest_path = str(os.environ.get("JUPYTER_TIKZ_SAVEDIR")) / dest_path
        else:
            dest_path = dest_path

        dest_path = dest_path.resolve()

        if dest_path.suffix != f".{ext}":
            dest_path = dest_path.with_suffix(dest_path.suffix + f".{ext}")

        dest_path.parent.mkdir(parents=True, exist_ok=True)

        if ext == "tikz":
            if not self.tikz_code:
                raise ValueError("No TikZ code to save.")
            dest_path.with_suffix(".tikz").write_text(self.tikz_code, encoding="utf-8")
        else:
            Path(self._hex_hash).with_suffix(f".{ext}").replace(
                dest_path.with_suffix(f".{ext}")
            )

    def run_latex(
        self,
        tex_program: str = "pdflatex",
        tex_args: str | None = None,
        rasterize: bool = False,
        full_err: bool = False,
        keep_temp: bool = False,
        save_image: str | None = None,
        dpi: int = 96,
        grayscale: bool = False,
        save_tex: str | None = None,
        save_tikz: str | None = None,
        save_pdf: str | None = None,
    ) -> Image | SVG | None:
        """Run the LaTeX program to render the LaTeX code.

        Args:
            tex_program: The LaTeX program to use for compilation.
            tex_args: Arguments to pass to the TeX program.
            rasterize: Output a rasterized image (PNG) instead of SVG.
            full_err: Print the full error message when an error occurs. If False, it prints only the last 20 lines.
            keep_temp: Keep temporary LaTeX files.
            save_image: Save the output image to file.
            dpi: DPI to use when rasterizing the image.
            grayscale: Set grayscale to a rasterized image.
            save_tex: Save the full LaTeX code to file.
            save_tikz: Save the TikZ code to file.
            save_pdf: Save the output PDF to file.

        Returns:
            Image | SVG | None: The rendered image. None if an error occurs.
        """
        try:

            tex_path = Path().resolve() / f"{self._hex_hash}.tex"
            tex_path.write_text(self.full_latex, encoding="utf-8")

            tex_command = tex_program
            if tex_args:
                tex_command += f" {tex_args}"
            tex_command += f" {tex_path}"

            res = self._run_command(tex_command, full_err=full_err)
            if res != 0:
                self._clearup_latex_garbage(keep_temp)
                return None

            image_format = "svg" if not rasterize else "png"

            if os.environ.get("JUPYTER_TIKZ_PDFTOCAIROPATH"):
                pdftocairo_path = os.environ.get("JUPYTER_TIKZ_PDFTOCAIROPATH")
            else:
                pdftocairo_path = "pdftocairo"

            pdftocairo_command = f"{pdftocairo_path} -{image_format}"
            if rasterize:
                pdftocairo_command += (
                    f" -singlefile -{'gray' if grayscale else 'transp'} -r {dpi}"
                )

            pdftocairo_command += f" {tex_path.with_suffix('.pdf')}"
            pdftocairo_command += (
                f" {tex_path.with_suffix('.svg')}"
                if not rasterize
                else f" {tex_path.parent / tex_path.stem}"
            )
            res = self._run_command(pdftocairo_command, full_err=full_err)

            if res != 0:
                self._clearup_latex_garbage(keep_temp)
                return None

            image = (
                display.Image(tex_path.with_suffix(".png"))
                if rasterize
                else display.SVG(tex_path.with_suffix(".svg"))
            )

            if save_image:
                self._save(save_image, image_format)
            if save_tex:
                self._save(save_tex, "tex")
            if save_pdf:
                self._save(save_pdf, "pdf")
            if save_tikz and self.tikz_code:
                self._save(save_tikz, "tikz")

            self._clearup_latex_garbage(keep_temp)

            return image
        except Exception as e:
            raise e
        finally:
            self._clearup_latex_garbage(keep_temp)

    def _render_jinja(self, ns) -> None:
        fs_loader = jinja2.FileSystemLoader(os.getcwd())

        tmpl_env = jinja2.Environment(
            loader=fs_loader,
            block_start_string="(**",  # Normal is '{%'.
            block_end_string="**)",  # Normal is '%}'.
            variable_start_string="(*",  # Normal is '{{'.
            variable_end_string="*)",  # Normal is '}}'.
            comment_start_string="(~",  # Normal is '{#'.
            comment_end_string="~)",  # Normal is '#}'.
        )

        tmpl = tmpl_env.from_string(self._code)

        self._code = tmpl.render(**ns)

full_latex: str property

Returns the full LaTeX code to render.

tikz_code: str | None property

Returns the TikZ code.

__init__(code, no_jinja=False, ns=None)

Initializes the TexDocument class.

Parameters:

Name Type Description Default
code str

LaTeX code to render.

required
no_jinja bool

Disable Jinja2 rendering.

False
ns dict[str, Any] | None

A namespace dictionary with the variables to render the Jinja2 template. It must be provided when use_jinja is True.

None

Raises:

Type Description
ValueError

If use_jinja is True and ns is not provided.

Source code in jupyter_tikz/jupyter_tikz.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self, code: str, no_jinja: bool = False, ns: dict[str, Any] | None = None
):
    """Initializes the `TexDocument` class.

    Args:
        code: LaTeX code to render.
        no_jinja: Disable Jinja2 rendering.
        ns: A namespace dictionary with the variables to render the Jinja2 template. It must be provided when `use_jinja` is `True`.

    Raises:
        ValueError: If `use_jinja` is `True` and `ns` is not provided.
    """
    self._code: str = code.strip()
    self._no_jinja: bool = no_jinja
    if not ns:
        ns = {}

    if not self._no_jinja:
        self._render_jinja(ns)

__repr__()

Returns a compact string representation of the object.

Source code in jupyter_tikz/jupyter_tikz.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __repr__(self) -> str:
    """Returns a compact string representation of the object."""
    params_dict = self.__dict__
    if "scale" in params_dict.keys():
        if params_dict["scale"] == 1.0:
            del params_dict["scale"]

    params = ", ".join(
        [
            f'{k if k != "_no_jinja" else "no_jinja"}={self._arg_head(v)}'
            for k, v in params_dict.items()
            if k not in ["_code", "full_latex", "tikz_code", "ns"] and v
        ]
    )
    if params:
        params = ", " + params
    return f"{self.__class__.__name__}({self._arg_head(self._code)}{params})"

__str__()

Returns the LaTeX code string to render.

Source code in jupyter_tikz/jupyter_tikz.py
 98
 99
100
def __str__(self) -> str:
    """Returns the LaTeX code string to render."""
    return self._code

run_latex(tex_program='pdflatex', tex_args=None, rasterize=False, full_err=False, keep_temp=False, save_image=None, dpi=96, grayscale=False, save_tex=None, save_tikz=None, save_pdf=None)

Run the LaTeX program to render the LaTeX code.

Parameters:

Name Type Description Default
tex_program str

The LaTeX program to use for compilation.

'pdflatex'
tex_args str | None

Arguments to pass to the TeX program.

None
rasterize bool

Output a rasterized image (PNG) instead of SVG.

False
full_err bool

Print the full error message when an error occurs. If False, it prints only the last 20 lines.

False
keep_temp bool

Keep temporary LaTeX files.

False
save_image str | None

Save the output image to file.

None
dpi int

DPI to use when rasterizing the image.

96
grayscale bool

Set grayscale to a rasterized image.

False
save_tex str | None

Save the full LaTeX code to file.

None
save_tikz str | None

Save the TikZ code to file.

None
save_pdf str | None

Save the output PDF to file.

None

Returns:

Type Description
Image | SVG | None

Image | SVG | None: The rendered image. None if an error occurs.

Source code in jupyter_tikz/jupyter_tikz.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def run_latex(
    self,
    tex_program: str = "pdflatex",
    tex_args: str | None = None,
    rasterize: bool = False,
    full_err: bool = False,
    keep_temp: bool = False,
    save_image: str | None = None,
    dpi: int = 96,
    grayscale: bool = False,
    save_tex: str | None = None,
    save_tikz: str | None = None,
    save_pdf: str | None = None,
) -> Image | SVG | None:
    """Run the LaTeX program to render the LaTeX code.

    Args:
        tex_program: The LaTeX program to use for compilation.
        tex_args: Arguments to pass to the TeX program.
        rasterize: Output a rasterized image (PNG) instead of SVG.
        full_err: Print the full error message when an error occurs. If False, it prints only the last 20 lines.
        keep_temp: Keep temporary LaTeX files.
        save_image: Save the output image to file.
        dpi: DPI to use when rasterizing the image.
        grayscale: Set grayscale to a rasterized image.
        save_tex: Save the full LaTeX code to file.
        save_tikz: Save the TikZ code to file.
        save_pdf: Save the output PDF to file.

    Returns:
        Image | SVG | None: The rendered image. None if an error occurs.
    """
    try:

        tex_path = Path().resolve() / f"{self._hex_hash}.tex"
        tex_path.write_text(self.full_latex, encoding="utf-8")

        tex_command = tex_program
        if tex_args:
            tex_command += f" {tex_args}"
        tex_command += f" {tex_path}"

        res = self._run_command(tex_command, full_err=full_err)
        if res != 0:
            self._clearup_latex_garbage(keep_temp)
            return None

        image_format = "svg" if not rasterize else "png"

        if os.environ.get("JUPYTER_TIKZ_PDFTOCAIROPATH"):
            pdftocairo_path = os.environ.get("JUPYTER_TIKZ_PDFTOCAIROPATH")
        else:
            pdftocairo_path = "pdftocairo"

        pdftocairo_command = f"{pdftocairo_path} -{image_format}"
        if rasterize:
            pdftocairo_command += (
                f" -singlefile -{'gray' if grayscale else 'transp'} -r {dpi}"
            )

        pdftocairo_command += f" {tex_path.with_suffix('.pdf')}"
        pdftocairo_command += (
            f" {tex_path.with_suffix('.svg')}"
            if not rasterize
            else f" {tex_path.parent / tex_path.stem}"
        )
        res = self._run_command(pdftocairo_command, full_err=full_err)

        if res != 0:
            self._clearup_latex_garbage(keep_temp)
            return None

        image = (
            display.Image(tex_path.with_suffix(".png"))
            if rasterize
            else display.SVG(tex_path.with_suffix(".svg"))
        )

        if save_image:
            self._save(save_image, image_format)
        if save_tex:
            self._save(save_tex, "tex")
        if save_pdf:
            self._save(save_pdf, "pdf")
        if save_tikz and self.tikz_code:
            self._save(save_tikz, "tikz")

        self._clearup_latex_garbage(keep_temp)

        return image
    except Exception as e:
        raise e
    finally:
        self._clearup_latex_garbage(keep_temp)

jupyter_tikz.TexFragment

Bases: TexDocument

This class provides functionality to create and render a standalone LaTeX document given a TikZ Picture or LaTeX fragment.

Source code in jupyter_tikz/jupyter_tikz.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
class TexFragment(TexDocument):
    """This class provides functionality to create and render a standalone LaTeX document given a TikZ Picture or LaTeX fragment."""

    TMPL = Template(
        "\\documentclass{standalone}\n"
        + "$preamble"
        + "\\begin{document}\n"
        + "$scale_begin"
        + "$tikzpicture_begin"
        + "$code"
        + "$tikzpicture_end"
        + "$scale_end"
        + "\\end{document}"
    )
    TMPL_STANDALONE_PREAMBLE = Template(
        "$graphicx_package"
        + "$tikz_package"
        + "$tex_packages"
        + "$tikz_libraries"
        + "$pgfplots_libraries"
    )

    def __init__(
        self,
        code: str,
        implicit_tikzpicture: bool = False,
        scale: float = 1.0,
        preamble: str | None = None,
        tex_packages: str | None = None,
        tikz_libraries: str | None = None,
        pgfplots_libraries: str | None = None,
        no_tikz: bool = False,
        **kwargs,
    ):
        """Initializes the `TexFragment` class.

        Args:
            code: TikZ Picture or LaTeX fragment to render.
            implicit_tikzpicture: A flag to indicate if the final LaTeX should implicitly wrap a TikZ Picture.
            scale: The scale factor to apply to the TikZ/LaTeX code. It uses the `\\scalebox` command from graphicx package.
            preamble: LaTeX preamble to insert before the document.
            tex_packages: Comma-separated list of TeX packages.
            tikz_libraries: Comma-separated list of TikZ libraries.
            pgfplots_libraries: Comma-separated list of pgfplots libraries.
            no_tikz: Force to not import the TikZ package.

        Raises:
            ValueError: If `preamble` and (`tex_packages`, `tikz_libraries`, and/or `pgfplots_libraries`) are provided at the same time.
        """
        if preamble and (tex_packages or tikz_libraries or pgfplots_libraries):
            raise ValueError(_EXTRAS_CONFLITS_ERR)

        self.template = "tikzpicture" if implicit_tikzpicture else "standalone-document"
        self.scale = scale or 1.0
        if preamble:
            self.preamble = preamble.strip() + "\n"
        else:
            self.preamble = self._build_standalone_preamble(
                tex_packages, tikz_libraries, pgfplots_libraries, no_tikz
            )

        super().__init__(code, **kwargs)

    def _build_standalone_preamble(
        self,
        tex_packages: str | None = None,
        tikz_libraries: str | None = None,
        pgfplots_libraries: str | None = None,
        no_tikz: bool = False,
    ) -> str:
        tikz_package = "" if no_tikz else "\\usepackage{tikz}\n"

        graphicx_package = "" if self.scale == 1 else "\\usepackage{graphicx}\n"

        tex_packages = "\\usepackage{%s}\n" % tex_packages if tex_packages else ""
        tikz_libraries = (
            "\\usetikzlibrary{%s}\n" % tikz_libraries if tikz_libraries else ""
        )
        pgfplots_libraries = (
            "\\usepgfplotslibrary{%s}\n" % pgfplots_libraries
            if pgfplots_libraries
            else ""
        )

        return self.TMPL_STANDALONE_PREAMBLE.substitute(
            graphicx_package=graphicx_package,
            tikz_package=tikz_package,
            tex_packages=tex_packages,
            tikz_libraries=tikz_libraries,
            pgfplots_libraries=pgfplots_libraries,
        )

    @property
    def full_latex(self) -> str:
        """Returns the full LaTeX code to render."""
        if self.scale != 1:
            scale_begin = indent("\\scalebox{" + str(self.scale) + "}{\n", " " * 4)
            scale_end = indent("}\n", " " * 4)
        else:
            scale_begin = ""
            scale_end = ""

        if self.template == "tikzpicture":
            tikzpicture_begin = indent("\\begin{tikzpicture}\n", " " * 4)
            tikzpicture_end = indent("\\end{tikzpicture}\n", " " * 4)
            code_indent = " " * 8
        else:
            tikzpicture_begin = ""
            tikzpicture_end = ""
            code_indent = " " * 4

        code = indent(self._code, code_indent) + "\n" if self._code else ""

        return self.TMPL.substitute(
            preamble=self.preamble,
            scale_begin=scale_begin,
            tikzpicture_begin=tikzpicture_begin,
            code=code,
            tikzpicture_end=tikzpicture_end,
            scale_end=scale_end,
        )

full_latex: str property

Returns the full LaTeX code to render.

__init__(code, implicit_tikzpicture=False, scale=1.0, preamble=None, tex_packages=None, tikz_libraries=None, pgfplots_libraries=None, no_tikz=False, **kwargs)

Initializes the TexFragment class.

Parameters:

Name Type Description Default
code str

TikZ Picture or LaTeX fragment to render.

required
implicit_tikzpicture bool

A flag to indicate if the final LaTeX should implicitly wrap a TikZ Picture.

False
scale float

The scale factor to apply to the TikZ/LaTeX code. It uses the \scalebox command from graphicx package.

1.0
preamble str | None

LaTeX preamble to insert before the document.

None
tex_packages str | None

Comma-separated list of TeX packages.

None
tikz_libraries str | None

Comma-separated list of TikZ libraries.

None
pgfplots_libraries str | None

Comma-separated list of pgfplots libraries.

None
no_tikz bool

Force to not import the TikZ package.

False

Raises:

Type Description
ValueError

If preamble and (tex_packages, tikz_libraries, and/or pgfplots_libraries) are provided at the same time.

Source code in jupyter_tikz/jupyter_tikz.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
def __init__(
    self,
    code: str,
    implicit_tikzpicture: bool = False,
    scale: float = 1.0,
    preamble: str | None = None,
    tex_packages: str | None = None,
    tikz_libraries: str | None = None,
    pgfplots_libraries: str | None = None,
    no_tikz: bool = False,
    **kwargs,
):
    """Initializes the `TexFragment` class.

    Args:
        code: TikZ Picture or LaTeX fragment to render.
        implicit_tikzpicture: A flag to indicate if the final LaTeX should implicitly wrap a TikZ Picture.
        scale: The scale factor to apply to the TikZ/LaTeX code. It uses the `\\scalebox` command from graphicx package.
        preamble: LaTeX preamble to insert before the document.
        tex_packages: Comma-separated list of TeX packages.
        tikz_libraries: Comma-separated list of TikZ libraries.
        pgfplots_libraries: Comma-separated list of pgfplots libraries.
        no_tikz: Force to not import the TikZ package.

    Raises:
        ValueError: If `preamble` and (`tex_packages`, `tikz_libraries`, and/or `pgfplots_libraries`) are provided at the same time.
    """
    if preamble and (tex_packages or tikz_libraries or pgfplots_libraries):
        raise ValueError(_EXTRAS_CONFLITS_ERR)

    self.template = "tikzpicture" if implicit_tikzpicture else "standalone-document"
    self.scale = scale or 1.0
    if preamble:
        self.preamble = preamble.strip() + "\n"
    else:
        self.preamble = self._build_standalone_preamble(
            tex_packages, tikz_libraries, pgfplots_libraries, no_tikz
        )

    super().__init__(code, **kwargs)