From 812a69cefed7af7564601ec1d9aa7b34687e0ea0 Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sat, 8 Jun 2024 12:01:45 -0700 Subject: [PATCH] Implement Typst backend (#48) --- Project.toml | 4 +- ext/MakieTeXCairoMakieExt.jl | 9 +- src/MakieTeX.jl | 7 +- src/layoutable.jl | 56 ++++++++++-- src/recipe.jl | 46 ++++++++-- src/rendering/pdf.jl | 22 ++--- src/rendering/typst.jl | 105 ++++++++++++++++++++++ src/types.jl | 165 +++++++++++++++++++++++++++++++---- 8 files changed, 366 insertions(+), 48 deletions(-) create mode 100644 src/rendering/typst.jl diff --git a/Project.toml b/Project.toml index a55ba3a..4bfc1aa 100644 --- a/Project.toml +++ b/Project.toml @@ -14,7 +14,7 @@ LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" Poppler_jll = "9c32591e-4766-534b-9725-b71a8799265b" Rsvg = "c4c386cf-5103-5370-be45-f3a111cca3b8" -Typst_jll = "eb4b1da6-20f6-5c66-9826-fdb8ad410d0e" +Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e" tectonic_jll = "d7dd28d6-a5e6-559c-9131-7eb760cdacc5" [weakdeps] @@ -35,7 +35,7 @@ Makie = "0.21.2" Poppler_jll = "21.9, 22, 23" Rsvg = "1" julia = "1.9" -Typst_jll = "0" +Typstry = "0.2" tectonic_jll = "0" [extras] diff --git a/ext/MakieTeXCairoMakieExt.jl b/ext/MakieTeXCairoMakieExt.jl index bb0ddcd..9995610 100644 --- a/ext/MakieTeXCairoMakieExt.jl +++ b/ext/MakieTeXCairoMakieExt.jl @@ -23,9 +23,9 @@ CairoMakie.cairo_scatter_marker(v::NTuple{N, <: MakieTeX.AbstractDocument}) wher # # Teximg -# Override `is_cairomakie_atomic_plot` to allow `TeXImg` to remain a unit, +# Override `is_cairomakie_atomic_plot` to allow `TeXImg` and `TypstImg` to remain a unit, # instead of auto-decomposing into its component scatter plot. -CairoMakie.is_cairomakie_atomic_plot(plot::TeXImg) = true +CairoMakie.is_cairomakie_atomic_plot(plot::Union{TeXImg, TypstImg}) = true # # Scatter markers @@ -81,7 +81,8 @@ function CairoMakie.draw_marker(ctx, marker::MakieTeX.CachedSVG, pos, scale, end -function CairoMakie.draw_marker(ctx, marker::Union{MakieTeX.CachedTEX, MakieTeX.CachedTeX, MakieTeX.CachedPDF}, pos, scale, +function CairoMakie.draw_marker(ctx, marker::Union{MakieTeX.CachedTEX, MakieTeX.CachedTeX, MakieTeX.CachedTypst, MakieTeX.CachedPDF}, + pos, scale, strokecolor #= unused =#, strokewidth #= unused =#, marker_offset, rotation) # get dimensions @@ -259,4 +260,4 @@ function CairoMakie.draw_plot(scene::Makie.Scene, screen::CairoMakie.Screen, img end -=# \ No newline at end of file +=# diff --git a/src/MakieTeX.jl b/src/MakieTeX.jl index 65b7646..ad9a177 100644 --- a/src/MakieTeX.jl +++ b/src/MakieTeX.jl @@ -3,7 +3,7 @@ module MakieTeX using Makie using Makie.MakieCore -using Colors, LaTeXStrings +using Colors, LaTeXStrings, Typstry using Base64 # Patch for Makie.jl `@Block` macro error @@ -36,6 +36,7 @@ include("layoutable.jl") include("rendering/pdf_utils.jl") include("rendering/tex.jl") +include("rendering/typst.jl") include("rendering/pdf.jl") include("rendering/svg.jl") @@ -46,9 +47,11 @@ export PDFDocument, CachedPDF export SVGDocument, CachedSVG export dvi2svg, latex2dvi, rsvg2recordsurf, svg2rsvg export teximg, teximg!, TeXImg -export LTeX +export typstimg, typstimg!, TypstImg +export LTeX, LTypst export LaTeXStrings, LaTeXString, latexstring, @L_str +export Typstry, TypstString, @typst_str "Try to write to `engine` and see what happens" function try_tex_engine(engine::Cmd) diff --git a/src/layoutable.jl b/src/layoutable.jl index 3cbb4b7..e4214cb 100644 --- a/src/layoutable.jl +++ b/src/layoutable.jl @@ -33,23 +33,63 @@ Makie.@Block LTeX begin end end +Makie.@Block LTypst begin + @attributes begin + "The Typst code to be compiled and drawn. Can be a String, a TypstDocument or a CachedTypst." + typst = "\\Typst" + "The density of pixels rendered (1 means 1 px == 1 pt)" + render_density::Int = 1 + "Controls if the graphic is visible." + visible::Bool = true + "A scaling factor to resize the graphic." + scale::Float32 = 1.0 + "The horizontal alignment of the graphic in its suggested boundingbox" + halign = :center + "The vertical alignment of the graphic in its suggested boundingbox" + valign = :center + "The counterclockwise rotation of the graphic in radians." + rotation::Float32 = 0f0 + "The extra space added to the sides of the graphic boundingbox." + padding = (0f0, 0f0, 0f0, 0f0) + "The height setting of the graphic." + height = Auto() + "The width setting of the graphic." + width = Auto() + "Controls if the parent layout can adjust to this element's width" + tellwidth::Bool = true + "Controls if the parent layout can adjust to this element's height" + tellheight::Bool = true + "The align mode of the graphic in its parent GridLayout." + alignmode = Inside() + end +end + LTeX(x, tex; kwargs...) = LTeX(x; tex = tex, kwargs...) +LTypst(x, typst; kwargs...) = LTypst(x; typst = typst, kwargs...) + +code(lt::LTeX) = lt.tex +code(lt::LTypst) = lt.typst + +img!(::Type{LTeX}, args...; kwargs...) = teximg!(args...; kwargs...) +img!(::Type{LTypst}, args...; kwargs...) = typstimg!(args...; kwargs...) -_to_cachedtex(x) = CachedTEX(x) -_to_cachedtex(x::AbstractDocument) = Cached(x) +_to_cached(::Type{LTeX}, x) = CachedTEX(x) +_to_cached(::Type{LTypst}, x) = CachedTypst(x) +_to_cached(::Type, x::AbstractDocument) = Cached(x) -function Makie.initialize_block!(l::LTeX) +function Makie.initialize_block!(l::T) where T <: Union{LTeX, LTypst} + _code = code(l) topscene = l.blockscene layoutobservables = l.layoutobservables textpos = Observable([Point3f(0, 0, 0)]) scale = Observable([Vec2f(1.0, 1.0)]) - cached_tex = lift(collect ∘ tuple ∘ _to_cachedtex, l.tex) + cached = lift(collect ∘ tuple ∘ (x -> _to_cached(T, x)), _code) - t = teximg!( - topscene, cached_tex; position = textpos, visible = l.visible, + t = img!(T, + topscene, cached; position = textpos, visible = l.visible, scale = scale, align = (:bottom, :left), rotations = l.rotation, markerspace = :pixel, @@ -58,7 +98,7 @@ function Makie.initialize_block!(l::LTeX) textbb = Ref(BBox(0, 1, 0, 1)) - onany(l.tex, l.scale, l.rotation, l.padding) do tex, scale, rotation, padding + onany(_code, l.scale, l.rotation, l.padding) do code, scale, rotation, padding textbb[] = rotatedrect(Makie.Rect2f(0,0,(t[1][][1].dims .* scale)...), rotation) autowidth = Makie.width(textbb[]) + padding[1] + padding[2] autoheight = Makie.height(textbb[]) + padding[3] + padding[4] @@ -82,7 +122,7 @@ function Makie.initialize_block!(l::LTeX) # trigger first update, otherwise bounds are wrong somehow - notify(l.tex) + notify(_code) # trigger bbox layoutobservables.suggestedbbox[] = layoutobservables.suggestedbbox[] diff --git a/src/recipe.jl b/src/recipe.jl index 8e70cc1..26529c7 100644 --- a/src/recipe.jl +++ b/src/recipe.jl @@ -33,6 +33,37 @@ $(Makie.ATTRIBUTES) ) end +""" + typstimg(typst; position, ...) + typstimg!(ax_or_scene, tex; position, ...) + +This recipe plots rendered `Typst` to your Figure or Scene. + +There are three types of input you can provide: +- Any `String`, which is rendered to LaTeX cognizant of the figure's overall theme, +- A [`TypstDocument`](@ref) object, which is rendered to Typst directly, and can be customized by the user, +- A [`CachedTypst`](@ref) object, which is a pre-rendered Typst document. + +`typst` may be a single one of these objects, or an array of them. + +## Attributes +$(Makie.ATTRIBUTES) +""" +@recipe(TypstImg, typst) do scene + merge( + default_theme(scene), + Attributes( + render_density = 2, + align = (:center, :center), + scale = 1.0, + position = [Point2{Float32}(0)], + rotation = [0f0], + space = :data, + markerspace = :pixel + ) + ) +end + # First, handle the case of one or more abstract strings passed in! # These are themable. @@ -87,18 +118,21 @@ function offset_from_align(align::Tuple{Symbol, Symbol}, wh)::Vec2f return Vec2f(x, y) end +__bc_if_array(::Type{TeXImg}, x) = _bc_if_array(CachedTeX, x) +__bc_if_array(::Type{TypstImg}, x) = _bc_if_array(CachedTypst, x) + _bc_if_array(f, x) = f(x) _bc_if_array(f, x::AbstractArray) = f.(x) # scatter: marker size, rotations to determine everything -function Makie.plot!(plot::TeXImg) +function Makie.plot!(plot::T) where T <: Union{TeXImg, TypstImg} # We always want to draw this at a 1:1 ratio, so increasing scale or # changing dpi should rerender - plottable_images = lift(plot[1], plot.render_density, plot.scale) do cachedtex, render_density, scale - if cachedtex isa AbstractString || cachedtex isa AbstractArray{<: AbstractString} - to_array(_bc_if_array(CachedTEX, cachedtex)) + plottable_images = lift(plot[1], plot.render_density, plot.scale) do ct, render_density, scale + if ct isa AbstractString || ct isa AbstractArray{<: AbstractString} + to_array(__bc_if_array(T, ct)) else - to_array(_bc_if_array(Cached, cachedtex)) + to_array(_bc_if_array(Cached, ct)) end end @@ -113,7 +147,7 @@ function Makie.plot!(plot::TeXImg) onany(plot, plottable_images, plot.position, plot.rotation, plot.align, plot.scale) do images, pos, rotations, align, scale if length(images) != length(pos) && !(pos isa Makie.VecTypes) # skip this update and let the next one propagate - @debug "TeXImg: Length of images ($(length(images))) != length of positions ($(length(pos))). Skipping this update." + @debug "$T: Length of images ($(length(images))) != length of positions ($(length(pos))). Skipping this update." return end diff --git a/src/rendering/pdf.jl b/src/rendering/pdf.jl index 1b91554..032dbd1 100644 --- a/src/rendering/pdf.jl +++ b/src/rendering/pdf.jl @@ -86,15 +86,15 @@ end # Rendering functions for the resulting Cairo surfaces and images """ - page2img(tex::CachedTeX, page::Int; scale = 1, render_density = 1) + page2img(ct::Union{CachedTeX, CachedTypst}, page::Int; scale = 1, render_density = 1) -Renders the `page` of the given `CachedTeX` object to an image, with the given `scale` and `render_density`. +Renders the `page` of the given `CachedTeX` or `CachedTypst` object to an image, with the given `scale` and `render_density`. This function reads the PDF using Poppler and renders it to a Cairo surface, which is then read as an image. """ -function page2img(tex::Union{CachedTeX, CachedPDF}, page::Int; scale = 1, render_density = 1) - document = update_handle!(tex) - page2img(document, page, size(tex); scale, render_density) +function page2img(ct::Union{CachedTeX, CachedTypst, CachedPDF}, page::Int; scale = 1, render_density = 1) + document = update_handle!(ct) + page2img(document, page, size(ct); scale, render_density) end function page2img(document::Ptr{Cvoid}, page::Int, tex_dims::Tuple; scale = 1, render_density = 1) @@ -136,7 +136,7 @@ function page2img(document::Ptr{Cvoid}, page::Int, tex_dims::Tuple; scale = 1, r end -firstpage2img(tex; kwargs...) = page2img(tex, 0; kwargs...) +firstpage2img(ct; kwargs...) = page2img(ct, 0; kwargs...) function page2recordsurf(document::Ptr{Cvoid}, page::Int; scale = 1, render_density = 1) w, h = pdf_get_page_size(document, page) @@ -167,16 +167,16 @@ function page2recordsurf(document::Ptr{Cvoid}, page::Int; scale = 1, render_dens end -firstpage2recordsurf(tex; kwargs...) = page2recordsurf(tex, 0; kwargs...) +firstpage2recordsurf(ct; kwargs...) = page2recordsurf(ct, 0; kwargs...) -function recordsurf2img(tex::CachedTeX, render_density = 1) +function recordsurf2img(ct::Union{CachedTeX, CachedTypst}, render_density = 1) # We can find the final dimensions (in pixel units) of the Rsvg image. # Then, it's possible to store the image in a native Julia array, # which simplifies the process of rendering. # Cairo does not draw "empty" pixels, so we need to fill here - w = ceil(Int, tex.dims[1] * render_density) - h = ceil(Int, tex.dims[2] * render_density) + w = ceil(Int, ct.dims[1] * render_density) + h = ceil(Int, ct.dims[2] * render_density) img = fill(Colors.ARGB32(0,0,0,0), w, h) @@ -187,7 +187,7 @@ function recordsurf2img(tex::CachedTeX, render_density = 1) c = Cairo.CairoContext(cs) # Render the parsed SVG to a Cairo context - render_surface(c, tex.surf) + render_surface(c, ct.surf) # The image is rendered transposed, so we need to flip it. return rotr90(permutedims(img)) diff --git a/src/rendering/typst.jl b/src/rendering/typst.jl new file mode 100644 index 0000000..b1d03ba --- /dev/null +++ b/src/rendering/typst.jl @@ -0,0 +1,105 @@ +#= +# Typst rendering +=# + +function rasterize(ct::CachedTypst, scale::Int64 = 1) + return page2img(ct, ct.doc.page; scale) +end + +# The main compilation method - compiles arbitrary Typst documents +""" + compile_typst(document::AbstractString) + +Compile the given document as a String and return the resulting PDF (also as a String). +""" +function compile_typst(document::AbstractString) + #= + Typst_jll v0.11+ supports compiling from `stdin`. + It does not yet support compiling to `stdout`. + + See also: + https://github.com/typst/typst/issues/410 + https://github.com/typst/typst/pull/3339 + =# + return mktempdir() do dir + cd(dir) do + + # First, create the typst file and write the document to it. + touch("temp.typ") + file = open("temp.typ", "w") + print(file, document) + close(file) + + # Now, we run the latexmk command in a pipeline so that we can redirect stdout and stderr to internal containers. + # First we establish these pipelines: + out = Pipe() + err = Pipe() + + try + # `pipeline` is not yet supported for `TypstCommand` + redirect_stdio(stdout=out, stderr=err) do + run(ignorestatus(typst`compile temp.typ`)) + end + + close(out.in) + close(err.in) + if !isfile("temp.pdf") + println("Typst did not write temp.pdf! Using the Typst_jll.jl.") + println("Files in temp directory are:\n" * join(readdir(), ',')) + printstyled("Stdout\n", bold=true, color = :blue) + println(read(out, String)) + printstyled("Stderr\n", bold=true, color = :red) + println(read(err, String)) + error() + end + finally + + # if pdf_num_pages("temp.pdf") > 1 + # @warn("The PDF has more than 1 page! Choosing the first page.") + # end + + # Generate the cropping margins + bbox = get_pdf_bbox("temp.pdf") + crop_box = ( + bbox[1] - _PDFCROP_DEFAULT_MARGINS[][1], + bbox[2] - _PDFCROP_DEFAULT_MARGINS[][2], + bbox[3] + _PDFCROP_DEFAULT_MARGINS[][3], + bbox[4] + _PDFCROP_DEFAULT_MARGINS[][4], + ) + crop_cmd = join(crop_box, " ") + + + out = Pipe() + err = Pipe() + try + redirect_stderr(err) do + redirect_stdout(out) do + Ghostscript_jll.gs() do gs_exe + run(`$gs_exe -o temp_cropped.pdf -sDEVICE=pdfwrite -c "[/CropBox [$crop_cmd]" -c "/PAGES pdfmark" -f temp.pdf`) + end + end + end + catch e + finally + close(out.in) + close(err.in) + if !isfile("temp_cropped.pdf") + println("`gs` failed to crop the PDF!") + println("Files in temp directory are:\n" * join(readdir(), ',')) + printstyled("Stdout\n", bold=true, color = :blue) + println(read(out, String)) + printstyled("Stderr\n", bold=true, color = :red) + println(read(err, String)) + error() + end + end + return isfile("temp_cropped.pdf") ? read("temp_cropped.pdf", String) : read("temp.pdf", String) + end + end + end +end + + +compile_typst(doc::TypstDocument) = compile_typst(String(doc.doc)) + +typst2pdf(args...) = compile_typst(args...) diff --git a/src/types.jl b/src/types.jl index d586fa4..b9d9747 100644 --- a/src/types.jl +++ b/src/types.jl @@ -270,6 +270,62 @@ Available keyword arguments are: """ texdoc(contents; kwargs...) = TEXDocument(contents, true; kwargs...) +struct TypstDocument <: AbstractDocument + contents::String + page::Int +end +TypstDocument(contents) = TypstDocument(contents, 0) +Cached(x::TypstDocument) = CachedPDF(x) +getdoc(doc::TypstDocument) = doc.contents +mimetype(::TypstDocument) = MIME"text/typst"() + +""" + TypstDocument(contents::AbstractString, add_defaults::Bool; preamble) + +This constructor function creates a `struct` of type `TypstDocument` which can be passed to `typstimg`. +All arguments are to be passed as strings. + +If `add_defaults` is `false`, then we will *not* automatically add document structure. +Note that in this case, keyword arguments will be disregarded and `contents` must be +a complete Typst document. + +Available keyword arguments are: +- `preamble`: arbitrary code inserted prior to the `contents`. Default: `""`. + +See also [`CachedTypst`](@ref), [`compile_typst`](@ref), etc. +""" +function TypstDocument( + contents::AbstractString, + add_defaults::Bool; + preamble::AbstractString = "", + ) + if add_defaults + return TypstDocument( + """ + $(preamble) + + $(contents) + """ + ) + else + return TypstDocument(contents) + end +end +TypstDocument(ts::LaTeXString) = TEXDocument(ts, true) + +""" + typstdoc(contents::AbstractString; kwargs...) + +A shorthand for `TypstDocument(contents, add_defaults=true; kwargs...)`. + +Available keyword arguments are: + +- `preamble`: arbitrary code inserted prior to the `contents`. Default: `""`. + +""" +typst_doc(contents; kwargs...) = TypstDocument(contents, true; kwargs...) + + #= # Cached documents @@ -443,13 +499,86 @@ end # do not rerun the pipeline on CachedTEX CachedTEX(ct::CachedTEX) = ct -function update_handle!(ct::CachedTEX) +struct CachedTypst <: AbstractCachedDocument + "The original `TypstDocument` which is compiled." + doc::TypstDocument + "The resulting compiled PDF" + pdf::Vector{UInt8} + "A pointer to the Poppler handle of the PDF. May be randomly GC'ed by Poppler." + ptr::Ref{Ptr{Cvoid}} # Poppler handle + "A surface to which Poppler has drawn the PDF. Permanent and cached." + surf::CairoSurface + "The dimensions of the PDF page, for ease of access." + dims::Tuple{Float64, Float64} +end +getdoc(doc::CachedTypst) = getdoc(doc.doc) +mimetype(::CachedTypst) = MIME"text/typst"() + +""" + CachedTypst(doc::TypstDocument) + +Compile a `TypstDocument`, compile it and return the cached Typst object. + +A `CachedTypst` struct stores the document and its compiled form, as well as some +pointers to in-program versions of it. It also stores the page dimensions. + +The constructor stores the following fields: +$(FIELDS) + +!!! note + This is a `mutable struct` because the pointer to the Poppler handle can change. + TODO: make this an immutable struct with a Ref to the handle?? OR maybe even the surface itself... + +!!! note + It is also possible to manually construct a `CachedTypst` with `nothing` in the `doc` field, + if you just want to insert a pre-rendered PDF into your figure. +""" +function CachedTypst(doc::TypstDocument) + pdf = Vector{UInt8}(typst2pdf(convert(String, doc))) + ptr = load_pdf(pdf) + surf = page2recordsurf(ptr, doc.page) + dims = (pdf_get_page_size(ptr, doc.page)) + + ct = CachedTypst( + doc, + pdf, + Ref(ptr), + surf, + dims# .+ (1, 1), + ) + + return ct +end + +function CachedTypst(str::Union{String, TypstString}; kwargs...) + CachedTypst(TypstDocument(str); kwargs...) +end + +function CachedTypst(pdf::Vector{UInt8}; kwargs...) + ptr = load_pdf(pdf) + surf = firstpage2recordsurf(ptr) + dims = pdf_get_page_size(ptr, 0) + + ct = CachedTypst( + nothing, + pdf, + Ref(ptr), + surf, + dims# .+ (1, 1), + ) + return ct +end + +# do not rerun the pipeline on CachedTypst +CachedTypst(ct::CachedTypst) = ct + +function update_handle!(ct::Union{CachedTEX, CachedTypst}) ct.ptr[] = load_pdf(ct.pdf) return ct.ptr[] end -Base.convert(::Type{CachedPDF}, ct::CachedTEX) = CachedPDF(PDFDocument(String(deepcopy(ct.pdf)), ct.doc.page), ct.ptr, ct.dims, ct.surf, Ref{Tuple{Matrix{ARGB32}, Float64}}((Matrix{ARGB32}(undef, 0, 0), 0))) -Base.convert(::Type{PDFDocument}, ct::CachedTEX) = PDFDocument(String(deepcopy(ct.pdf)), ct.doc.page) +Base.convert(::Type{CachedPDF}, ct::Union{CachedTEX, CachedTypst}) = CachedPDF(PDFDocument(String(deepcopy(ct.pdf)), ct.doc.page), ct.ptr, ct.dims, ct.surf, Ref{Tuple{Matrix{ARGB32}, Float64}}((Matrix{ARGB32}(undef, 0, 0), 0))) +Base.convert(::Type{PDFDocument}, ct::Union{CachedTEX, CachedTypst}) = PDFDocument(String(deepcopy(ct.pdf)), ct.doc.page) function Base.show(io::IO, ct::CachedTEX) if isnothing(ct.doc) @@ -461,6 +590,16 @@ function Base.show(io::IO, ct::CachedTEX) end end +function Base.show(io::IO, ct::CachedTypst) + if isnothing(ct.doc) + println(io, "CachedTypst(no document, $(ct.ptr), $(ct.dims))") + elseif length(ct.doc.contents) > 1000 + println(io, "CachedTypst(TypstDocument(...), $(ct.ptr), $(ct.dims))") + else + println(io, "CachedTypst($(ct.doc), $(ct.ptr), $(ct.dims))") + end +end + function implant_math(str) return TEXDocument( """\\(\\displaystyle $str\\)""", true; @@ -517,10 +656,9 @@ function rotatedrect(rect::Rect{2, T}, angle)::Rect{2, T} where T return Rect2(rmins..., (rmaxs .- rmins)...) end -function Makie.boundingbox(cachedtex::CachedTEX, position, rotation, scale, - align) - origin = offset_from_align(align, cachedtex.dims) - box = Rect2f(Point2f(origin), Vec2f(cachedtex.dims) * scale) +function Makie.boundingbox(ct::Union{CachedTEX, CachedTypst}, position, rotation, scale, align) + origin = offset_from_align(align, ct.dims) + box = Rect2f(Point2f(origin), Vec2f(ct.dims) * scale) rect = rotatedrect(box, rotation) new_origin = Point3f(rect.origin..., 0) new_widths = Vec3f(rect.widths..., 0) @@ -528,18 +666,15 @@ function Makie.boundingbox(cachedtex::CachedTEX, position, rotation, scale, end # this method copied from Makie.jl -function Makie.boundingbox(cachedtexs::AbstractVector{CachedTEX}, positions, rotations, scale, - align) - - isempty(cachedtexs) && (return Rect3f((0, 0, 0), (0, 0, 0))) +function Makie.boundingbox(cts::AbstractVector{<:Union{CachedTEX, CachedTypst}}, positions, rotations, scale, align) + isempty(cts) && (return Rect3f((0, 0, 0), (0, 0, 0))) bb = Rect3f() - broadcast_foreach(cachedtexs, positions, rotations, scale, - align) do cachedtex, pos, rot, scl, aln + broadcast_foreach(cts, positions, rotations, scale, align) do ct, pos, rot, scl, aln if !Makie.isfinite_rect(bb) - bb = Makie.boundingbox(cachedtex, pos, rot, scl, aln) + bb = Makie.boundingbox(ct, pos, rot, scl, aln) else - bb = Makie.union(bb, Makie.boundingbox(cachedtex, pos, rot, scl, aln)) + bb = Makie.union(bb, Makie.boundingbox(ct, pos, rot, scl, aln)) end end !Makie.isfinite_rect(bb) && error("Invalid `TeX` boundingbox")