Metadata-Version: 2.4
Name: retex
Version: 1.9.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Text Processing :: Markup :: LaTeX
Summary: Compile LaTeX documents to PDF, SVG, ODT, HTML and Markdown from Python, with retex, a LuaLaTeX-compatible TeX engine in Rust
Keywords: latex,tex,pdf,typesetting,luatex
Home-Page: https://gitlab.com/severin-lemaignan/retex
License-Expression: CC0-1.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://gitlab.com/severin-lemaignan/retex/-/blob/main/CHANGELOG.md
Project-URL: Documentation, https://severin-lemaignan.gitlab.io/retex/retex-manual.pdf
Project-URL: Homepage, https://gitlab.com/severin-lemaignan/retex
Project-URL: Issues, https://gitlab.com/severin-lemaignan/retex/-/issues
Project-URL: Repository, https://gitlab.com/severin-lemaignan/retex

# retex for Python

Compile LaTeX documents from Python, to PDF, SVG, ODT, ODP, HTML and
Markdown, with [retex](https://gitlab.com/severin-lemaignan/retex), a
TeX engine written in Rust whose output is pixel-identical to LuaLaTeX's.
retex runs in the Python process and starts no TeX program.

* Source code, issues and releases:
  <https://gitlab.com/severin-lemaignan/retex>
* The manual (PDF): <https://severin-lemaignan.gitlab.io/retex/retex-manual.pdf>
  ("retex in programs")
* Try retex in your browser: <https://severin-lemaignan.gitlab.io/retex>

```python
import retex

tex = retex.Retex()
doc = tex(r"""
\documentclass{article}
\begin{document}
Hello, \LaTeX{} from Python! $e^{i\pi} + 1 = 0$
\end{document}
""")

pdf = doc.to_pdf()        # bytes
svg = doc.to_svg()        # str: every page, stacked
odt = doc.to_odt()        # bytes
html = doc.to_html()      # str: one file, fonts and pictures in it
md = doc.to_markdown()    # str
doc.save("hello.pdf")     # or .svg, .odt, .odp, .html, .md
```

A build runs every pass the document needs, BibTeX, biber and makeindex
included. What it reported is in `doc.diagnostics`, `doc.errors` and
`doc.warnings`. A build that writes no output raises `retex.RetexError`,
whose `diagnostics` and `log` say why. In Jupyter, a document shows as
its pages (an SVG image; front ends that take PNG images show the first
page at 96 dpi).

Pages as images, drawn in the process (from the page's SVG, with
[resvg](https://github.com/linebender/resvg)):

```python
png = doc.to_png()                 # bytes: the first page at 150 dpi
a = doc.to_numpy(page=0, dpi=300)  # numpy array, uint8, (height, width, 3)
a = doc.to_numpy(alpha=True)       # (height, width, 4), transparent background
pages = doc.to_numpy(page=None)    # a list, one array per page
im = doc.to_pil(crop=True)         # a PIL image, cut down to the ink
```

Pages are numbered from 0 (negative numbers count from the end).
`crop=True` keeps what the page paints, with a margin of 2 points. For
a formula, use `\pagestyle{empty}` or the `standalone` class so that
the page number is not part of it. numpy and Pillow are needed only by
`to_numpy` and `to_pil`.

Options, all keyword arguments of `Retex`:

| option | meaning |
|---|---|
| `passes` | at most this many passes (default 5; 1 runs TeX once) |
| `shell_escape` | let `\write18` run programs (default `False`) |
| `accessible` | tagged PDF, checked for accessibility (PDF/UA-2) |
| `working_dir` | where the document's own files (pictures, `.bib`) are |
| `job_name` | the job name (default `document`) |
| `files` | `{name: str or bytes}`: files the document reads |
| `keep_files` | keep the build's files (log, `.aux`) in this directory |

`Retex.compile_file(path)` compiles a `.tex` file, its directory being
the working directory.

retex needs a TeX Live installation (2025 or 2026) for the LaTeX
packages and fonts. Builds release the GIL. They run one at a time in a
process, and change the current directory while they run.

Build the wheel with [maturin](https://www.maturin.rs/): `maturin build
--release` in this directory. `cargo xtask python` builds it and runs
`tests/`. For PyPI, build a manylinux wheel with `maturin build --release --zig
--compatibility manylinux_2_28` (with [zig](https://ziglang.org/) on
`PATH`).

## Examples

The scripts are in
[examples/](https://gitlab.com/severin-lemaignan/retex/-/tree/main/crates/retex-python/examples).

**A formula in a matplotlib figure**, typeset by LaTeX (amsmath, any
package) rather than by mathtext:

```python
formula = retex.compile(r"""\documentclass{standalone}
\begin{document}$\displaystyle \varphi(x) = \frac{1}{\sqrt{2\pi}}\, e^{-x^2/2}$
\end{document}""").to_numpy(dpi=300, alpha=True, crop=True)
ax.add_artist(AnnotationBbox(OffsetImage(formula, zoom=fig.dpi / 300),
                             (-2.6, 0.3), frameon=False))
```

**One PDF per person**: a template filled in and compiled in a loop
(certificates, letters, badges). A `Retex` keeps its options from one
build to the next:

```python
tex = retex.Retex(passes=2)
for name in ["Ada Lovelace", "Alan Turing", "Grace Hopper"]:
    tex(TEMPLATE.replace("@NAME@", name)).save(f"certificate-{name.split()[-1]}.pdf")
```

**A contact sheet**: every page of a document as a thumbnail, in one
image:

```python
pages = retex.Retex().compile_file("thesis.tex").to_pil(page=None, dpi=24)
w, h = pages[0].size
sheet = Image.new("RGB", (8 * w, rows * h), "white")
for i, page in enumerate(pages):
    sheet.paste(page, ((i % 8) * w, (i // 8) * h))
```

**A check for continuous integration**: build every document and print
its errors, with their file and line, as the `retex` program does:

```python
tex = retex.Retex()
for path in pathlib.Path("docs").rglob("*.tex"):
    try:
        problems = tex.compile_file(path).errors
    except retex.RetexError as e:
        problems = e.diagnostics
    for d in problems:
        print(d)   # docs/intro.tex:12: error: undefined command \sectoin
```

**An animated GIF**: numpy computes the curves, TikZ draws them, one
`tikzpicture` a frame. With the `standalone` class's `tikz` option each
picture is a page, and `page=None` gives every page as an image:

```python
a = np.linspace(0, 2 * np.pi, 300)
pictures = []
for degrees in range(0, 360, 5):
    t = np.radians(degrees)
    x, y = 2 * np.sin(3 * a + t), 2 * np.sin(2 * a)
    path = " -- ".join(f"({u:.3f},{v:.3f})" for u, v in zip(x, y))
    pictures.append(rf"\begin{{tikzpicture}} ... \draw {path}; \end{{tikzpicture}}")
doc = retex.compile(r"\documentclass[tikz]{standalone}\begin{document}"
                    + "\n".join(pictures) + r"\end{document}")
frames = doc.to_pil(page=None, dpi=72)
frames[0].save("lissajous.gif", save_all=True, append_images=frames[1:], duration=60, loop=0)
```

![Lissajous curves, a ribbon of colours, turning: an animated GIF](https://gitlab.com/severin-lemaignan/retex/-/raw/main/crates/retex-python/examples/images/lissajous.gif)

**TeX to pixels and back**: a glyph drawn as an array, then redrawn as a
TikZ picture of dots, one for each cell, as large as the cell is dark (a
vector halftone):

```python
ink = 1 - retex.compile(GLYPH).to_numpy(dpi=40).mean(axis=2) / 255
dots = [rf"\fill ({c},{-r}) circle ({0.55 * ink[r, c]**0.5:.3f});" for r, c in cells]
retex.compile(TIKZ_HEAD + "\n".join(dots) + TIKZ_TAIL).save("halftone.pdf")
```

![An italic ampersand redrawn as coloured TikZ dots](https://gitlab.com/severin-lemaignan/retex/-/raw/main/crates/retex-python/examples/images/halftone.png)

