texformules 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
13
+ # What the examples write.
14
+ *.pdf
15
+ *.png
16
+ !docs/**/*.pdf
17
+ !docs/**/*.png
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
4
+ [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [0.1.0] — 2026-10-18
7
+
8
+ First release on PyPI. The formulas were a module shared by two private projects; the
9
+ figures were the `latex_figures` package, never published.
10
+
11
+ ### Added
12
+
13
+ - **`split(text)`** cuts a line into text and LaTeX `Fragment`s on latexdetok's tree:
14
+ a formula takes its whole word, punctuation included, and a command keeps its
15
+ arguments even without braces; `%` comments are removed (`comments` returns them);
16
+ text commands, `~` and ligatures are rendered without LaTeX. The whole line is
17
+ checked by latexdetok, and a word with an error or a warning, or with a command
18
+ that would affect the other formulas of the batch, stays text, with its `Problem`s
19
+ located.
20
+ - **`FormulaBook(cache, preamble=…, engine=…)`** typesets the missing formulas in a
21
+ single compilation (`render`), and keeps each one as a grey transparent PNG and as
22
+ a one-page vector PDF, with its width, height and depth (`Formula`, and `PdfBox` in
23
+ PDF points). The cache key follows the content of every file the preamble reads
24
+ outside the distribution, recorded with `-recorder`. Rejected formulas keep TeX's
25
+ message, and the places of an undefined command (`Formula.marks`).
26
+ `Formula.to_json()` gives sizes in em for web pages.
27
+ - `default_cache()`, `default_preamble()` (amsmath, amssymb and siunitx),
28
+ `french_spacing()`, `BP_PER_PT`.
29
+ - **`texformules.figures`**, with the `figures` extra (matplotlib ≥ 3.8):
30
+ `apply_style`, `style_params`, `restore_style`, `figure_size`, `style_axes` (with
31
+ `decimal_comma`), `DecimalCommaFormatter`, `add_inset`, `save_figure` (cropped by
32
+ pdfcrop, ImageMagick or epstool), from `latex_figures` 0.1.0.
33
+ - The names of `latex_figures` (`latexify`, `parametres_latex`, `restaurer`,
34
+ `fig_size`, `format_axes`, `ScalarFormatterVirgule`, `add_subplot_axes`, `save_fig`,
35
+ `PREAMBULE`) as deprecated aliases, with their old keywords and defaults.
36
+ - `py.typed`, documentation in `docs/DOC.md`, examples, CI on Linux and macOS with
37
+ Python 3.13 and 3.14 and a job compiling with TeX Live, publication through PyPI
38
+ trusted publishing.
39
+
40
+ ### Changed, for those who used the unpublished code
41
+
42
+ - Comments, docstrings and error messages are in English: `rejected by LaTeX`,
43
+ `preamble: …`, `compilation interrupted`, `no LaTeX distribution on this machine`.
44
+ - Importing `texformules` no longer sets latexdetok's language to French nor lowers
45
+ its logger's level: call `latexdetok.set_language("fr")` and set the `latexdetok`
46
+ logger's level yourself.
47
+ - The preamble manifest is now `preamble-<hash>.json` (was `preambule-…`): formulas
48
+ cached before are typeset once more, under new keys.
49
+ - The figures' new default preamble loads amsmath and siunitx instead of French babel
50
+ and siunitx; the deprecated `latexify` and `parametres_latex` keep the old one.
51
+
52
+ [0.1.0]: https://github.com/antnardo/texformules/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 antnardo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,317 @@
1
+ Metadata-Version: 2.5
2
+ Name: texformules
3
+ Version: 0.1.0
4
+ Summary: LaTeX formulas typeset in one batch, cached as PNG and one-page PDF with their baseline; matplotlib figures styled for LaTeX documents.
5
+ Project-URL: Homepage, https://github.com/antnardo/texformules
6
+ Project-URL: Documentation, https://github.com/antnardo/texformules/blob/main/docs/DOC.md
7
+ Project-URL: Repository, https://github.com/antnardo/texformules
8
+ Project-URL: Issues, https://github.com/antnardo/texformules/issues
9
+ Project-URL: Changelog, https://github.com/antnardo/texformules/blob/main/CHANGELOG.md
10
+ Author-email: antnardo <antnardo@users.noreply.github.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: baseline,formula,latex,math,matplotlib,pdf,png,reportlab,siunitx
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Education
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Natural Language :: English
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Printing
25
+ Classifier: Topic :: Scientific/Engineering :: Visualization
26
+ Classifier: Topic :: Text Processing :: Markup :: LaTeX
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.13
29
+ Requires-Dist: latexdetok>=0.7.0
30
+ Requires-Dist: pillow>=10.0
31
+ Requires-Dist: pypdfium2>=4.30
32
+ Provides-Extra: figures
33
+ Requires-Dist: matplotlib>=3.8; extra == 'figures'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # texformules
37
+
38
+ [![CI](https://github.com/antnardo/texformules/actions/workflows/ci.yml/badge.svg)](https://github.com/antnardo/texformules/actions/workflows/ci.yml)
39
+ [![PyPI](https://img.shields.io/pypi/v/texformules.svg)](https://pypi.org/project/texformules/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/texformules.svg)](https://pypi.org/project/texformules/)
41
+ [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/antnardo/texformules/blob/main/LICENSE)
42
+
43
+ Real LaTeX formulas for documents built in Python. texformules cuts a line of text
44
+ into prose and LaTeX fragments, typesets all the missing fragments in **one LaTeX
45
+ compilation**, and keeps each one in a cache as a PNG and as a **one-page PDF**,
46
+ together with its **baseline**, so that it sits on the line of the surrounding text.
47
+ An optional extra, `texformules.figures`, styles matplotlib figures to match the same
48
+ LaTeX documents.
49
+
50
+ ```bash
51
+ pip install texformules # formulas
52
+ pip install "texformules[figures]" # and the matplotlib figures
53
+ ```
54
+
55
+ ```python
56
+ from texformules import FormulaBook, default_cache, split
57
+
58
+ book = FormulaBook(default_cache())
59
+ fragments = split(r"Kinetic energy theorem: $\Delta E_c = W(\vec F)$, 2 marks")
60
+ formulas = book.render(f.text for f in fragments if f.latex)
61
+
62
+ for fragment in fragments:
63
+ if fragment.latex and formulas[fragment.text].ok:
64
+ formula = formulas[fragment.text]
65
+ print(book.pdf(formula.key), formula.pdf_box) # page, and its box in PDF points
66
+ else:
67
+ print(fragment.text)
68
+ ```
69
+
70
+ Full reference: [docs/DOC.md](https://github.com/antnardo/texformules/blob/main/docs/DOC.md).
71
+
72
+ ## Why
73
+
74
+ Formulas written for a LaTeX document use the macros of its preamble: `\SI` from
75
+ siunitx, `\vect`, `\dd`, whatever the author defined. Nothing short of LaTeX knows
76
+ them: not a conversion to Unicode, not matplotlib's mathtext, not KaTeX. So when a
77
+ Python program has to show those formulas — in a PDF report built with reportlab, on
78
+ a web page, in a desktop application — the only faithful renderer is LaTeX itself,
79
+ with the same preamble.
80
+
81
+ Calling LaTeX once per formula is slow: loading a large preamble alone takes seconds.
82
+ texformules sends all the formulas still missing from the cache to a single
83
+ compilation, then keeps each of them:
84
+
85
+ - **a PNG**, grey and transparent, at the resolution you ask for (288 dpi by default,
86
+ 600 for print);
87
+ - **a one-page PDF**, the very page LaTeX produced, cut out of the batch: vector, sharp
88
+ at any zoom and on any printer;
89
+ - **its box**: width, height and depth in TeX points, and in PDF points through
90
+ `Formula.pdf_box`. The depth puts the baseline of the formula on the baseline of
91
+ the text.
92
+
93
+ The next request for the same formula, with the same preamble, is served from the
94
+ cache without LaTeX.
95
+
96
+ ## Requirements
97
+
98
+ - Python 3.13 or later (latexdetok, which reads the LaTeX, requires it).
99
+ - A TeX distribution on the `PATH` for anything to be typeset: TeX Live, MacTeX or
100
+ MiKTeX, with `lualatex` (preferred) or `pdflatex`, `kpsewhich`, and the `preview`
101
+ package. The default preamble also needs `amsmath`, `amssymb` and `siunitx`. On
102
+ Debian or Ubuntu:
103
+
104
+ ```bash
105
+ sudo apt-get install texlive-luatex texlive-latex-extra texlive-science preview-latex-style
106
+ ```
107
+
108
+ - Without TeX, the package still imports and splits text; `FormulaBook.available` is
109
+ false and every formula comes back rejected, with the reason, so that you can show
110
+ the source instead.
111
+
112
+ The Python dependencies are latexdetok, pypdfium2 (renders and cuts the PDF pages,
113
+ no dvipng nor ImageMagick) and Pillow. The `figures` extra adds matplotlib.
114
+
115
+ ## Splitting text
116
+
117
+ `split(text)` reads the line as TeX would (with latexdetok) and returns a list of
118
+ `Fragment`s: `text`, `latex` (true for a fragment to typeset) and `problems`.
119
+
120
+ ```python
121
+ >>> [(f.text, f.latex) for f in split(r"so ($x=1$), then \SI{5e-8}N % note to self")]
122
+ [('so ', False), ('($x=1$),', True), (' then ', False), ('\\SI{5e-8}N', True)]
123
+ ```
124
+
125
+ - A LaTeX fragment covers the whole word around the formula, punctuation included,
126
+ so that a line break never leaves "(" or "," alone; and a command keeps its
127
+ arguments, even without braces (`\SI{5e-8}N`).
128
+ - `%` comments are removed: they are notes for the author, not for the reader
129
+ (`comments(text)` returns them).
130
+ - What TeX would print as plain text is translated without LaTeX: `~`, `--`, `---`,
131
+ `\og`/`\fg`, `\%`, group braces…
132
+ - The whole line is checked by latexdetok. A word with an error or a warning (a `$`
133
+ never closed, `_` outside math, `\input` or `\def` inside a formula) is **not** sent
134
+ to LaTeX, where it would break the batch: it stays as typed, and its `problems`
135
+ say where (`start`, `end`), how bad (`severity`) and why (`message`,
136
+ `suggestion`).
137
+
138
+ `french_spacing(text)` sets the no-break spaces of French typography, for the text
139
+ fragments of French documents.
140
+
141
+ ## Rendering
142
+
143
+ ```python
144
+ from texformules import FormulaBook, default_cache
145
+
146
+ book = FormulaBook(default_cache(), dpi=600, size=10)
147
+ formulas = book.render([r"$\frac{a}{b}$", r"\SI{9.81}{m/s^2}", r"$\oops$"])
148
+
149
+ ok = formulas[r"$\frac{a}{b}$"]
150
+ ok.width, ok.height, ok.depth # TeX points, half-point border included
151
+ ok.pdf_box # PdfBox(width, height, depth) in PDF points
152
+ book.image(ok.key) # Path to the PNG
153
+ book.pdf(ok.key) # Path to the one-page PDF
154
+
155
+ bad = formulas[r"$\oops$"]
156
+ bad.ok, bad.error, bad.marks # False, 'Undefined control sequence.', ((1, 6),)
157
+ ```
158
+
159
+ `render` returns a dict keyed by source. A formula LaTeX rejects keeps its error
160
+ message and, for an undefined command, the places of that command in its source
161
+ (`marks`), so you can highlight them. `Formula.to_json()` gives a JSON-ready summary
162
+ (size in em for web pages, or the error).
163
+
164
+ `render` holds a lock: one `FormulaBook` can be shared by the threads of a web server.
165
+ pdfium itself is not thread-safe: if other code in your process uses pypdfium2, hold
166
+ your own lock around `render` as well.
167
+
168
+ ## With reportlab-layout
169
+
170
+ [reportlab-layout](https://pypi.org/project/reportlab_layout/) sets a PDF page inline,
171
+ on the baseline, with `inline_pdf` (version 1.10 with the `pdf` extra), and an image
172
+ with `inline_image`. Both take the box of `Formula.pdf_box`:
173
+
174
+ ```python
175
+ from xml.sax.saxutils import escape
176
+
177
+ from reportlab_layout import inline_pdf
178
+ from texformules import FormulaBook, default_cache, split
179
+
180
+ book = FormulaBook(default_cache())
181
+
182
+ def markup(text: str) -> str:
183
+ fragments = split(text)
184
+ formulas = book.render(f.text for f in fragments if f.latex)
185
+ parts = []
186
+ for fragment in fragments:
187
+ formula = formulas.get(fragment.text) if fragment.latex else None
188
+ if formula is not None and formula.ok:
189
+ box = formula.pdf_box
190
+ parts.append(inline_pdf(book.pdf(formula.key), width=box.width,
191
+ height=box.height, depth=box.depth))
192
+ else:
193
+ parts.append(escape(fragment.text)) # french_spacing(...) first, for French
194
+ return "".join(parts)
195
+ ```
196
+
197
+ Pass the markup to reportlab-layout's `InlineParagraph` (a plain `Paragraph` cannot
198
+ draw PDF pages); the paragraph style needs `autoLeading="max"`. A rejected formula
199
+ falls back to its source here; set it in a monospaced font if you prefer. reportlab-layout writes each font
200
+ stream once, however many formulas share it, so a page of vector formulas weighs less
201
+ than the same page with 600 dpi PNGs.
202
+
203
+ ## Elsewhere
204
+
205
+ - **Web pages**: serve `book.image(key)` and use `formula.to_json()`: `width`,
206
+ `height` and `depth` are in em of the type size, so `height: {height}em;
207
+ vertical-align: -{depth}em` sets the image on the baseline at any font size.
208
+ - **Other PDF libraries**: the one-page PDF can be laid down as a form XObject; its
209
+ MediaBox is the box, and the baseline lies `pdf_box.depth` above its bottom.
210
+
211
+ ## Your own preamble
212
+
213
+ By default the preamble loads amsmath, amssymb and siunitx. Pass any preamble, without
214
+ `\documentclass` (the batch uses `book` at 10 pt):
215
+
216
+ ```python
217
+ book = FormulaBook(
218
+ default_cache(),
219
+ preamble=r"""
220
+ \usepackage{amsmath,amssymb}
221
+ \usepackage{siunitx}
222
+ \usepackage{fontspec}\setmainfont{TeX Gyre Pagella}
223
+ \newcommand\vect[1]{\vec{#1}}
224
+ \input{mymacros}
225
+ """,
226
+ engine="lualatex",
227
+ )
228
+ ```
229
+
230
+ - `engine` is a name or a path; by default `lualatex`, else `pdflatex`. fontspec needs
231
+ lualatex (or xelatex).
232
+ - `size` is the type size in TeX points, `dpi` the PNG resolution; both enter the
233
+ cache key.
234
+ - A preamble may `\input` files of yours: they are followed (see below).
235
+ - `default_preamble()` uses `\input{ampackages}` instead when `kpsewhich` finds that
236
+ file: it is the author's own macro suite, absent anywhere else.
237
+
238
+ ## The cache
239
+
240
+ `default_cache()` is `~/Library/Caches/texformules` on macOS,
241
+ `%LOCALAPPDATA%\texformules\Cache` on Windows and `$XDG_CACHE_HOME/texformules`
242
+ (`~/.cache/texformules` by default) elsewhere; any folder will do. Each formula is stored under a
243
+ 24-character key as `<key>.json`, `<key>.png` and `<key>.pdf`, written atomically, so
244
+ several programs can share the folder.
245
+
246
+ The key is a hash of the source, the engine, the preamble, the size, the resolution,
247
+ and **the content of every file the preamble reads outside the TeX distribution**.
248
+ Each compilation runs with `-recorder`; the files listed in its `.fls` that do not
249
+ belong to the distribution (your `.tex`, `.sty`, `.lua`… including `TEXMFHOME`) are
250
+ recorded in a manifest, `preamble-<hash>.json`. Edit a macro in one of them and the
251
+ next batch typesets the formulas again. Data files (`.py`, `.csv`…) are not followed:
252
+ they change often, and never a formula.
253
+
254
+ To clear the cache, delete the folder. Rejections that are the formula's own fault
255
+ are cached too; those due to the preamble, an interrupted compilation or a missing
256
+ TeX are not, and are retried.
257
+
258
+ ## Figures
259
+
260
+ `texformules.figures` (extra `figures`) styles matplotlib for figures inserted in a
261
+ LaTeX document: sizes in centimetres, LaTeX fonts and preamble, a decimal comma that
262
+ does not touch the locale, insets, and cropping to the ink.
263
+
264
+ ```python
265
+ import matplotlib.pyplot as plt
266
+ import numpy as np
267
+ from texformules.figures import apply_style, save_figure, style_axes
268
+
269
+ apply_style(8, 6) # 8 cm by 6 cm, before creating the figure
270
+ x = np.linspace(-4, 4, 500)
271
+ fig, ax = plt.subplots()
272
+ ax.plot(x, np.sinc(x) ** 2)
273
+ ax.set_xlabel(r"$\theta$ (\unit{\radian})") # siunitx is in the preamble
274
+ style_axes(ax) # grey spines, outward ticks, 0,5 and ×10⁵
275
+ save_figure(fig, "slit.pdf") # saved, then cropped by pdfcrop
276
+ ```
277
+
278
+ `apply_style` changes `rcParams` for every figure to come, `restore_style()` undoes it,
279
+ and `style_params(...)` returns the same settings for `plt.rc_context`. The figures
280
+ came from the `latex_figures` package; its names (`latexify`, `format_axes`,
281
+ `save_fig`, `restaurer`, `ScalarFormatterVirgule`…) still work, as deprecated aliases
282
+ listed in DOC.md.
283
+
284
+ ## Limits
285
+
286
+ - One formula is one line of the batch document: display environments that need
287
+ several paragraphs (`align` outside math…) are not supported; `$$…$$` and `\[…\]`
288
+ are set inline in `\displaystyle`.
289
+ - Commands that would affect the other formulas of the batch — definitions, file
290
+ input and output, catcode changes, the `document` environment — are refused inside
291
+ a formula. Define your macros in the preamble.
292
+ - Only an undefined command is located inside a rejected formula; for other errors,
293
+ the message is all TeX gives.
294
+ - Each one-page PDF embeds the fonts subset for its whole batch: on its own it is
295
+ heavier than its PNG (about 15 kB against 6 kB); it pays off when the document
296
+ embeds each font stream once.
297
+ - latexdetok's messages are in English; for French, call
298
+ `latexdetok.set_language("fr")` or set `LATEXDETOK_LANG=fr`.
299
+
300
+ ## Development
301
+
302
+ ```bash
303
+ uv sync --group dev
304
+ uv run ruff format src tests examples
305
+ uv run ruff check src tests examples
306
+ uv run mypy
307
+ uv run pytest -q # the `latex` tests are skipped without TeX
308
+ uv run pytest -q -m latex # only those that compile
309
+ uv build
310
+ ```
311
+
312
+ The version is `texformules.__version__`; a GitHub release tagged `v<version>`
313
+ publishes to PyPI through trusted publishing.
314
+
315
+ ## License
316
+
317
+ MIT, see [LICENSE](https://github.com/antnardo/texformules/blob/main/LICENSE).