Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
6ef2206
feat: TeXString to feed LaTeX into Makie's text() via AbstractTextPri…
jkrumbiegel May 14, 2026
96b83e6
feat(TeXString): Support align = (..., :baseline) via LaTeX box-depth…
jkrumbiegel May 15, 2026
309ee6d
refactor(TeXString): Fold baseline detection into the single render c…
jkrumbiegel May 15, 2026
c4e45d3
fix(TeXString): Compensate for PDF crop margin in alignment math
jkrumbiegel May 15, 2026
ab4b9a4
fix(TeXString): HiRes crop + standalone border=3pt for exact ink padding
jkrumbiegel May 15, 2026
02b854d
feat(CairoMakie): Vector-render CachedPDF markers via Poppler instead…
jkrumbiegel May 15, 2026
bfce63d
refactor(TeXString): Drop AbstractString conformance; opt in via Maki…
jkrumbiegel May 15, 2026
e2478ca
refactor: Replace TeXString with makietex_latex_handler for theming
jkrumbiegel May 15, 2026
0ac5ff4
feat: MakieTeXLaTeX struct as configurable, themable handler (preambl…
jkrumbiegel May 15, 2026
406fc5d
feat(MakieTeXLaTeX): Declare text_primitive_types = (ImageTextPrimiti…
jkrumbiegel May 15, 2026
28e6f74
chore: Drop text_primitive_types override; no longer needed with Plot…
jkrumbiegel May 16, 2026
e1df599
refactor: Emit PlotSpec directly into text_specs channel; drop ImageT…
jkrumbiegel May 16, 2026
30510dc
feat(GLMakieExt): Wire CachedPDF rasterization via rasterize_marker_f…
jkrumbiegel May 16, 2026
90f566b
refactor(rasterize_marker_for_gpu): Move override into MakieTeX core;…
jkrumbiegel May 16, 2026
71d4b8a
refactor: Centralize LaTeX rendering via text recipe; drop legacy types
jkrumbiegel May 17, 2026
df5d1f2
refactor(text): Hoist place_text! onto shared AbstractPdfTextHandler
jkrumbiegel May 18, 2026
b397c0b
feat(text): Add Typst / FullTypst text_handler
jkrumbiegel May 18, 2026
cf9bc39
refactor: Move LaTeX and Typst integration into package extensions
jkrumbiegel May 18, 2026
f938bd1
refactor(typst): Drop Makie.is_text_input piracy
jkrumbiegel May 18, 2026
66991ba
refactor: Drop legacy Typst markers; unify handler structs; trim deps
jkrumbiegel May 18, 2026
0d15ece
test: Rewrite suite as PixelMatch reftests; drop SVG support
jkrumbiegel May 18, 2026
00c4959
fix(latex): Pass --keep-logs to tectonic so baseline depth is parsed
jkrumbiegel May 18, 2026
4b73dae
test: Use PixelMatch.@test_pixelmatch directly; switch font_scaling t…
jkrumbiegel May 18, 2026
743add8
fix(typst): Skip text-line strut for display math so bbox is tight
jkrumbiegel May 18, 2026
5347286
fix(gpu): Pre-fill rasterization buffer with transparent black
jkrumbiegel May 18, 2026
e7ee9e0
feat: Add PDF and SVG scatter markers
jkrumbiegel May 19, 2026
80f753c
refactor: Eager-load PDF/SVG markers at construction, free on GC
jkrumbiegel May 19, 2026
51b6f0a
feat(markers): Preserve PDF/SVG aspect ratio under scalar markersize
jkrumbiegel May 19, 2026
e98ac03
test: Use transparent page fill in marker_pdf fixture
jkrumbiegel May 19, 2026
3907aee
test(markers): Label SVG marker fixture 'SVG' instead of single 'S'
jkrumbiegel May 19, 2026
c1f1563
ci: Use test env [sources] directly; gate xvfb to Linux; auto-pick arch
jkrumbiegel May 19, 2026
49dd1d7
compat: Allow Colors 0.13
jkrumbiegel May 19, 2026
52cb588
ci: Bump julia-actions/cache to v3 (saves on failure)
jkrumbiegel May 19, 2026
ac6a0db
ci: Back to standard julia-buildpkg + julia-runtest
jkrumbiegel May 19, 2026
2a41c78
ci: Gate GLMakie behind GPU availability
jkrumbiegel May 19, 2026
5d211e9
ci: Suppress auto-precompile around Pkg.rm(GLMakie)
jkrumbiegel May 19, 2026
6ca516e
test: Gate GLMakie load on CI + (apple|windows) instead of try/catch
jkrumbiegel May 19, 2026
00c9c42
test: Rename gate to SKIP_GLMAKIE; use plain conditional using
jkrumbiegel May 19, 2026
1c1862d
ci: Filter reference_images artifact to just failing tests
jkrumbiegel May 19, 2026
ede5106
test: Generate SVG/PDF marker fixtures via Typst with TGHM
jkrumbiegel May 19, 2026
d8989bf
test: Pin every LaTeX/Typst handler to TeX Gyre Heros Makie
jkrumbiegel May 19, 2026
7e512e6
fixup: Drop accidentally-committed .DS_Store
jkrumbiegel May 19, 2026
77ac983
ci: Wipe stale test/Manifest and instantiate after Pkg.rm(GLMakie)
jkrumbiegel May 19, 2026
2c16fae
ci: Use Pkg.update after Pkg.rm(GLMakie) to refresh manifest cleanly
jkrumbiegel May 19, 2026
e94355a
ci: Drop GLMakie via TOML edit, not Pkg.rm
jkrumbiegel May 19, 2026
56f1899
ci: Drop julia-buildpkg — julia-runtest's Pkg.test resolves on its own
jkrumbiegel May 19, 2026
288b2d0
ci: Skip windows-latest for now; bump docs cache to v3
jkrumbiegel May 19, 2026
2eb1e8a
docs: Rewrite around text_handler / PDF / SVG markers
jkrumbiegel May 19, 2026
f205e9d
feat(markers): 1-based page index for PDF; docs polish + Feynman demos
jkrumbiegel May 19, 2026
b771294
docs(typst): Move fletcher #import inline into the Label content
jkrumbiegel May 19, 2026
d49576c
docs(latex): Correct the compile-cost note for full mode
jkrumbiegel May 19, 2026
785bb65
docs: Add tellheight = false to Feynman Labels so neighbor axis isn't…
jkrumbiegel May 19, 2026
501cc99
docs: Drop manual colsize! in Feynman examples; auto width is the point
jkrumbiegel May 19, 2026
2743431
docs: Use full = true for both Feynman examples so all text matches
jkrumbiegel May 19, 2026
b94d7e4
docs: Lift Feynman diagram source into a local `diagram` variable
jkrumbiegel May 19, 2026
759e314
docs: Use DocumenterVitepress.deploydocs to handle per-base layout
jkrumbiegel May 19, 2026
0df6bae
docs: warnonly only locally; CI should fail on doc errors
jkrumbiegel May 19, 2026
dcbcbd1
docs: checkdocs = :none — internal helpers stay out of the docs
jkrumbiegel May 19, 2026
af9bc41
docs: checkdocs = :public
jkrumbiegel May 19, 2026
c0d761d
fix(latex): Always load xcolor in the template
jkrumbiegel May 19, 2026
83a990e
ci: Grant statuses: write so Documenter can post the docs preview link
jkrumbiegel May 19, 2026
10a46d4
rename: `full` -> `render_strings` on LaTeX / Typst handlers
jkrumbiegel May 19, 2026
4c60320
docs(index): Minimal example uses set_theme! + render_strings
jkrumbiegel May 19, 2026
795b2dd
docs(index): Plain-string xlabel; split x and ylabel onto separate lines
jkrumbiegel May 20, 2026
1b183b9
release: v0.5.0 (breaking)
jkrumbiegel May 20, 2026
f80efd9
ci: Enforce CHANGELOG.md update on PRs (skip with skip-changelog label)
jkrumbiegel May 20, 2026
d343245
docs(markers): Vector-of-SVGs example with Julia / Python / MATLAB logos
jkrumbiegel May 20, 2026
bdbf2d2
docs: Rename section 'Vector markers' -> 'PDF & SVG'
jkrumbiegel May 20, 2026
e904a6b
docs: Rename markers.md -> pdf_svg.md so URL matches the section title
jkrumbiegel May 20, 2026
cd67a5e
docs(readme): Generate README via Quarto with PixelMatch CI check
jkrumbiegel May 20, 2026
f62ef64
docs(pdf_svg): Reuse README barplot as the vector-of-SVGs example
jkrumbiegel May 20, 2026
121bf72
fix(readme): Use Pkg.instantiate instead of Pkg.develop so CI honors …
jkrumbiegel May 20, 2026
36e0aab
ci(readme): Pin Julia to 1.12.5 to match _readme/Manifest.toml
jkrumbiegel May 20, 2026
9ef247c
ci(readme): Drop committed Manifest; let CI instantiate from scratch
jkrumbiegel May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ tectonic_jll = "d7dd28d6-a5e6-559c-9131-7eb760cdacc5"

[weakdeps]
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"

[extensions]
MakieTeXCairoMakieExt = "CairoMakie"
MakieTeXGLMakieExt = "GLMakie"

[compat]
Cairo = "1.0.5"
CairoMakie = "0.12.2"
CairoMakie = "0.12.2, 0.16"
Colors = "0.9, 0.10, 0.11, 0.12"
DocStringExtensions = "0.8, 0.9"
Ghostscript_jll = "9"
Glib_jll = "2"
LaTeXStrings = "1"
Makie = "0.21.2"
Makie = "0.21.2, 0.25"
Poppler_jll = "21.9, 22, 23"
Rsvg = "1"
julia = "1.9"
Expand Down
31 changes: 29 additions & 2 deletions ext/MakieTeXCairoMakieExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ module MakieTeXCairoMakieExt

using CairoMakie, MakieTeX
using Makie
using Makie.MakieCore
using Poppler_jll
using Cairo
using Colors
Expand All @@ -21,11 +20,39 @@ end
CairoMakie.cairo_scatter_marker(v::AbstractArray{<: MakieTeX.AbstractDocument}) = CairoMakie.cairo_scatter_marker.(v)
CairoMakie.cairo_scatter_marker(v::NTuple{N, <: MakieTeX.AbstractDocument}) where N = CairoMakie.cairo_scatter_marker.(v)

# Vector-render a cached PDF (or TEX/Typst, which both wrap a PDF page) as a
# scatter marker. Mirrors CairoMakie's `Matrix{<:Colorant}` dispatch in its
# transform stack, but hands the page to Poppler, which renders content
# directly into the Cairo context — no raster intermediate, no resampling,
# hairlines stay crisp.
function CairoMakie.draw_marker(
ctx, marker::Union{MakieTeX.CachedPDF, MakieTeX.CachedTEX, MakieTeX.CachedTypst}, pos,
strokecolor #= unused =#, strokewidth #= unused =#, mat
)
w, h = marker.dims
document = MakieTeX.update_handle!(marker)
page = ccall(
(:poppler_document_get_page, Poppler_jll.libpoppler_glib),
Ptr{Cvoid}, (Ptr{Cvoid}, Cint), document, marker.doc.page
)
Cairo.translate(ctx, pos[1], pos[2])
CairoMakie.cairo_transform(ctx, mat)
Cairo.scale(ctx, 1.0 / w, 1.0 / h)
Cairo.translate(ctx, -w / 2, -h / 2)
ccall(
(:poppler_page_render, Poppler_jll.libpoppler_glib),
Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}), page, ctx.ptr
)
return
end

# # Teximg

# Override `is_cairomakie_atomic_plot` to allow `TeXImg` to remain a unit,
# instead of auto-decomposing into its component scatter plot.
CairoMakie.is_cairomakie_atomic_plot(plot::TeXImg) = true
# Disabled while bringing MakieTeX up against Makie >= 0.25 — the scatter
# child plot of the TeXImg recipe handles rendering on its own there.
# CairoMakie.is_cairomakie_atomic_plot(plot::TeXImg) = true

# # Scatter markers

Expand Down
39 changes: 33 additions & 6 deletions ext/MakieTeXGLMakieExt.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
module MakieTeXGLMakieExt
#=
This extension tells GLMakie how to rasterize a cached document, if it is required.
=#

# Rasterize cached LaTeX/PDF documents into a Matrix{ARGB32} so GLMakie's
# scatter marker pipeline can upload them as a GPU texture. CairoMakie has
# its own native vector dispatch (see MakieTeXCairoMakieExt) and does NOT
# go through this hook, so this extension only affects GL/WGL-style
# backends and doesn't compromise CairoMakie's vector quality.

using GLMakie, MakieTeX
using Makie

# Heuristic for rasterization density. `s` is per-marker pixel-size in
# markerspace (Vec2f); take its longest axis and scale by a factor so that
# 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.

s = scale isa AbstractVector ? first(scale) : scale
return _rasterize_doc(doc, _density_for_size(s))
end

function Makie.rasterize_marker_for_gpu(docs::AbstractVector{<:MakieTeX.AbstractCachedDocument}, scale)
# Per-marker scale if `scale` is a Vector, otherwise broadcast the
# scalar.
sizes = scale isa AbstractVector ? scale : fill(scale, length(docs))
return [_rasterize_doc(d, _density_for_size(sz)) for (d, sz) in zip(docs, sizes)]
end

# GLMakie.GLAbstraction.gl_convert(ct::MakieTeX.AbstractCachedDocument) = MakieTeX.rasterize(ct, #=scale=#1)
# GLMakie.GLAbstraction.gl_convert((ct::MakieTeX.AbstractCachedDocument, scale::Real)) = MakieTeX.rasterize(ct, scale)
# `MakieTeX.rasterize(::CachedTEX/CachedTypst, _)` currently ignores its
# `scale` arg; `page2img(…; render_density)` honors it. Route through that
# uniformly so fontsize controls bitmap resolution.
function _rasterize_doc(doc::MakieTeX.AbstractCachedDocument, density::Int)
return MakieTeX.page2img(doc, doc.doc isa Nothing ? 0 : doc.doc.page;
render_density = density)
end

end
end
4 changes: 3 additions & 1 deletion src/MakieTeX.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module MakieTeX

using Makie
using Makie.MakieCore

using Colors, LaTeXStrings, Typstry
using Base64
Expand Down Expand Up @@ -41,6 +40,8 @@ include("rendering/typst.jl")
include("rendering/pdf.jl")
include("rendering/svg.jl")

include("text_integration.jl")

export Cached
export TeXDocument, CachedTeX
export TEXDocument, CachedTEX
Expand All @@ -50,6 +51,7 @@ export SVGDocument, CachedSVG
export dvi2svg, latex2dvi, rsvg2recordsurf, svg2rsvg
export teximg, teximg!, TeXImg
export LTeX
export MakieTeXLaTeX, compile_latex_for_makie

export LaTeXStrings, LaTeXString, latexstring, @L_str
export Typstry, TypstString, @typst_str
Expand Down
248 changes: 248 additions & 0 deletions src/text_integration.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# Integration with Makie's `text` recipe via the `AbstractTextPrimitive` +
# `latex_handler` extension hooks (added in Makie 0.25). Lets MakieTeX
# render any `LaTeXString` passed into `text()` (and therefore Axis labels,
# titles, tick labels via `xtickformat`, etc.) with a real LaTeX engine
# instead of MathTeXEngine's in-process glyph approximation.
#
# Usage:
#
# set_theme!(latex_handler = MakieTeXLaTeX()) # global default
# with_theme(latex_handler = MakieTeXLaTeX(preamble = "…")) do … end # scoped override
# text!(scene, L"…"; latex_handler = MakieTeXLaTeX(preamble = "…")) # per-plot override
#
# Each `MakieTeXLaTeX` value carries its own preamble / class options /
# engine, so different text plots in the same figure can compile against
# different LaTeX setups (e.g. one Axis using `physics` package macros and
# another using `siunitx`).
#
# Without a handler set, Makie's default MathTeXEngine path is used — good
# for quick iteration; switch to MakieTeXLaTeX to polish a publication-
# quality figure without changing plot code (especially useful when the
# LaTeXStrings come from a third-party plotting function you don't control).
#
# See companion PR: https://github.com/MakieOrg/Makie.jl/pull/5632

# Scatter centers its marker on the position, then adds `marker_offset`. To
# make `position` correspond to the alignment edge of the marker, shift by
# half the marker size scaled by the alignment fraction. `valign === :baseline`
# is supported: `baseline_from_bottom` is the descender depth in markerspace.
function _latex_align_offset(align::Tuple, wh::Makie.Vec2f, baseline_from_bottom::Real = 0.0f0)
halign, valign = align
fhalign = halign === :left ? 0.0f0 :
halign === :center ? 0.5f0 :
halign === :right ? 1.0f0 : Float32(halign)
ox = (0.5f0 - fhalign) * wh[1]
oy = if valign === :baseline
0.5f0 * wh[2] - Float32(baseline_from_bottom)
else
fvalign = valign === :bottom ? 0.0f0 :
valign === :center ? 0.5f0 :
valign === :top ? 1.0f0 : Float32(valign)
(0.5f0 - fvalign) * wh[2]
end
return Makie.Vec2f(ox, oy)
end

const _DEFAULT_PREAMBLE = "\\usepackage{amsmath, amsfonts, xcolor}\n\\pagestyle{empty}\n\\nopagecolor"
const _DEFAULT_CLASSOPTIONS = "preview, tightpage, 12pt"

"""
MakieTeXLaTeX(; preamble, classoptions, engine, border_pt,
crop_margin_pt, base_pt)

A `latex_handler` for Makie's `text` recipe that renders `LaTeXString`
content with a real LaTeX engine (via MakieTeX). Construct one and pass it
to `set_theme!` / `with_theme` / a plot's `latex_handler` attribute.

# Fields

* `preamble` — LaTeX preamble (everything between `\\documentclass{…}` and
`\\begin{document}`). Default loads `amsmath, amsfonts, xcolor` and sets
`\\pagestyle{empty}\\nopagecolor` so the page background is transparent.
* `classoptions` — options to the standalone class (without `border=`, that
field is appended automatically from `border_pt`). Default
`"preview, tightpage, 12pt"`.
* `engine` — `nothing` (use `MakieTeX.CURRENT_TEX_ENGINE[]`) or a `Cmd` like
`` `lualatex` `` / `` `pdflatex` `` / `` `tectonic` ``.
* `border_pt` — pt margin built into the standalone page MediaBox; must be
larger than `crop_margin_pt` so the safety crop below has room to extend.
* `crop_margin_pt` — symmetric pt safety pad cropped around the ink, so
anti-aliased glyph edges aren't clipped by the page boundary.
* `base_pt` — LaTeX font size assumed in the document (Makie's `fontsize`
is scaled by `fontsize / base_pt`).
"""
Base.@kwdef struct MakieTeXLaTeX
preamble::String = _DEFAULT_PREAMBLE
classoptions::String = _DEFAULT_CLASSOPTIONS
engine::Union{Nothing, Cmd} = nothing
border_pt::Int = 3
crop_margin_pt::Float32 = 2.0f0
base_pt::Float32 = 12.0f0
end

"""
compile_latex_for_makie(h::MakieTeXLaTeX, latex_src::AbstractString)
-> (cached::CachedPDF, baseline_pt::Float32)

Compile a LaTeX snippet under the configuration `h` to a `CachedPDF` and
simultaneously extract the descender depth (`\\dp`) of the rendered box —
needed for `align = (:left, :baseline)`. Single LaTeX run: the content is
wrapped in `\\sbox` + `\\typeout`, then `\\usebox`'d, so the same compile
produces both the PDF that gets rendered and a log line we parse back for
the baseline. The returned `cached` document is cropped with a symmetric
`h.crop_margin_pt` margin to avoid clipping anti-aliased glyph edges.
"""
function compile_latex_for_makie(h::MakieTeXLaTeX, latex_src::AbstractString)
src = String(latex_src)
body = """
\\newsavebox\\makietexbaselinebox%
\\sbox\\makietexbaselinebox{$(src)}%
\\typeout{MAKIETEX_BASELINE_DEPTH=\\the\\dp\\makietexbaselinebox}%
\\usebox\\makietexbaselinebox
"""
# Build the TEXDocument ourselves so we can set `border=Npt` from the
# handler config — gives the page MediaBox enough room for our crop
# margin (otherwise the crop clamps to MediaBox and the margin
# disappears in y).
doc = TEXDocument(body, true;
requires = "\\RequirePackage{luatex85}",
preamble = h.preamble,
class = "standalone",
classoptions = "$(h.classoptions), border=$(h.border_pt)pt",
)
engine = h.engine === nothing ? CURRENT_TEX_ENGINE[] : h.engine
pdf, baseline_pt = _compile_latex_capture_baseline(String(doc.contents), engine, h.crop_margin_pt)
# `CachedTEX(::Vector{UInt8})` is broken upstream (it stashes `nothing`
# into a strictly-typed `doc::TEXDocument` field). `CachedPDF` works
# fine and is what `page2img` dispatches on anyway.
return CachedPDF(PDFDocument(pdf)), baseline_pt
end

# Like MakieTeX's `crop_pdf`, but reads Ghostscript's `%%HiResBoundingBox`
# (sub-pt precision) rather than the integer `%%BoundingBox`. With the
# integer version, glyph edges can be jittered by up to ~1pt within the
# crop, which propagates into visible baseline misalignment.
function _hires_pdf_bbox(path::String)
out = Pipe(); err = Pipe()
success(pipeline(`$(Ghostscript_jll.gs()) -q -dBATCH -dNOPAUSE -sDEVICE=bbox $path`, stdout = out, stderr = err))
close(out.in); close(err.in)
result = read(err, String)
m = match(r"%%HiResBoundingBox: ([-0-9.]+) ([-0-9.]+) ([-0-9.]+) ([-0-9.]+)", result)
m === nothing && error("could not extract %%HiResBoundingBox from gs output")
return parse.(Float64, (m.captures[1], m.captures[2], m.captures[3], m.captures[4]))
end

function _hires_crop_pdf(path::String, margin::Real)
bb = _hires_pdf_bbox(path)
crop_box = (bb[1] - margin, bb[2] - margin, bb[3] + margin, bb[4] + margin)
crop_cmd = join(crop_box, " ")
out_path = "_hires_cropped.pdf"
out = Pipe(); err = Pipe()
try
redirect_stderr(err) do
redirect_stdout(out) do
Ghostscript_jll.gs() do gs_exe
run(`$gs_exe -o $out_path -sDEVICE=pdfwrite -c "[/CropBox [$crop_cmd]" -c "/PAGES pdfmark" -f $path`)
end
end
end
catch e
finally
close(out.in); close(err.in)
end
return read(out_path)
end

# Mirror of MakieTeX.compile_latex's tempdir-and-latexmk pipeline, but also
# reads `temp.log` before the directory is torn down and pulls the baseline
# depth out of it. Returns the cropped PDF as `Vector{UInt8}` plus the depth
# in pt.
function _compile_latex_capture_baseline(document::String, engine::Cmd, crop_margin_pt::Real)
return mktempdir() do dir
cd(dir) do
write("temp.tex", document)
out = Pipe(); err = Pipe()
try
cmd = if engine == `tectonic`
`$(tectonic_jll.tectonic()) temp.tex`
else
`latexmk -file-line-error --shell-escape -cd -$(engine) -interaction=nonstopmode temp.tex`
end
run(pipeline(ignorestatus(cmd), stdout = out, stderr = err))
finally
close(out.in); close(err.in)
end
log_text = isfile("temp.log") ? read("temp.log", String) : ""
m = match(r"MAKIETEX_BASELINE_DEPTH=([-0-9.]+)pt", log_text)
baseline_pt = m === nothing ? 0.0f0 : max(0.0f0, parse(Float32, m.captures[1]))
pdf = _hires_crop_pdf("temp.pdf", crop_margin_pt)
return pdf, baseline_pt
end
end
end

# Makie calls the handler as `h(outputs, latex_str, i, N, _inputs...)`. We
# make `MakieTeXLaTeX` callable with that exact signature.
function (h::MakieTeXLaTeX)(
outputs::NamedTuple, latex_str::LaTeXString,
i, N, fontsize, font, align, rotation, justification,
lineheight, word_wrap_width, offset, fonts, color, strokecolor, strokewidth
)

fs = Float32(Makie.sv_getindex(fontsize, i))
al = Makie.sv_getindex(align, i)
rot = convert(Makie.Quaternionf, Makie.sv_getindex(rotation, i))
off = Makie.Vec3f(Makie.sv_getindex(offset, i))

# Single LaTeX compile: yields the rendered PDF + the box-depth in pt for
# `align = (..., :baseline)`. The `CachedPDF` is passed straight to scatter
# as the marker — CairoMakie's `draw_marker(::CachedPDF, …)` override (in
# MakieTeXCairoMakieExt) hands it to `poppler_page_render`, which puts
# the PDF's vector content directly into the figure's Cairo surface (no
# raster intermediate, so hairlines stay crisp).
cached, baseline_pt = compile_latex_for_makie(h, String(latex_str))

# `dim_pt` includes the safety pad `h.crop_margin_pt` on each side (added
# at crop time to keep anti-aliased glyphs from clipping). The natural
# ink box is `dim_pt - 2 * crop_margin_pt`. Alignment refers to the ink
# box so the safety pad doesn't shift positions.
scale = fs / h.base_pt
dim_pt = Makie.Vec2f(Float32(cached.dims[1]), Float32(cached.dims[2]))
target_size = dim_pt .* scale
ink_size = (dim_pt .- 2 * h.crop_margin_pt) .* scale
baseline_from_bottom = baseline_pt * scale

align_off = _latex_align_offset(al, ink_size, baseline_from_bottom)
marker_offset = Makie.Vec3f(align_off[1], align_off[2], 0) + off

# `text_blocks` must have one entry per input string. This block has no
# glyphs — only a Scatter spec — so we push an empty range.
curr = length(outputs.glyphindices)
push!(outputs.text_blocks, (curr + 1):curr)
push!(
outputs.glyphcollections, Makie.GlyphCollection(
UInt64[], Makie.NativeFont[], Makie.Point3f[], Makie.GlyphExtent[],
Makie.Vec2f[], Makie.Quaternionf[], Makie.RGBAf[], Makie.RGBAf[], Float32[]
)
)

# Emit the LaTeX render as a single Scatter PlotSpec. Positions are in
# block-relative markerspace (origin = the text block's anchor point);
# the text recipe shifts by the projected block position late, so we
# only need to supply a zero point here. `space`/`markerspace` are
# patched in by the recipe to match the parent text plot.
push!(outputs.text_specs, Makie.PlotSpec(:Scatter, Makie.Point3f[Makie.Point3f(0, 0, 0)];
marker = [cached],
markersize = [target_size],
marker_offset = [marker_offset],
rotation = [rot],
))
push!(outputs.text_spec_block_indices, i)

# Block-relative bbox: marker_offset is the center, ink box is target
# minus the 2*margin safety pad on each side; rotation is applied.
half = 0.5f0 .* Makie.Vec3f(target_size..., 0)
bb = Makie.Rect3d(Makie.to_ndim(Makie.Point3d, marker_offset, 0) .- half, Makie.Vec3d(target_size..., 0))
push!(outputs.text_spec_bboxes, Makie.rotate_bbox(bb, rot))
return
end
Loading
Loading