Skip to content

feat: support high-fidelity LaTeX math rendering - #4

Merged
jpfulton-fultonengineeringservices merged 5 commits into
mainfrom
feat/latex-math-rendering
Sep 14, 2026
Merged

jpfulton-fultonengineeringservices merged 5 commits into
mainfrom
feat/latex-math-rendering

Conversation

@jpfulton-fultonengineeringservices

Copy link
Copy Markdown
Contributor

Summary

Adds a math_mode option (legacy | latex) for equation rendering.

Vector display equations (new [math] extra)

  • New diagrams.math_latex module: ziamath LaTeX → SVG → svglib vector Drawing. Glyphs are drawn as SVG paths (svg2=False for renderer compat), so no font embedding is needed and equations stay crisp at any zoom.
  • Graceful per-formula fallback chain: ziamath vector → matplotlib mathtext PNG → clean_latex plain text. A bad formula never aborts a build.

Inline math images

  • text.fmt gains math_mode: latex mode recognises $...$, $$..$$ and \(..\) inline spans and renders them as small transparent PNGs embedded as base64 data-URI <img valign="middle"> tags.
  • Backtick code spans are protected before math extraction (so v$W^coef stays code), and Pandoc-style delimiter heuristics keep currency prose ("costs $5 and $10") as prose.

Parser

  • markdown.py: $$ display blocks, single-line \[..\] / $$..$$ equations, fence-state tracking in latex mode. Legacy path preserved byte-identically.

Dependencies

  • [math] and [all] extras gain ziamath>=0.13 + svglib>=1.5 (pure Python, MIT, py3.10+).

Test plan

  • 15 new unit tests for the renderers (vector drawing, PNG, data-URI tag, per-span fallbacks)
  • 5 new parser tests ($$ blocks, single-line forms, legacy regression, sidebar suppression)
  • 5 new fmt tests (dollar math, money heuristic, code protection, per-span fallback)
  • Integration test: single-markdown build in both modes
  • uv run ruff check + ruff format --check pass
  • uv run mypy src tests (strict) passes
  • uv run pytest: 173 passed, coverage 87.87% (gate 85%)
  • uv build succeeds

Add a math_mode option (legacy | latex) for equation rendering:

- new diagrams.math_latex module: ziamath LaTeX -> SVG -> svglib vector
  Drawing for display equations (glyphs drawn as SVG paths, no font
  embedding), degrading to matplotlib mathtext PNG then clean_latex
  plain text so a bad formula never aborts a build
- SingleMdConfig / MarkdownConfig gain math_mode plumbing through
  parse_section_lines and parse_md_file
- text.fmt gains math_mode: latex mode recognises $...$ and $$...$$
  inline spans (rendered as base64 data-URI <img> tags), protects
  backtick code spans before math extraction, and applies Pandoc-style
  delimiter heuristics so currency prose stays prose
- markdown parser: $$ display blocks, single-line \[..\] / $$..$$
  equations, fence-state tracking in latex mode
- [math] and [all] extras gain ziamath + svglib (pure Python, MIT)
- unit + integration tests for renderers, fallback chain, delimiter
  heuristics, and legacy-mode regression

Signed-off-by: J. Patrick Fulton <patrick@fultonengineeringservices.com>
svglib <2.0 declares rlpycairo (and therefore pycairo) as a hard
dependency; pycairo has no Linux/CI wheels and its meson build needs
libcairo2-dev. svglib 2.2.0 moved the raster backend behind the
optional "bitmaps" extra, so svg2rlg alone is pure-Python. Pin
svglib>=2.2,<3.0 in the [math]/[all] extras and dev group.

Signed-off-by: J. Patrick Fulton <patrick@fultonengineeringservices.com>
The inline renderer drew the formula into a fixed 6x1.2in axes and used
bbox_inches="tight", which cropped to the full axes rectangle: every
inline span became the same ~336pt-wide image with centred content.

Render via a figure-level text artist instead so tight-cropping follows
the glyphs, and return the expression's ink descent (from the mathtext
layout, in points) so make_inline_math_img can emit a negative numeric
valign. ReportLab interprets that as the image-bottom offset relative
to the baseline, so each formula now sits on the text baseline with a
width that tracks its content.

Signed-off-by: J. Patrick Fulton <patrick@fultonengineeringservices.com>
Signed-off-by: J. Patrick Fulton <patrick@fultonengineeringservices.com>
The previous fix cropped with bbox_inches="tight", which follows the
text *layout* box: it keeps the font's descent metric below the ink, so
every image carried phantom whitespace under the glyphs and formulas
floated above the surrounding text baseline.

Render the expression onto a figure sized exactly to the mathtext
layout box with the baseline at `depth` from the bottom (the approach
math_to_image uses, but transparent). The reported descent is then the
exact image-bottom-to-baseline distance, so ReportLab's numeric valign
puts the math baseline on the text baseline. Parse at 4x72 dpi and
scale to points to avoid whole-point depth quantisation.

Adds a regression test asserting a descender-free glyph's ink touches
the image bottom.

Signed-off-by: J. Patrick Fulton <patrick@fultonengineeringservices.com>
@jpfulton-fultonengineeringservices
jpfulton-fultonengineeringservices merged commit 274e3cd into main Sep 14, 2026
6 checks passed
@jpfulton-fultonengineeringservices
jpfulton-fultonengineeringservices deleted the feat/latex-math-rendering branch September 14, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant