Skip to content

Commit

Permalink
More improvements to SVG rendering
Browse files Browse the repository at this point in the history
The Wikipedia icon now works.  SVGs are placed and scaled correctly.

The only remaining question is coloring.
  • Loading branch information
asinghvi17 committed Apr 9, 2024
1 parent 9f3c981 commit bfad07e
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 29 deletions.
1 change: 0 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 27 additions & 17 deletions ext/MakieTeXCairoMakieExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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


Expand Down
9 changes: 9 additions & 0 deletions src/rendering/pdf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.

"""
Expand Down
29 changes: 27 additions & 2 deletions src/rendering/svg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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

Expand Down
65 changes: 56 additions & 9 deletions src/types.jl
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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."
Expand All @@ -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."
Expand All @@ -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)))
Expand Down
25 changes: 25 additions & 0 deletions test/svg.jl
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit bfad07e

Please sign in to comment.