diff --git a/Project.toml b/Project.toml index 4297266..bb2cb89 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "MakieTeX" uuid = "6d554a22-29e7-47bd-aee5-0c5f06619414" authors = ["Anshul Singhvi"] -version = "0.3.1" +version = "0.3.2" [deps] Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" @@ -18,11 +18,11 @@ tectonic_jll = "d7dd28d6-a5e6-559c-9131-7eb760cdacc5" [compat] Cairo = "1.0.5" -CairoMakie = "0.10" +CairoMakie = "0.11" Colors = "0.9, 0.10, 0.11, 0.12" DocStringExtensions = "0.8, 0.9" LaTeXStrings = "1" -Makie = "0.18, 0.19" +Makie = "0.20" Poppler_jll = "21.9" julia = "1" diff --git a/src/MakieTeX.jl b/src/MakieTeX.jl index 0da3ab4..31f3a14 100644 --- a/src/MakieTeX.jl +++ b/src/MakieTeX.jl @@ -26,7 +26,7 @@ const TEXT_RENDER_DENSITY = Ref(5) include("types.jl") include("rendering.jl") include("recipe.jl") -include("text_override.jl") +include("text_utils.jl") include("layoutable.jl") export TeXDocument, CachedTeX @@ -51,6 +51,8 @@ end "Checks whether the default latex engine is correct" function __init__() + + # First, determine latex engine support latexmk = Sys.which("latexmk") if isnothing(latexmk) @warn """ @@ -61,25 +63,39 @@ function __init__() Defaulting to the bundled `tectonic` renderer for now. """ CURRENT_TEX_ENGINE[] = `tectonic` - return + else + t1 = try_tex_engine(CURRENT_TEX_ENGINE[]) # by default `lualatex` + + if !isnothing(t1) + + @warn(""" + The specified TeX engine $(CURRENT_TEX_ENGINE[]) is not available. + Trying pdflatex: + """ + ) + + CURRENT_TEX_ENGINE[] = `pdflatex` + else + return + end + + t2 = try_tex_engine(CURRENT_TEX_ENGINE[]) + if !isnothing(t2) + + @warn "Could not find a TeX engine; defaulting to bundled `tectonic`" + CURRENT_TEX_ENGINE[] = `tectonic` + else + return + end + end - t1 = try_tex_engine(CURRENT_TEX_ENGINE[]) - isnothing(t1) && return - - @warn(""" - The specified TeX engine $(CURRENT_TEX_ENGINE[]) is not available. - Trying pdflatex: - """ - ) - - CURRENT_TEX_ENGINE[] = `pdflatex` - - t2 = try_tex_engine(CURRENT_TEX_ENGINE[]) - isnothing(t1) && return + + + # TODO: Once the correct tex engine is found, load the rendering extensions + # (currently CairoMakie, but we may do WGLMakie in the future since it can display SVG) + - @warn "Could not find a TeX engine; defaulting to bundled `tectonic`" - CURRENT_TEX_ENGINE[] = `tectonic` return end diff --git a/src/layoutable.jl b/src/layoutable.jl index 58efa05..e54179a 100644 --- a/src/layoutable.jl +++ b/src/layoutable.jl @@ -1,8 +1,8 @@ -import Makie.MakieLayout: inherit +import Makie: inherit # This code has basically been adapted from the Label code in the main repo. -Makie.MakieLayout.@Block LTeX begin +Makie.@Block LTeX begin @attributes begin "The LaTeX code to be compiled and drawn. Can be a String, a TeXDocument or a CachedTeX." tex = "\\LaTeX" @@ -35,7 +35,7 @@ end LTeX(x, tex; kwargs...) = LTeX(x; tex = tex, kwargs...) -function Makie.MakieLayout.initialize_block!(l::LTeX) +function Makie.initialize_block!(l::LTeX) topscene = l.blockscene layoutobservables = l.layoutobservables @@ -55,16 +55,16 @@ function Makie.MakieLayout.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 - textbb[] = Makie.rotatedrect(Makie.MakieLayout.Rect2f(0,0,(t[1][][1].dims .* scale)...), rotation) - autowidth = Makie.MakieLayout.width(textbb[]) + padding[1] + padding[2] - autoheight = Makie.MakieLayout.height(textbb[]) + padding[3] + padding[4] + textbb[] = Makie.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] layoutobservables.autosize[] = (autowidth, autoheight) end onany(layoutobservables.computedbbox, l.padding) do bbox, padding - tw = Makie.MakieLayout.width(textbb[]) - th = Makie.MakieLayout.height(textbb[]) + tw = Makie.width(textbb[]) + th = Makie.height(textbb[]) box = bbox.origin[1] boy = bbox.origin[2] @@ -72,7 +72,7 @@ function Makie.MakieLayout.initialize_block!(l::LTeX) tx = box + padding[1] + 0.5 * tw ty = boy + padding[3] + 0.5 * th - textpos[] = Makie.MakieLayout.Point3f[(tx, ty, 0)] + textpos[] = Makie.Point3f[(tx, ty, 0)] end diff --git a/src/recipe.jl b/src/recipe.jl index 0bc26ca..1f97fcb 100644 --- a/src/recipe.jl +++ b/src/recipe.jl @@ -2,6 +2,22 @@ # scale::Real # render_density::Real # rotations::Vector{Real} +""" + teximg(tex; position, ...) + teximg!(ax_or_scene, tex; position, ...) + +This recipe plots rendered `TeX` 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 [`TeXDocument`](@ref) object, which is rendered to LaTeX directly, and can be customized by the user, +- A [`CachedTeX`](@ref) object, which is a pre-rendered LaTeX document. + +`tex` may be a single one of these objects, or an array of them. + +## Attributes +$(Makie.ATTRIBUTES) +""" @recipe(TeXImg, tex) do scene merge( default_theme(scene), @@ -17,6 +33,34 @@ ) end +# First, handle the case of one or more abstract strings passed in! +# These are themable. + +# Makie.used_attributes(::Type{<: TeXImg}, string_s::Union{<: AbstractString, AbstractVector{<: AbstractString}}) = (:font, :fontsize, :justification, :color, :word_wrap_width, :lineheight) +# Makie.convert_arguments(::Type{<: TeXImg}, string::AbstractString) = Makie.convert_arguments(TeXImg, [string]) + +# function Makie.convert_arguments( +# ::Type{<: TeXImg}, +# strings::AbstractVector{<: AbstractString}; +# font = Makie.texfont(), +# fontsize = 14, +# justification = Makie.automatic, +# color = :black, +# word_wrap_width = -1, +# lineheight = 1.0, +# ) + +# # This function will convert the strings to CachedTeX, so that it can track changes in attributes. +# # It will have to handle the case where the parameters given are for all strings in an array, or per string, +# # using Makie's `broadcast_foreach` function. + +# # First, we need to convert the strings to CachedTeX. +# # This is done by using the `CachedTeX` constructor, which will render the LaTeX and store it in a CachedTeX object. +# # This is then stored in an array, which is then returned. + + +# end + function Makie.boundingbox(x::T) where T <: TeXImg Makie.boundingbox( x[1][] isa CachedTeX ? [x[1][]] : x[1][], @@ -63,7 +107,7 @@ function Makie.plot!(plot::T) where T <: 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 cachedtex, render_density, scale - to_array(firstpage2img((cachedtex); render_density = render_density * scale)) + to_array(firstpage2img.((cachedtex); render_density = render_density * scale)) end scatter_images = Observable(plottable_images[]) @@ -77,7 +121,7 @@ function Makie.plot!(plot::T) where T <: TeXImg onany(plottable_images, plot.position, plot.rotations, plot.align, plot.scale) do images, pos, rotations, align, scale if length(images) != length(pos) # skip this update and let the next one propagate - @debug "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 @@ -94,7 +138,7 @@ function Makie.plot!(plot::T) where T <: TeXImg notify(scatter_rotations) end - plot.position[] = plot.position[] + notify(plot.position) # trigger the first update scatter!( plot, @@ -112,7 +156,6 @@ end # CairoMakie direct drawing method function draw_tex(scene::Scene, screen::CairoMakie.Screen, cachedtex::CachedTeX, position::VecTypes, scale::VecTypes, rotation::Real, align::Tuple{Symbol, Symbol}) # establish some initial values - x0, y0 = 0.0, 0.0 w, h = cachedtex.dims ctx = screen.context # First we center the position with respect to the center of the image, @@ -124,21 +167,22 @@ function draw_tex(scene::Scene, screen::CairoMakie.Screen, cachedtex::CachedTeX, # Then, we find the appropriate "marker offset" w.r.t. alignment. # This is separate because of Cairo's reversed y-axis. halign, valign = align - pos = Point2f(0) - pos = if halign == :left - pos .- (-scale[1] / 2, 0) + offset_pos = Point2f(0) + # First, we handle the horizontal alignment + offset_pos = if halign == :left + offset_pos .- (-scale[1] / 2, 0) elseif halign == :center - pos .- (0, 0) + offset_pos .- (0, 0) elseif halign == :right - pos .- (scale[1] / 2, 0) + offset_pos .- (scale[1] / 2, 0) end - - pos = if valign == :top - pos .+ (0, scale[2]/2) + # and then the vertical alignment. + offset_pos = if valign == :top + offset_pos .+ (0, scale[2]/2) elseif valign == :center - pos .+ (0, 0) + offset_pos .+ (0, 0) elseif valign == :bottom - pos .- (0, scale[2]/2) + offset_pos .- (0, scale[2]/2) end # Calculate, with respect to the rotation, where the rotated center of the image @@ -161,7 +205,7 @@ function draw_tex(scene::Scene, screen::CairoMakie.Screen, cachedtex::CachedTeX, #compensate for that with previously calculated values Cairo.translate(ctx, cx, cy) # apply "marker offset" to implement/simulate alignment - Cairo.translate(ctx, pos[1], pos[2]) + Cairo.translate(ctx, offset_pos[1], offset_pos[2]) # scale the marker appropriately Cairo.scale( ctx, @@ -198,6 +242,10 @@ function draw_tex(scene::Scene, screen::CairoMakie.Screen, cachedtex::CachedTeX, Cairo.restore(ctx) end +# 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 + function CairoMakie.draw_plot(scene::Scene, screen::CairoMakie.Screen, img::T) where T <: MakieTeX.TeXImg broadcast_foreach(img[1][], img.position[], img.scale[], CairoMakie.remove_billboard(img.rotations[]), img.align[]) do cachedtex, position, scale, rotation, align diff --git a/src/rendering.jl b/src/rendering.jl index e3a6903..1a58a1c 100644 --- a/src/rendering.jl +++ b/src/rendering.jl @@ -17,7 +17,7 @@ end function compile_latex( document::AbstractString; tex_engine = CURRENT_TEX_ENGINE[], - options = `-file-line-error -halt-on-error` + options = `-file-line-error` ) use_tex_engine=tex_engine @@ -46,7 +46,7 @@ function compile_latex( run(pipeline(ignorestatus(`$bin temp.tex`), stdout=out, stderr=err)) end else # latexmk - latex_cmd = `latexmk $options --shell-escape -$use_tex_engine -cd -interaction=nonstopmode temp.tex` + latex_cmd = `latexmk $options --shell-escape -cd -$use_tex_engine -interaction=nonstopmode temp.tex` run(pipeline(ignorestatus(latex_cmd), stdout=out, stderr=err)) end suc = success(latex) @@ -80,7 +80,7 @@ function compile_latex( redirect_stdout(devnull) do Ghostscript_jll.gs() do gs_exe mtperl() do perl_exe - run(`$perl_exe $pdfcrop --margin $crop_margins $() --gscmd $gs_exe temp.pdf temp_cropped.pdf`) + run(`$perl_exe $pdfcrop --margin $crop_margins --gscmd $gs_exe temp.pdf temp_cropped.pdf`) return read("temp_cropped.pdf", String) end end @@ -104,6 +104,17 @@ latex2pdf(args...; kwargs...) = compile_latex(args...; kwargs...) # Pure poppler pipeline - directly from PDF to Cairo surface. +""" + load_pdf(pdf::String)::Ptr{Cvoid} + load_pdf(pdf::Vector{UInt8})::Ptr{Cvoid} + +Loads a PDF file into a Poppler document handle. + +Input may be either a String or a `Vector{UInt8}`, each representing the PDF file in memory. + +!!! warn + The String input does **NOT** represent a filename! +""" load_pdf(pdf::String) = load_pdf(Vector{UInt8}(pdf)) function load_pdf(pdf::Vector{UInt8})::Ptr{Cvoid} # Poppler document handle @@ -144,6 +155,13 @@ end # Rendering functions for the resulting Cairo surfaces and images +""" + page2img(tex::CachedTeX, 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`. + +This function reads the PDF using Poppler and renders it to a Cairo surface, which is then read as an image. +""" function page2img(tex::CachedTeX, page::Int; scale = 1, render_density = 1) document = tex.ptr page = ccall( @@ -168,7 +186,7 @@ function page2img(tex::CachedTeX, page::Int; scale = 1, render_density = 1) Cairo.set_antialias(ctx, Cairo.ANTIALIAS_BEST) Cairo.save(ctx) - # Render the page to the surface + # Render the page to the surface using Poppler ccall( (:poppler_page_render, Poppler_jll.libpoppler_glib), Cvoid, @@ -255,7 +273,11 @@ end # Utility functions +""" + pdf_num_pages(filename::String)::Int +Returns the number of pages in a PDF file located at `filename`, using the Poppler executable. +""" function pdf_num_pages(filename::String) metadata = Poppler_jll.pdfinfo() do exe read(`$exe $filename`, String) @@ -270,6 +292,11 @@ function pdf_num_pages(filename::String) return parse(Int, split(pageinfo, ' ')[end]) end +""" + pdf_num_pages(document::Ptr{Cvoid})::Int + +`document` must be a Poppler document handle. Returns the number of pages in the document. +""" function pdf_num_pages(document::Ptr{Cvoid}) ccall( (:poppler_document_get_n_pages, Poppler_jll.libpoppler_glib), @@ -305,7 +332,18 @@ function pdf_get_page_size(document::Ptr{Cvoid}, page_number::Int) return (width[], height[]) end -"Split an in memory PDF and return a vector of its pages" +""" + split_pdf(pdf::Union{Vector{UInt8}, String})::Vector{UInt8} + +Splits a PDF into its constituent pages, returning a Vector of UInt8 arrays, each representing a page. + +The input must be a PDF file, either as a String or as a Vector{UInt8} of the PDF's bytes. + +!!! warn + The input String does **NOT** represent a filename! + +This uses Ghostscript to actually split the PDF and return PDF files. If you just want to render the PDF, use [`load_pdf`](@ref) and [`page2img`](@ref) instead. +""" function split_pdf(pdf::Union{Vector{UInt8}, String}) mktempdir() do dir cd(dir) do diff --git a/src/text_override.jl b/src/text_utils.jl similarity index 99% rename from src/text_override.jl rename to src/text_utils.jl index 2de1c9d..e18f43a 100644 --- a/src/text_override.jl +++ b/src/text_utils.jl @@ -149,6 +149,8 @@ to_array(f::AbstractVector) = f to_array(f::T) where T <: Makie.VecTypes = T[f] to_array(f::T) where T = T[f] +# We use Makie's spec-API to redirect text calls with our input to + ### WARNING: deprecated code lies below # this was rendered invalid by the text refactor, which # simplified all of the text calls into a central call diff --git a/src/types.jl b/src/types.jl index ce59c85..641ff72 100644 --- a/src/types.jl +++ b/src/types.jl @@ -100,6 +100,14 @@ These are primarily: 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 `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...) diff --git a/test/runtests.jl b/test/runtests.jl index 2f65798..97e343d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,7 +20,7 @@ function render_texample(url) fig = Figure() - lt = Label(fig[1, 1], CachedTeX(TeXDocument(read(Downloads.download(url), String)))) + lt = LTeX(fig[1, 1], CachedTeX(TeXDocument(read(Downloads.download(url), String), false))) @test true @@ -28,7 +28,7 @@ function render_texample(url) filename = splitdir(splitext(url)[1])[2] - save_test(joinpath(texample, filename), fig) + save_test(joinpath("texample", filename), fig) @test true @@ -38,7 +38,7 @@ end @testset "MakieTeX.jl" begin can_access_texample = try - Downloads.download("https://texample.net/media/tikz/examples/TEX/rotated_triangle.tex") + Downloads.download("https://texample.net/media/tikz/examples/TEX/rotated-triangle.tex") true catch e false @@ -72,7 +72,8 @@ end fig = Figure() - @test_warn r"The PDF has more than 1 page! Choosing the first page." Label(fig[1, 1], CachedTeX(TeXDocument(read(Downloads.download("https://texample.net/media/tikz/examples/TEX/mandala.tex"), String)))) + @test_warn r"There were 7 pages in the document! Selecting first page." LTeX(fig[1, 1], CachedTeX(TeXDocument(read(Downloads.download("https://texample.net/media/tikz/examples/TEX/mandala.tex"), String)))) + # @test_nowarn LTeX(fig[1, 1], CachedTeX(TeXDocument(read(Downloads.download("https://texample.net/media/tikz/examples/TEX/mandala.tex"), String)))) resize_to_layout!(fig) @@ -102,7 +103,7 @@ end # mkpath(joinpath(example_path, "aligns")) - # f = Figure(resolution = (200, 200)) + # f = Figure(size = (200, 200)) # lt = Label(f[1, 1], LaTeXString("Hello from Makie\\TeX{}!")) # teximg = lt.blockscene.plots[1] @@ -119,8 +120,8 @@ end @testset "Layouting" begin @testset "Logo" begin - fig = Figure(figure_padding = 1, resolution = (1, 1)) - @test_nowarn Label(fig[1, 1], LaTeXString("Makie\\TeX.jl")) + fig = Figure(figure_padding = 1, size = (1, 1)) + @test_nowarn LTeX(fig[1, 1], LaTeXString("Makie\\TeX.jl")) @test_nowarn resize_to_layout!(fig) save_test("logo", fig) @@ -132,7 +133,7 @@ end @testset "Corrupting Axis" begin - fig = Figure(fontsize = 12, resolution = (300, 300)) + fig = Figure(fontsize = 12, size = (300, 300)) # Create a GridLayout for the axis and labels gl = fig[1, 1] = GridLayout() # Create the Axis within this layout, leave space for the title and labels @@ -155,7 +156,7 @@ end end # @testset "Integrating with Axis" begin - # fig = Figure(fontsize = 12, resolution = (300, 300)) + # fig = Figure(fontsize = 12, size = (300, 300)) # ax = Axis( # fig[1,1]; # xlabel = LaTeXString("time (\$t\$) in arbitrary units"), @@ -192,7 +193,7 @@ end """) fig = Figure() - @test_nowarn lab = Label(fig[1, 1], td) + @test_nowarn lab = LTeX(fig[1, 1], td) @test_nowarn save_test("link", fig) end @@ -207,7 +208,7 @@ end # @testset "Theming" begin # @test_nowarn begin # fig = with_theme(theme_dark()) do - # fig = Figure(fontsize = 12, resolution = (300, 300)) + # fig = Figure(fontsize = 12, size = (300, 300)) # ax = Axis( # fig[1,1]; # xlabel = LaTeXString("time (\$t\$) in arbitrary units"),