Skip to content

feat: LaTeX and Typst text handlers for Makie's text_handler hook#73

Open
jkrumbiegel wants to merge 74 commits into
masterfrom
jk/text-primitive-integration
Open

feat: LaTeX and Typst text handlers for Makie's text_handler hook#73
jkrumbiegel wants to merge 74 commits into
masterfrom
jk/text-primitive-integration

Conversation

@jkrumbiegel

@jkrumbiegel jkrumbiegel commented May 14, 2026

Copy link
Copy Markdown
Member

Adds two text handlers — MakieTeX.LaTeX and MakieTeX.Typst — for Makie's proposed text_handler attribute. Both compile content to PDF, cache it, and place it through Makie's text recipe with align = (…, :baseline) support.

Requires the Makie branch at MakieOrg/Makie.jl#5632.

using CairoMakie, MakieTeX, tectonic_jll

with_theme(text_handler = MakieTeX.LaTeX()) do
    fig = Figure(size = (900, 600), fontsize = 25)
    ax = Axis(fig[1, 1];
        title  = L"\text{Roman tick labels via } \LaTeX",
        xlabel = L"\mathcal{X}",
        xticks = 0:2:10,
        xtickformat = vs -> [latexstring("\\mathrm{$(roman(v))}") for v in vs],
    )
    lines!(ax, 0:0.1:10, sin)
end
Real-LaTeX axis labels, title, and Roman-numeral xticks

API

  • MakieTeX.LaTeX(; full, preamble, classoptions, engine, border_pt, crop_margin_pt) — engine choice between lualatex / pdflatex / bundled tectonic_jll.
  • MakieTeX.Typst(; full, preamble, font, font_paths, crop_margin_pt).
  • LaTeXString / TypstString always route through the matching handler. full = true also routes plain AbstractString inputs (matplotlib's text.usetex analogue).
  • Works at the theme level (set_theme!, with_theme) or per-plot. Multiple handlers in one figure are fine.

Engine code lives in package extensions keyed on tectonic_jll / Typstry, so neither is a hard dep.

Output

  • CairoMakie renders cached PDFs vector-natively via draw_marker(::CachedPDF, …).
  • GLMakie / WGLMakie rasterize to ARGB32 at GPU upload via rasterize_marker_for_gpu.

Checks

  • existing teximg / LTeX recipes still work on Makie 0.25
  • live handler swap via plot.text_handler = …
  • CairoMakie PDF/SVG output stays vector
  • reference image suite (in test/reference_tests.jl)

Comment thread ext/MakieTeXGLMakieExt.jl Outdated
# a 30pt fontsize → ~density 4 (good visual fidelity without huge textures).
_density_for_size(s) = max(2, ceil(Int, maximum(s) / 8))

function Makie.rasterize_marker_for_gpu(doc::MakieTeX.AbstractCachedDocument, scale)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm what's scale here? Is it px_per_unit?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was markersize, but I don't think that part makes much sense yet.

The text recipe in Makie 0.25+ now routes `LaTeXString` content through a
`text_handler` extension hook with a two-phase `compile_text` /
`place_text!` protocol, so the standalone TeXImg / LTeX entry points and
the TEX/CachedTEX wrappers no longer carry their weight.

- Delete TeXImg recipe, LTeX block, text_utils helpers, and the
  test_recipe.jl dev scratchpad.
- Delete TEXDocument, TeXDocument, CachedTEX, CachedTeX (and the
  rendering/tex.jl compile_latex / latex2pdf helpers). LaTeX scaffolding
  is now inlined in compile_text; the compute graph at the text-recipe
  level provides the caching CachedTEX used to.
- Drop empty WGLMakie and RPRMakie extensions; GL/WGL rasterization
  goes through Makie.rasterize_marker_for_gpu.
- CairoMakieExt: drop stale old-signature draw_marker methods (CachedSVG
  variant and the legacy 8-arg overload) and the now-unreferenced
  RENDER_EXTRASAFE switch.

Implement the new two-phase protocol:

- `AbstractLaTeX` supertype with shared `_compile_latex_block` /
  `_escape_for_text_mode` helpers and a single `place_text!` method.
- `LaTeX` (claims `::LaTeXString` only — math/scientific use).
- `FullLaTeX` (claims `::LaTeXString` and plain `::AbstractString` with
  text-mode escaping — matplotlib usetex style).
- Color, fontsize, and lineheight are baked into the LaTeX source via
  `\definecolor` + `\color` and `\fontsize{}{}\selectfont`. The
  resulting PDF is already correctly sized and colored; CairoMakie
  renders the vector PDF directly via Poppler, GL backends rasterize
  through `rasterize_marker_for_gpu`.
- Inline `\textcolor{...}{...}` / `\fontsize{...}{...}` in user-supplied
  source override the outer wrapping in the natural LaTeX way.
Introduce AbstractPdfTextHandler supertype so the (CachedPDF, baseline_pt)
pipeline can be reused by additional handlers. Subtype AbstractLaTeX under
it and rename _latex_align_offset → _pdf_align_offset. Report ink_size
(layout box) as the bbox rather than the full marker dim so axis title
gaps and tick label padding don't include the crop_margin_pt antialias
pad.
Mirror of the LaTeX handler for the Typst engine. `text(typst"...")` or
plain strings (via FullTypst) compile through Typst and feed the same
PDF marker pipeline.

Implementation notes:
- `top-edge: "ascender", bottom-edge: "descender"` gives a content-
  independent line box.
- Math content doesn't inherit text edges, so a `#hide[Mp]` strut is
  prepended with a negative `#h(...)` retraction to establish text-line
  metrics on the line without adding horizontal whitespace.
- Descender measured from the "Mp" reference glyph and emitted as
  metadata, queried back via `typst query <makietex-baseline>`.
- Blank input returns `nothing` so empty Axis subtitles don't contribute
  phantom protrusions.

Bump Typstry compat 0.3 → 0.7 (Typst 0.14).
Typstry and tectonic_jll are now [weakdeps] — MakieTeX no longer pulls
them in by default. Users opt in by loading the respective package:

  using MakieTeX, tectonic_jll  # activates MakieTeXLaTeXExt
  using MakieTeX, Typstry       # activates MakieTeXTypstExt

The LaTeX/Typst handler struct definitions (LaTeX, FullLaTeX, Typst,
FullTypst) stay in core so they exist regardless of which extension is
loaded. Concrete compile_text dispatches and the engine-running code
live in the extensions. The shared place_text! / bbox / crop helpers
move to src/pdf_text_handler.jl.

Legacy TypstDocument / CachedTypst struct definitions stay in core; the
constructors that need Typst compilation move to MakieTeXTypstExt.

LaTeXStrings remains a hard dep (lightweight, transitively required by
Typstry too).
Now that Makie no longer needs an explicit opt-in for non-AbstractString
text types (single-arg `text()` shorthand removed), TypstString flows
through `compile_text` dispatch on the handler with no trait line
needed in our extension.
- Remove TypstDocument / CachedTypst / compile_typst / cached_doc /
  cached_pdf / update_handle!(::CachedTypst) and the matching legacy
  scatter-marker code in the Typst extension. The text_handler path is
  the only supported way to render Typst now. Drop test/typst.jl.
- Collapse `LaTeX` + `FullLaTeX` and `Typst` + `FullTypst` into single
  `LaTeX` and `Typst` structs with a `full::Bool` flag controlling
  whether plain `AbstractString` inputs are routed through the engine.
- Don't export `LaTeX` / `Typst` — too generic, and `Typst` collides
  with Typstry's export. Use as `MakieTeX.LaTeX` / `MakieTeX.Typst`.
- Typst handler gains a `font_paths::Vector{String}` field; the
  extension prepends those to `TYPST_FONT_PATHS`. Drop the hardcoded
  Julia Mono injection — Typstry's bundled font is no longer pulled in
  by default.
- Simplify the Typst compile call: drop `ignorestatus` + try/catch,
  let `run` fail naturally on engine errors.
- Use a multiline raw string literal for the default LaTeX preamble.
Wipe the legacy test suite (which referenced types we deleted earlier)
and start over with reference-image tests modelled on AlgebraOfGraphics:

- test/Project.toml pins the test env via [sources] to the Makie branch
  jk/breaking-0.25-work; drop once that branch lands. Test env is julia
  1.12+ so [sources] is available; main package compat stays at 1.9.
- test/reftest.jl: backend-aware reftest helper. Saves a recorded image,
  pixel-diffs against the committed reference, optionally updates via
  UPDATE_REFIMAGES=true.
- test/reference_tests.jl: figures covering the rework — axes with
  LaTeX/Typst handlers in full and per-element modes, Labels surrounded
  by Boxes for bbox visualization, and alignment grids with a red cross
  anchor for each halign × valign combo (including :baseline).
- test/runtests.jl runs both backends.

Also drops SVG support entirely (SVGDocument, CachedSVG, all of the
Rsvg-backed rendering path) — doesn't fit the text-primitive direction.
The LaTeX extension emits a \typeout{MAKIETEX_BASELINE_DEPTH=…} marker
and parses it from temp.log to compute the descender used by the
:baseline valign. tectonic discards its log unless --keep-logs is set,
so baseline_pt silently fell through to 0 and :baseline collapsed onto
the ink bottom (descenders sat on the anchor).
…o TGHM

Drop the custom reftest.jl harness in favor of PixelMatch's
@test_pixelmatch, which already handles save/compare/diff,
JULIA_REFERENCETESTS_UPDATE, and interactive ref updates. Each builder
in reference_tests.jl now returns a Makie.Figure; run_reftests calls a
small compare() helper that renders via Makie.colorbuffer and forwards
to the macro. Reference images are renamed to PixelMatch's underscore
convention (<name>_ref.png) and the .gitignore patterns updated to
match. Stale rec/diff intermediates removed.

font_scaling now uses TeXGyreHerosMakie-Regular.otf (Makie ships TGHM
in its asset dir) for all three handlers instead of the legacy
Helvetica.otf file. The Typst handler points at a scratch dir holding
only the Regular OTF because the four bundled TGHM variants share
broken OS/2 metadata (usWeightClass=500 and fsSelection=REGULAR for
every variant), so Typst can't pick a specific weight from the asset
dir and lands on Bold.
The compile template prepended a hidden 'Mp' strut to give the line
proper text-line metrics. For inline / mixed / plain-text content the
strut sits on the same line as the body and is invisible. For display
math ($ … $) the body is a Typst block, so the strut becomes its own
empty text line above the equation — the cached bbox then includes a
tall band of whitespace.

Detect block equations inside the context block via b.func() ==
math.equation and b.at("block", default: false), and omit the strut
in that case.
page2img seeded the Cairo ARGB32 surface with ARGB32(1, 1, 1, 0). That
violates Cairo's premultiplied-alpha invariant (alpha=0 requires rgb=0)
and the leftover white rgb leaked into anti-aliased glyph edges as
they were painted on top. The texture then carried gray-tinted edge
pixels (e.g. (0.553, 0.553, 0.553, 0.447) instead of (0, 0, 0, 0.447)),
which made GLMakie's rendered text look washed out compared to
CairoMakie's vector path. Seed with ARGB32(0, 0, 0, 0) so AA edges
stay pure black with partial alpha.
@jkrumbiegel jkrumbiegel changed the title feat: TeXString feeding LaTeX into Makie's text() via AbstractTextPrimitive feat: LaTeX and Typst text handlers for Makie's text_handler hook May 18, 2026
@asinghvi17

Copy link
Copy Markdown
Member

It looks like Claude got a little overzealous and dropped a bunch of functionality (notably svg and direct pdf rendering) which are part of the reason this package exists.

@jkrumbiegel

Copy link
Copy Markdown
Member Author

Nah that wasn't Claude by itself, that was me. I felt like the main reason this package exists was to include TeX (it's in the name) or Typst as the modern alternative, and then mostly for rendering special text (mostly complex math), not arbitrary pdf markers like the Wikipedia or Gopher scatter examples. So I reduced it down to the essentials as it's a breaking version anyway with the main changes.

We can also keep the pdf/svg API though if you feel like it's worth it.

The parent project doesn't need to be instantiated separately;
Pkg.test creates its own temp env from test/Project.toml's [sources]
and installs whatever it actually needs. Skipping buildpkg avoids a
stale parent manifest leaving the test env in an inconsistent state
(which was the Windows FFTA-listed-but-not-installed symptom).
Drops the old CachedTEX / teximg / LTeX content (those APIs are gone)
and starts over with four focused pages:

- index.md — Vitepress hero + minimal LaTeX example.
- latex.md — the LaTeX handler: quick start, full mode (matching fonts
  across the figure), and preambles with packages / macros (physics,
  siunitx). Realistic damped-oscillation and density-of-states examples.
- typst.md — same shape for the Typst handler. Default font config + a
  preamble that defines a kB shortcut.
- markers.md — PDF and SVG scatter markers using a committed Julia-dots
  asset (SVG + Typst-generated PDF), aspect-preservation note, mixing
  markers.

api.md is a small @docs index of the public types. Old Documenter.jl
references to deleted constants / cached types are gone.

docs/Project.toml trims to what's actually needed (Makie, CairoMakie,
ComputePipeline pinned to jk/breaking-0.25-work; Typstry + tectonic_jll
so the engines load in @example blocks). Drops Data.toml +
DataToolkit-era machinery.
- PDF marker page numbering is now 1-based (default `page = 1`).
  Internal conversion to Poppler's 0-based happens at the boundary.
- Doc examples drop explicit fontsize / Figure size — defaults look fine.
- Rewrote the latex.md 'Full mode' paragraph: the two paths use
  different fonts (not 'subtly different'), and the tradeoff is
  convenience vs speed.
- Removed the 'Mixing markers' subsection — covered by the Makie scatter
  vector-input convention.
- New 'Complex content in layouts' section on both latex.md and
  typst.md: tree-level e+e- → μ+μ- as a tikz-feynman / fletcher diagram
  in a `Label`, paired with the angular distribution as a plot.
  Shows the LaTeX preamble adding packages and Typst's
  `#import "@preview/..."` syntax.
Typst's #import can sit anywhere — no need to route it through the
handler's preamble. Drops the wrapping handler config and reads more
naturally.
LaTeX strings aren't cached across renders in the current compute-graph
flow — every save re-runs the engine on each text element. Reword the
'full mode' tradeoff to reflect that.
Keeps the Label() call short so the layout structure (Label + Axis) is
visible at a glance.
Documenter.deploydocs(target = "build") pushed the whole build dir
including the build/<i>/ subdir that DocumenterVitepress writes per
base, ending up at previews/PR73/1/. The DocumenterVitepress wrapper
reads bases.txt and deploys each base into the correct subfolder.
\definecolor{maincolor}{...} is part of the unconditional document
template, so xcolor must be loaded — otherwise a user preamble that
forgets to include xcolor would error out. Move the \usepackage{xcolor}
above the user preamble so custom preambles can focus on their own
packages.
The point of the flag is that plain `String` inputs also get rendered
through the engine (not just LaTeXString / TypstString). `full` was
vague; `render_strings` says it.
set_theme! reads more linearly than a do-block on first sight, and
render_strings = true keeps the figure in a single font for an
all-in-one first impression. set_theme!() reset is hidden via # hide.
Adds python.svg (Wikimedia) and matlab.svg (devicon) to docs/src/assets/
and a new `@example` block right under the single-marker SVG example
that scatter-plots six points with the three logos cycled per-point.

Also hardens `MakieTeX.SVG` to fall back to the SVG's viewBox when the
file has no explicit width/height attributes (which the devicon
matlab.svg doesn't). Previously librsvg returned a 0×0 intrinsic size
and the marker failed to render.
Adds _readme/ (README.qmd, make.jl, check.jl) and a readme.yml workflow
that renders via Quarto, text-diffs README.md against HEAD, and
PixelMatch's the rendered PNGs strictly (n_diff > 0 fails). On mismatch
the script writes _ref/_diff PNGs and the workflow uploads README.md +
README_files/ as a 'rendered-readme' artifact for download.
Quarto's notebook runner enforces an exact Julia patch match against the
manifest. Committing a Manifest pinned us to one Julia version forever;
let Pkg.instantiate generate one matching whatever Julia the workflow's
on, and the runner sees a self-consistent set.
@jkrumbiegel jkrumbiegel marked this pull request as ready for review May 22, 2026 07:39
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.

2 participants