diff --git a/Project.toml b/Project.toml index 8f52bcf..58746fb 100644 --- a/Project.toml +++ b/Project.toml @@ -12,7 +12,6 @@ Ghostscript_jll = "61579ee1-b43e-5ca0-a5da-69d92c66a64b" Glib_jll = "7746bdde-850d-59dc-9ae8-88ece973131d" LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" -Perl_jll = "83958c19-0796-5285-893e-a1267f8ec499" Poppler_jll = "9c32591e-4766-534b-9725-b71a8799265b" Rsvg = "c4c386cf-5103-5370-be45-f3a111cca3b8" tectonic_jll = "d7dd28d6-a5e6-559c-9131-7eb760cdacc5" diff --git a/ext/MakieTeXCairoMakieExt.jl b/ext/MakieTeXCairoMakieExt.jl index 5d64968..3ed7271 100644 --- a/ext/MakieTeXCairoMakieExt.jl +++ b/ext/MakieTeXCairoMakieExt.jl @@ -6,7 +6,7 @@ using MakieTeX.Makie.MakieCore using MakieTeX.Poppler_jll using MakieTeX.Cairo using MakieTeX.Colors -using MakieTeX.Base64 +using MakieTeX.Rsvg # CairoMakie direct drawing method function draw_tex(scene, screen::CairoMakie.Screen, cachedtex::MakieTeX.CachedTeX, position::VecTypes, scale::VecTypes, rotation::Real, align::Tuple{Symbol, Symbol}) @@ -122,28 +122,38 @@ function CairoMakie.draw_plot(scene::Makie.Scene, screen::CairoMakie.Screen, img end +function cairo_pattern_get_rgba(pattern::CairoPattern) + r, g, b, a = (Ref(0.0) for _ in 1:4) + @ccall Cairo.Cairo_jll.libcairo.cairo_pattern_get_rgba(pattern.ptr::Ptr{Cvoid}, r::Ptr{Cdouble}, g::Ptr{Cdouble}, b::Ptr{Cdouble}, a::Ptr{Cdouble})::Cint + return RGBA{Float64}(r[], g[], b[], a[]) +end + function CairoMakie.draw_marker(ctx, marker::MakieTeX.CachedSVG, pos, scale, strokecolor #= unused =#, strokewidth #= unused =#, marker_offset, rotation) - - # convert marker to Cairo compatible image data - marker_surf = marker.surf - - w, h = marker.dims - + Cairo.save(ctx) + # Obtain the initial color from the pattern. + # This allows us to support marker coloring by CSS themes. pattern = Cairo.get_source(ctx) - r, g, b, a = (Ref(0.0) for _ in 1:4) - cairo_status_for_get_rgba = @ccall Cairo.Cairo_jll.libcairo.cairo_pattern_get_rgba(pattern.ptr::Ptr{Cvoid}, r::Ptr{Cdouble}, g::Ptr{Cdouble}, b::Ptr{Cdouble}, a::Ptr{Cdouble})::Cint - color_hex = Colors.hex(RGBAf(r[], g[], b[], a[])) - style_string = "color:#$color_hex; stroke:#$(Colors.hex(strokecolor)); stroke-width:$strokewidth;" - #rsvg_handle_success = @ccall Rsvg.Librsvg_jll.librsvg.rsvg_handle_set_stylesheet(handle.ptr::Ptr{Cvoid}, style_string::Cstring, length(style_string)::Csize_t, C_NULL::Ptr{Cvoid})::Bool + color_hex = Colors.hex(cairo_pattern_get_rgba(pattern)) + # Generate a CSS style string for the SVG. + style_string = """fill: #$color_hex; stroke: #$(Colors.hex(strokecolor)); stroke-width: $strokewidth;""" + @show style_string + # Set the stylesheet for the Rsvg handle + # Here, we generate the Rsvg handle from the original SVG document, and don't use the cached version. + # This is because I'm not sure whether the repeated setting of the stylesheeet will affect the cached version. + # If it does not, then we can simply replace this line with `marker.handle[]`, + # and go about our merry way. + handle = MakieTeX.svg2rsvg(marker.doc.doc) + rsvg_handle_success = @ccall Rsvg.Librsvg_jll.librsvg.rsvg_handle_set_stylesheet(handle.ptr::Ptr{Cvoid}, style_string::Cstring, length(style_string)::Csize_t, C_NULL::Ptr{Cvoid})::Bool + rsvg_handle_success || @warn("MakieTeX: Failed to set stylesheet for Rsvg handle.") + # Begin the drawing process Cairo.translate(ctx, - scale[1]/2 + pos[1] + marker_offset[1], - scale[2]/2 + pos[2] + marker_offset[2]) + pos[1] #= the initial marker position =# + marker_offset[1] #= the marker offset =# - scale[1]#= center of the marker =#, + pos[2] #= the initial marker position =# + marker_offset[2] #= the marker offset =# - scale[2]#= center of the marker =#,) Cairo.rotate(ctx, CairoMakie.to_2d_rotation(rotation)) - Cairo.scale(ctx, scale[1] / w, scale[2] / h) - Cairo.set_source_surface(ctx, marker_surf, -w/2, -h/2) - Cairo.paint(ctx) + MakieTeX.handle_render_document(ctx, handle, MakieTeX._RsvgRectangle(scale[2], scale[1], scale[1], scale[2])) + Cairo.restore(ctx) end diff --git a/src/rendering/pdf.jl b/src/rendering/pdf.jl index 9b98817..0842a7d 100644 --- a/src/rendering/pdf.jl +++ b/src/rendering/pdf.jl @@ -15,7 +15,16 @@ function CachedPDF(pdf::PDFDocument, page::Int = 0) return CachedPDF(pdf, Ref(ptr), dims, surf) end +function rasterize(pdf::CachedPDF, scale::Real = 1) + if last(pdf.image_cache[]) == scale + return first(pdf.image_cache[]) + else + img = pdf2img(pdf, page; scale) + pdf.image_cache[] = (img, scale) + return img + end +end # Pure poppler pipeline - directly from PDF to Cairo surface. """ diff --git a/src/rendering/svg.jl b/src/rendering/svg.jl index 111eb23..32f5d00 100644 --- a/src/rendering/svg.jl +++ b/src/rendering/svg.jl @@ -9,7 +9,7 @@ Finally, it implements the MakieTeX cached-document API. =# function CachedSVG(svg::SVGDocument) - handle = svg2rsvg(svg.svg) + handle = svg2rsvg(svg.doc) surf, ctx = rsvg2recordsurf(handle) dh = Rsvg.handle_get_dimensions(handle) dims = Float64.((dh.width, dh.height)) @@ -26,6 +26,30 @@ function rasterize(ct::CachedSVG, scale::Real = 1) end end +# First, fill in missing parts of the `Rsvg.jl` API which allow us to use new and approve Rsvg primitives. + +""" +RsvgRectangle is a simple struct of: + height::Float64 + width::Float64 + x::Float64 + y::Float64 +""" +struct _RsvgRectangle + height::Float64 + width::Float64 + x::Float64 # origin + y::Float64 # origin +end + +""" +handle_render_document(cr::CairoContext, handle::RsvgHandle, viewport::_RsvgRectangle) +""" +function handle_render_document(cr::CairoContext, handle::RsvgHandle, viewport::_RsvgRectangle) + ccall((:rsvg_handle_render_document, Rsvg.Librsvg_jll.librsvg), Bool, + (Rsvg.RsvgHandle, Ptr{Nothing}, Ref{_RsvgRectangle}, Ptr{Nothing}), handle, cr.ptr, Ref(viewport), C_NULL) +end + function svg2rsvg(svg::String, dpi = 72.0) handle = Rsvg.handle_new_from_data(svg) Rsvg.handle_set_dpi(handle, Float64(dpi)) @@ -35,7 +59,8 @@ end function rsvg2recordsurf(handle::Rsvg.RsvgHandle) surf = Cairo.CairoRecordingSurface() ctx = Cairo.CairoContext(surf) - Rsvg.handle_render_cairo(ctx, handle) + d = Rsvg.handle_get_dimensions(handle) + handle_render_document(ctx, handle, _RsvgRectangle(d.height, d.width, 0, 0)) return (surf, ctx) end diff --git a/src/types.jl b/src/types.jl index 909dbff..9742263 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,9 +1,51 @@ +""" + abstract type AbstractDocument + +""" abstract type AbstractDocument end + +""" + abstract type AbstractCachedDocument + +""" abstract type AbstractCachedDocument <: AbstractDocument end +""" + rasterize(doc::AbstractCachedDocument, scale::Real = 1) + +Render a `CachedDocument` to an image at a given scale. This is a convenience function which +calls the appropriate rendering function for the document type. Returns an image as a `Matrix{ARGB32}`. +""" function rasterize end +""" + draw_to_cairo_surface(doc::AbstractCachedDocument, surf::CairoSurface) + +Render a `CachedDocument` to a Cairo surface. This is a convenience function which +calls the appropriate rendering function for the document type. +""" function draw_to_cairo_surface end +""" + Cached(doc::AbstractDocument)::AbstractCachedDocument + +Generic interface to cache a document and return it. +""" +function Cached end + +# Define Makie conversion functions which keep these types in the pipeline. +# The backend-specific functions and rasterizers are kept in the backends' extensions. +Makie.convert_attribute(x::AbstractCachedDocument, ::Makie.key"marker") = x +Makie.convert_attribute(x::AbstractCachedDocument, ::Makie.key"marker", ::Makie.key"scatter") = x +Makie.to_spritemarker(x::AbstractCachedDocument) = Cached(x) + +Makie.convert_attribute(x::AbstractDocument, ::Makie.key"marker") = Cached(x) +Makie.convert_attribute(x::AbstractDocument, ::Makie.key"marker", ::Makie.key"scatter") = Cached(x) +Makie.to_spritemarker(x::AbstractDocument) = Cached(x) # this should never be called + +#= +Now, we define the structs which hold the documents and their cached versions. +=# + """ SVGDocument(svg::AbstractString) @@ -12,8 +54,9 @@ A document type which stores an SVG string. Is converted to [`CachedSVG`](@ref) for use in plotting. """ struct SVGDocument <: AbstractDocument - svg::String + doc::String end +Cached(x::SVGDocument) = CachedSVG(x) """ PDFDocument(pdf::AbstractString) @@ -23,8 +66,11 @@ A document type which holds a raw PDF as a string. Is converted to [`CachedPDF`](@ref) for use in plotting. """ struct PDFDocument <: AbstractDocument - pdf::String + doc::String + page::Union{Nothing, Int} end +PDFDocument(doc::String) = PDFDocument(doc, 0) +Cached(x::PDFDocument) = CachedPDF(x) """ EPSDocument(eps::AbstractString) @@ -34,12 +80,15 @@ A document type which holds an EPS string. Is converted to [`CachedPDF`](@ref) for use in plotting. """ struct EPSDocument <: AbstractDocument - eps::String + doc::String end +Cached(x::EPSDocument) = CachedPDF(x) + struct TeXDocument <: AbstractDocument contents::String end +Cached(x::TeXDocument) = CachedTeX(x) """ TeXDocument(contents::AbstractString, add_defaults::Bool; requires, preamble, class, classoptions) @@ -137,9 +186,9 @@ CachedPDF(PDFDocument(...), [page = 0]) """ struct CachedPDF <: AbstractCachedDocument "A reference to the `PDFDocument` which is cached here." - pdf::PDFDocument + doc::PDFDocument "A pointer to the Poppler handle of the PDF. May be randomly GC'ed by Poppler." - poppler::Ref{Ptr{Cvoid}} + ptr::Ref{Ptr{Cvoid}} "The dimensions of the PDF page in points, for ease of access." dims::Tuple{Float64, Float64} "A Cairo surface to which Poppler has drawn the PDF. Permanent and cached." @@ -156,7 +205,7 @@ end struct CachedSVG <: AbstractCachedDocument "The original `SVGDocument` which is cached here, i.e., the text of that SVG." - svg::SVGDocument + doc::SVGDocument "A pointer to the Rsvg handle of the SVG. May be randomly GC'ed by Rsvg, so is stored as a `Ref` in case it has to be refreshed." handle::Ref{Rsvg.RsvgHandle} "The dimensions of the SVG in points, for ease of access." @@ -167,9 +216,7 @@ struct CachedSVG <: AbstractCachedDocument image_cache::Ref{Tuple{Matrix{ARGB32}, Float64}} end -Makie.convert_attribute(x::AbstractCachedDocument, ::Makie.key"marker") = x -Makie.convert_attribute(x::AbstractCachedDocument, ::Makie.key"marker", ::Makie.key"scatter") = x -Makie.to_spritemarker(x::AbstractCachedDocument) = x + function CachedSVG(svg::SVGDocument, rsvg_handle::Rsvg.RsvgHandle, dims::Tuple{Float64, Float64}, surf::CairoSurface) return CachedSVG(svg, Ref(rsvg_handle), dims, surf, Ref{Tuple{Matrix{ARGB32}, Float64}}((Matrix{ARGB32}(undef, 0, 0), 0))) diff --git a/test/svg.jl b/test/svg.jl new file mode 100644 index 0000000..e8eac2a --- /dev/null +++ b/test/svg.jl @@ -0,0 +1,25 @@ +using MakieTeX, CairoMakie +using Downloads +using Rsvg, Cairo + + +svg = SVGDocument(read(Base.download("https://raw.githubusercontent.com/file-icons/icons/master/svg/Go-Old.svg"), String)); +bvsvg = SVGDocument(read(Base.download("https://upload.wikimedia.org/wikipedia/commons/6/6b/Bitmap_VS_SVG.svg"), String)); +wsvg = SVGDocument(read(Base.download("https://upload.wikimedia.org/wikipedia/en/8/80/Wikipedia-logo-v2.svg"), String)); + + + +csvg = CachedSVG(svg) +cbvsvg = CachedSVG(bvsvg) +cwsvg = CachedSVG(wsvg) + +f, a, p = scatter(Point2f(1); marker = svg, markersize = 60, axis = (; limits = (0,2,0,2))) +p.color = :red +p.strokecolor = :blue +p.strokewidth = 2 +f +ys = rand(10) +f, a, p1 = scatter(ys; marker = wsvg, markersize = 30) +p2 = scatter!(ys; marker = Circle, markersize = 30) +translate!(p2, 0,0,-1) +f \ No newline at end of file