From b6fc488b380a8a65e636d983974e7b69acf215d6 Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sat, 8 Jun 2024 12:01:45 -0700 Subject: [PATCH 1/7] Implement Typst backend --- 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") From 0e1838175d4ad4ef6e16214d2767779ed647240b Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sat, 8 Jun 2024 13:08:03 -0700 Subject: [PATCH 2/7] Fix method ambiguity --- src/layoutable.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/layoutable.jl b/src/layoutable.jl index e4214cb..0b79aba 100644 --- a/src/layoutable.jl +++ b/src/layoutable.jl @@ -73,9 +73,11 @@ code(lt::LTypst) = lt.typst img!(::Type{LTeX}, args...; kwargs...) = teximg!(args...; kwargs...) img!(::Type{LTypst}, args...; kwargs...) = typstimg!(args...; kwargs...) -_to_cached(::Type{LTeX}, x) = CachedTEX(x) -_to_cached(::Type{LTypst}, x) = CachedTypst(x) -_to_cached(::Type, x::AbstractDocument) = Cached(x) +__to_cached(T, x) = T(x) +__to_cached(T, x::AbstractDocument) = Cached(x) + +_to_cached(::Type{LTeX}, x) = __to_cached(CachedTEX, x) +_to_cached(::Type{LTypst}, x) = __to_cached(CachedTypst, x) function Makie.initialize_block!(l::T) where T <: Union{LTeX, LTypst} From 57245bbff61659995a8bb4ff1252915e97a04354 Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sat, 8 Jun 2024 13:11:50 -0700 Subject: [PATCH 3/7] Fix grammar and minor bugs --- ext/MakieTeXCairoMakieExt.jl | 4 ++-- src/recipe.jl | 2 +- src/rendering/typst.jl | 2 +- src/types.jl | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/MakieTeXCairoMakieExt.jl b/ext/MakieTeXCairoMakieExt.jl index 9995610..f64405b 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` and `TypstImg` to remain a unit, +# 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::Union{TeXImg, TypstImg}) = true +CairoMakie.is_cairomakie_atomic_plot(plot::TeXImg) = true # # Scatter markers diff --git a/src/recipe.jl b/src/recipe.jl index 26529c7..7d1b075 100644 --- a/src/recipe.jl +++ b/src/recipe.jl @@ -40,7 +40,7 @@ end 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, +- Any `String`, which is rendered to Typst 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. diff --git a/src/rendering/typst.jl b/src/rendering/typst.jl index b1d03ba..6d54a25 100644 --- a/src/rendering/typst.jl +++ b/src/rendering/typst.jl @@ -44,7 +44,7 @@ function compile_typst(document::AbstractString) close(out.in) close(err.in) if !isfile("temp.pdf") - println("Typst did not write temp.pdf! Using the Typst_jll.jl.") + println("Typst did not write temp.pdf! Using Typst_jll.jl.") println("Files in temp directory are:\n" * join(readdir(), ',')) printstyled("Stdout\n", bold=true, color = :blue) println(read(out, String)) diff --git a/src/types.jl b/src/types.jl index b9d9747..154924a 100644 --- a/src/types.jl +++ b/src/types.jl @@ -311,7 +311,7 @@ function TypstDocument( return TypstDocument(contents) end end -TypstDocument(ts::LaTeXString) = TEXDocument(ts, true) +TypstDocument(ts::TypstString) = TypstDocument(ts, true) """ typstdoc(contents::AbstractString; kwargs...) From 5aea23c1032d52cec4e45a79ac3328e840f1d484 Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sat, 8 Jun 2024 13:43:51 -0700 Subject: [PATCH 4/7] Remove `TypstImg` and `LTypst` --- src/MakieTeX.jl | 3 +-- src/layoutable.jl | 58 +++++++---------------------------------------- src/recipe.jl | 47 +++++--------------------------------- 3 files changed, 15 insertions(+), 93 deletions(-) diff --git a/src/MakieTeX.jl b/src/MakieTeX.jl index ad9a177..bf8ed0f 100644 --- a/src/MakieTeX.jl +++ b/src/MakieTeX.jl @@ -47,8 +47,7 @@ export PDFDocument, CachedPDF export SVGDocument, CachedSVG export dvi2svg, latex2dvi, rsvg2recordsurf, svg2rsvg export teximg, teximg!, TeXImg -export typstimg, typstimg!, TypstImg -export LTeX, LTypst +export LTeX export LaTeXStrings, LaTeXString, latexstring, @L_str export Typstry, TypstString, @typst_str diff --git a/src/layoutable.jl b/src/layoutable.jl index 0b79aba..3cbb4b7 100644 --- a/src/layoutable.jl +++ b/src/layoutable.jl @@ -33,65 +33,23 @@ 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_cached(T, x) = T(x) -__to_cached(T, x::AbstractDocument) = Cached(x) -_to_cached(::Type{LTeX}, x) = __to_cached(CachedTEX, x) -_to_cached(::Type{LTypst}, x) = __to_cached(CachedTypst, x) +_to_cachedtex(x) = CachedTEX(x) +_to_cachedtex(x::AbstractDocument) = Cached(x) -function Makie.initialize_block!(l::T) where T <: Union{LTeX, LTypst} +function Makie.initialize_block!(l::LTeX) - _code = code(l) topscene = l.blockscene layoutobservables = l.layoutobservables textpos = Observable([Point3f(0, 0, 0)]) scale = Observable([Vec2f(1.0, 1.0)]) - cached = lift(collect ∘ tuple ∘ (x -> _to_cached(T, x)), _code) + cached_tex = lift(collect ∘ tuple ∘ _to_cachedtex, l.tex) - t = img!(T, - topscene, cached; position = textpos, visible = l.visible, + t = teximg!( + topscene, cached_tex; position = textpos, visible = l.visible, scale = scale, align = (:bottom, :left), rotations = l.rotation, markerspace = :pixel, @@ -100,7 +58,7 @@ function Makie.initialize_block!(l::T) where T <: Union{LTeX, LTypst} textbb = Ref(BBox(0, 1, 0, 1)) - onany(_code, l.scale, l.rotation, l.padding) do code, scale, rotation, padding + onany(l.tex, l.scale, l.rotation, l.padding) do tex, 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] @@ -124,7 +82,7 @@ function Makie.initialize_block!(l::T) where T <: Union{LTeX, LTypst} # trigger first update, otherwise bounds are wrong somehow - notify(_code) + notify(l.tex) # trigger bbox layoutobservables.suggestedbbox[] = layoutobservables.suggestedbbox[] diff --git a/src/recipe.jl b/src/recipe.jl index 7d1b075..7752a1d 100644 --- a/src/recipe.jl +++ b/src/recipe.jl @@ -33,37 +33,6 @@ $(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 Typst 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. @@ -118,21 +87,18 @@ 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::T) where T <: Union{TeXImg, TypstImg} +function Makie.plot!(plot::TeXImg) # 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 ct, render_density, scale - if ct isa AbstractString || ct isa AbstractArray{<: AbstractString} - to_array(__bc_if_array(T, ct)) + 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)) else - to_array(_bc_if_array(Cached, ct)) + to_array(_bc_if_array(Cached, cachedtex)) end end @@ -147,7 +113,7 @@ function Makie.plot!(plot::T) where T <: Union{TeXImg, TypstImg} 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 "$T: Length of images ($(length(images))) != length of positions ($(length(pos))). Skipping this update." + @debug "TeXImg: Length of images ($(length(images))) != length of positions ($(length(pos))). Skipping this update." return end @@ -177,4 +143,3 @@ function Makie.plot!(plot::T) where T <: Union{TeXImg, TypstImg} markerspace = plot.markerspace, ) end - From 82286b3a11f6551a4022d07e965ce2777b8dee93 Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sun, 9 Jun 2024 00:18:43 -0700 Subject: [PATCH 5/7] Integrate Typst documentation --- docs/src/api.md | 2 ++ docs/src/formats.md | 15 +++++++++++++++ docs/src/index.md | 6 ++---- src/MakieTeX.jl | 1 + src/types.jl | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 5cf90aa..182b8e5 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -37,10 +37,12 @@ SVGDocument PDFDocument EPSDocument TEXDocument +TypstDocument ``` ### Cached document types ```@docs CachedTEX +CachedTypst CachedPDF CachedSVG CachedEPS diff --git a/docs/src/formats.md b/docs/src/formats.md index 837d1da..e734989 100644 --- a/docs/src/formats.md +++ b/docs/src/formats.md @@ -58,6 +58,21 @@ scatter(fig[2, 1], rand(10), rand(10), marker=tex_document, markersize = 50) fig ``` +## Typst + +```@example main +using MakieTeX, CairoMakie + +typst_string = typst"$ integral_0^pi sin(x)^2 diff x $"; +typst_document = TypstDocument(typst_string); +cached_typst = CachedTypst(typst_document); +cached_pdf = convert(CachedPDF, cached_typst); + +fig = Figure(size=(100, 100)); +LTeX(fig[1, 1], cached_pdf); +fig +``` + ## PDF ```@example main diff --git a/docs/src/index.md b/docs/src/index.md index ba53963..53586af 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -55,9 +55,7 @@ teximg(raw""" Rendering can occur either to a bitmap (for GL backends) or to a Cairo surface (for CairoMakie). Both of these have APIs ([`rasterize`](@ref) and [`draw_to_cairo_surface`](@ref)). -Each rendering format has its own complexities, so the rendering pipelines are usually separate. SVG uses librsvg, PDF and EPS use Poppler directly, and TeX uses the available local TeX render (if not, `tectonic` is bundled with MakieTeX) to render to a PDF, which then follows the Poppler pipeline. - -A hypothetical future Typst backend would likely also be a Typst -> PDF -> Poppler pipeline. [Typst_jll](https://github.com/JuliaBinaryWrappers/Typst_jll.jl) already exists, so it would be fairly easy to bundle. +Each rendering format has its own complexities, so the rendering pipelines are usually separate. SVG uses librsvg while PDF and EPS use Poppler directly. TeX uses the available local TeX renderer (if not, `tectonic` is bundled with MakieTeX) and Typst uses Typst_jll.jl to render to a PDF, which then each follow the Poppler pipeline. ### Makie @@ -67,4 +65,4 @@ When rendering to Makie, MakieTeX rasterizes the document to a bitmap by default ```@raw html -``` \ No newline at end of file +``` diff --git a/src/MakieTeX.jl b/src/MakieTeX.jl index bf8ed0f..a738443 100644 --- a/src/MakieTeX.jl +++ b/src/MakieTeX.jl @@ -43,6 +43,7 @@ include("rendering/svg.jl") export Cached export TeXDocument, CachedTeX export TEXDocument, CachedTEX +export TypstDocument, CachedTypst export PDFDocument, CachedPDF export SVGDocument, CachedSVG export dvi2svg, latex2dvi, rsvg2recordsurf, svg2rsvg diff --git a/src/types.jl b/src/types.jl index 154924a..98f79c9 100644 --- a/src/types.jl +++ b/src/types.jl @@ -282,7 +282,7 @@ 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`. +This constructor function creates a `struct` of type `TypstDocument`. All arguments are to be passed as strings. If `add_defaults` is `false`, then we will *not* automatically add document structure. From 78eac7cbbcee9b2de50acb922db2cbad4b59bf05 Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sun, 9 Jun 2024 00:58:16 -0700 Subject: [PATCH 6/7] Refactor duplicated code --- src/rendering/pdf_utils.jl | 41 +++++++++++++++++++ src/rendering/tex.jl | 47 ++-------------------- src/rendering/typst.jl | 47 ++-------------------- src/types.jl | 80 ++++++++++++-------------------------- 4 files changed, 73 insertions(+), 142 deletions(-) diff --git a/src/rendering/pdf_utils.jl b/src/rendering/pdf_utils.jl index 5e88280..93737e1 100644 --- a/src/rendering/pdf_utils.jl +++ b/src/rendering/pdf_utils.jl @@ -144,4 +144,45 @@ Crop a PDF file using Ghostscript. This alters the crop box but does not actually remove elements. """ function crop_pdf(path::String; margin = _PDFCROP_DEFAULT_MARGINS[]) + # 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(path) + crop_box = ( + bbox[1] - margin[1], + bbox[2] - margin[2], + bbox[3] + margin[3], + bbox[4] + margin[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 $path`) + 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(path, String) end diff --git a/src/rendering/tex.jl b/src/rendering/tex.jl index 8ad3aac..580800b 100644 --- a/src/rendering/tex.jl +++ b/src/rendering/tex.jl @@ -29,6 +29,7 @@ function compile_latex( # First, create the tex file and write the document to it. touch("temp.tex") + path = "temp.pdf" file = open("temp.tex", "w") print(file, document) close(file) @@ -48,8 +49,8 @@ function compile_latex( suc = success(latex) close(out.in) close(err.in) - if !isfile("temp.pdf") - println("Latex did not write temp.pdf! Using the $(tex_engine) engine.") + if !isfile(path) + println("Latex did not write $(path)! Using the $(tex_engine) engine.") println("Files in temp directory are:\n" * join(readdir(), ',')) printstyled("Stdout\n", bold=true, color = :blue) println(read(out, String)) @@ -58,47 +59,7 @@ function compile_latex( 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) + return crop_pdf(path) end end end diff --git a/src/rendering/typst.jl b/src/rendering/typst.jl index 6d54a25..2df9acf 100644 --- a/src/rendering/typst.jl +++ b/src/rendering/typst.jl @@ -27,6 +27,7 @@ function compile_typst(document::AbstractString) # First, create the typst file and write the document to it. touch("temp.typ") file = open("temp.typ", "w") + path = "temp.pdf" print(file, document) close(file) @@ -43,8 +44,8 @@ function compile_typst(document::AbstractString) close(out.in) close(err.in) - if !isfile("temp.pdf") - println("Typst did not write temp.pdf! Using Typst_jll.jl.") + if !isfile(path) + println("Typst did not write $(path)! Using Typst_jll.jl.") println("Files in temp directory are:\n" * join(readdir(), ',')) printstyled("Stdout\n", bold=true, color = :blue) println(read(out, String)) @@ -53,47 +54,7 @@ function compile_typst(document::AbstractString) 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) + return crop_pdf(path) end end end diff --git a/src/types.jl b/src/types.jl index 98f79c9..59060c5 100644 --- a/src/types.jl +++ b/src/types.jl @@ -450,23 +450,7 @@ $(FIELDS) It is also possible to manually construct a `CachedTEX` with `nothing` in the `doc` field, if you just want to insert a pre-rendered PDF into your figure. """ -function CachedTEX(doc::TEXDocument; kwargs...) - - pdf = Vector{UInt8}(latex2pdf(convert(String, doc); kwargs...)) - ptr = load_pdf(pdf) - surf = page2recordsurf(ptr, doc.page) - dims = (pdf_get_page_size(ptr, doc.page)) - - ct = CachedTEX( - doc, - pdf, - Ref(ptr), - surf, - dims# .+ (1, 1), - ) - - return ct -end +CachedTEX(doc::TEXDocument; kwargs...) = cached_doc(CachedTEX, latex2pdf, doc; kwargs...) function CachedTEX(str::String; kwargs...) return CachedTEX(implant_text(str); kwargs...) @@ -481,20 +465,7 @@ function CachedTEX(x::LaTeXString; kwargs...) end end -function CachedTEX(pdf::Vector{UInt8}; kwargs...) - ptr = load_pdf(pdf) - surf = firstpage2recordsurf(ptr) - dims = pdf_get_page_size(ptr, 0) - - ct = CachedTEX( - nothing, - pdf, - Ref(ptr), - surf, - dims# .+ (1, 1), - ) - return ct -end +CachedTEX(pdf::Vector{UInt8}; kwargs...) = cached_pdf(CachedTEX, pdf; kwargs...) # do not rerun the pipeline on CachedTEX CachedTEX(ct::CachedTEX) = ct @@ -533,13 +504,24 @@ $(FIELDS) 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))) +CachedTypst(doc::TypstDocument) = cached_doc(CachedTypst, typst2pdf, doc) + +function CachedTypst(str::Union{String, TypstString}; kwargs...) + CachedTypst(TypstDocument(str); kwargs...) +end + +CachedTypst(pdf::Vector{UInt8}; kwargs...) = cached_pdf(CachedTypst, pdf; kwargs...) + +# do not rerun the pipeline on CachedTypst +CachedTypst(ct::CachedTypst) = ct + +function cached_doc(T, f, doc; kwargs...) + pdf = Vector{UInt8}(f(convert(String, doc); kwargs...)) ptr = load_pdf(pdf) surf = page2recordsurf(ptr, doc.page) dims = (pdf_get_page_size(ptr, doc.page)) - ct = CachedTypst( + ct = T( doc, pdf, Ref(ptr), @@ -550,16 +532,12 @@ function CachedTypst(doc::TypstDocument) return ct end -function CachedTypst(str::Union{String, TypstString}; kwargs...) - CachedTypst(TypstDocument(str); kwargs...) -end - -function CachedTypst(pdf::Vector{UInt8}; kwargs...) +function cached_pdf(T, pdf; kwargs...) ptr = load_pdf(pdf) surf = firstpage2recordsurf(ptr) dims = pdf_get_page_size(ptr, 0) - ct = CachedTypst( + ct = T( nothing, pdf, Ref(ptr), @@ -569,9 +547,6 @@ function CachedTypst(pdf::Vector{UInt8}; kwargs...) 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[] @@ -580,25 +555,18 @@ end 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) +function _show(io, ct, x, y) if isnothing(ct.doc) - println(io, "CachedTEX(no document, $(ct.ptr), $(ct.dims))") + println(io, x, "(no document, $(ct.ptr), $(ct.dims))") elseif length(ct.doc.contents) > 1000 - println(io, "CachedTEX(TexDocument(...), $(ct.ptr), $(ct.dims))") + println(io, x, "(", y, "(...), $(ct.ptr), $(ct.dims))") else - println(io, "CachedTEX($(ct.doc), $(ct.ptr), $(ct.dims))") + println(io, x, "($(ct.doc), $(ct.ptr), $(ct.dims))") 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 +Base.show(io::IO, ct::CachedTEX) = _show(io, ct, "CachedTEX", "TEXDocument") +Base.show(io::IO, ct::CachedTypst) = _show(io, ct, "CachedTypst", "TypstDocument") function implant_math(str) return TEXDocument( From e255725756a3c1fa8ea0038a6a3ca86a4d91246b Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sun, 9 Jun 2024 04:03:01 -0700 Subject: [PATCH 7/7] Implement some Typst tests --- test/runtests.jl | 21 +++++++++++++++++++++ test/tex.jl | 29 +++++------------------------ test/typst.jl | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 test/typst.jl diff --git a/test/runtests.jl b/test/runtests.jl index f7cbc93..87e5bdd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -16,6 +16,27 @@ function save_test(filename, fig; kwargs...) end +function render_texample(cached, document, url) + + fig = Figure() + + lt = LTeX(fig[1, 1], convert(CachedPDF, cached(document(read(Downloads.download(url), String), false)))) + + @test true + + resize_to_layout!(fig) + + filename = splitdir(splitext(url)[1])[2] + + save_test(joinpath(@__DIR__, "test_images", "texample", filename), fig) + + + @test true + +end + + include("tex.jl") +include("typst.jl") include("svg.jl") include("pdf.jl") diff --git a/test/tex.jl b/test/tex.jl index 87756c6..ce9f9af 100644 --- a/test/tex.jl +++ b/test/tex.jl @@ -1,29 +1,9 @@ using CairoMakie, MakieTeX using Test, Downloads -function render_texample(url) - - fig = Figure() - - lt = LTeX(fig[1, 1], CachedTeX(TeXDocument(read(Downloads.download(url), String), false))) - - @test true - - resize_to_layout!(fig) - - filename = splitdir(splitext(url)[1])[2] - - save_test(joinpath(@__DIR__, "test_images", "texample", filename), fig) - - - @test true - -end - - @testset "TeX rendering" begin - can_access_texample = try + can_access_example = try Downloads.download("https://texample.net/media/tikz/examples/TEX/rotated-triangle.tex") true catch e @@ -31,7 +11,7 @@ end @warn "Cannot access texample.net; skipping tests that require it." end - can_access_texample && @testset "texample.net" begin + can_access_example && @testset "texample.net" begin mkpath(joinpath(example_path, "texample")) @@ -49,7 +29,8 @@ end for name in names @testset "$name" begin - render_texample("https://texample.net/media/tikz/examples/TEX/$name.tex") + render_texample(CachedTeX, TeXDocument, + "https://texample.net/media/tikz/examples/TEX/$name.tex") end end @@ -251,4 +232,4 @@ end end =# -end \ No newline at end of file +end diff --git a/test/typst.jl b/test/typst.jl new file mode 100644 index 0000000..cb39caa --- /dev/null +++ b/test/typst.jl @@ -0,0 +1,37 @@ +using CairoMakie, MakieTeX +using Test, Downloads + +url(name) = "https://raw.githubusercontent.com/typst/packages/main/packages/preview/cetz/0.2.2/gallery/$name.typ" + +@testset "Typst rendering" begin + + names = [ + "karls-picture", + "tree", + "waves", + "pie-chart", + "plot", + "barchart" + ] + + can_access_example = try + _url = url(first(names)) + Downloads.download(_url) + true + catch e + false + @warn "Cannot access $_url; skipping tests that require it." + end + + can_access_example && @testset "cetz" begin + + for name in names + + @testset "$name" begin + render_texample(CachedTypst, TypstDocument, url(name)) + end + + end + + end +end