-
-
Notifications
You must be signed in to change notification settings - Fork 290
/
infrastructure.jl
426 lines (348 loc) · 15.4 KB
/
infrastructure.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
####################################################################################################
# Infrastructure #
####################################################################################################
################################################################################
# Types #
################################################################################
@enum RenderType SVG PNG PDF EPS
"The Cairo backend object. Used to dispatch to CairoMakie methods."
struct CairoBackend <: Makie.AbstractBackend
typ::RenderType
path::String
px_per_unit::Float64
pt_per_unit::Float64
antialias::Int # cairo_antialias_t
end
"""
struct CairoScreen{S} <: AbstractScreen
A "screen" type for CairoMakie, which encodes a surface
and a context which are used to draw a Scene.
"""
struct CairoScreen{S} <: Makie.AbstractScreen
scene::Scene
surface::S
context::Cairo.CairoContext
pane::Nothing # TODO: GtkWindowLeaf
end
function CairoBackend(path::String; px_per_unit=1, pt_per_unit=1, antialias = Cairo.ANTIALIAS_BEST)
ext = splitext(path)[2]
typ = if ext == ".png"
PNG
elseif ext == ".svg"
SVG
elseif ext == ".pdf"
PDF
elseif ext == ".eps"
EPS
else
error("Unsupported extension: $ext")
end
CairoBackend(typ, path, px_per_unit, pt_per_unit, antialias)
end
# we render the scene directly, since we have
# no screen dependent state like in e.g. opengl
Base.insert!(screen::CairoScreen, scene::Scene, plot) = nothing
function Base.show(io::IO, ::MIME"text/plain", screen::CairoScreen{S}) where S
println(io, "CairoScreen{$S} with surface:")
println(io, screen.surface)
end
# Default to ARGB Surface as backing device
# TODO: integrate Gtk into this, so we can have an interactive display
"""
CairoScreen(scene::Scene; antialias = Cairo.ANTIALIAS_BEST)
Create a CairoScreen backed by an image surface.
"""
function CairoScreen(scene::Scene; device_scaling_factor = 1, antialias = Cairo.ANTIALIAS_BEST)
w, h = round.(Int, scene.camera.resolution[] .* device_scaling_factor)
surf = Cairo.CairoARGBSurface(w, h)
# this sets a scaling factor on the lowest level that is "hidden" so its even
# enabled when the drawing space is reset for strokes
# that means it can be used to increase or decrease the image resolution
ccall((:cairo_surface_set_device_scale, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble, Cdouble),
surf.ptr, device_scaling_factor, device_scaling_factor)
ctx = Cairo.CairoContext(surf)
Cairo.set_antialias(ctx, antialias)
# Set the miter limit (when miter transitions to bezel) to mimic GLMakie behaviour
ccall((:cairo_set_miter_limit, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble), ctx.ptr, 2.0)
return CairoScreen(scene, surf, ctx, nothing)
end
function get_type(surface::Cairo.CairoSurface)
return ccall((:cairo_surface_get_type, Cairo.libcairo), Cint, (Ptr{Nothing},), surface.ptr)
end
is_vector_backend(ctx::Cairo.CairoContext) = is_vector_backend(ctx.surface)
function is_vector_backend(surf::Cairo.CairoSurface)
typ = get_type(surf)
return typ in (Cairo.CAIRO_SURFACE_TYPE_PDF, Cairo.CAIRO_SURFACE_TYPE_PS, Cairo.CAIRO_SURFACE_TYPE_SVG)
end
"""
CairoScreen(
scene::Scene, path::Union{String, IO}, mode::Symbol;
antialias = Cairo.ANTIALIAS_BEST
)
Creates a CairoScreen pointing to a given output path, with some rendering type defined by `mode`.
"""
function CairoScreen(scene::Scene, path::Union{String, IO}, mode::Symbol; device_scaling_factor = 1, antialias = Cairo.ANTIALIAS_BEST)
# the surface size is the scene size scaled by the device scaling factor
w, h = round.(Int, scene.camera.resolution[] .* device_scaling_factor)
if mode == :svg
surf = Cairo.CairoSVGSurface(path, w, h)
elseif mode == :pdf
surf = Cairo.CairoPDFSurface(path, w, h)
elseif mode == :eps
surf = Cairo.CairoEPSSurface(path, w, h)
elseif mode == :png
surf = Cairo.CairoARGBSurface(w, h)
else
error("No available Cairo surface for mode $mode")
end
# this sets a scaling factor on the lowest level that is "hidden" so its even
# enabled when the drawing space is reset for strokes
# that means it can be used to increase or decrease the image resolution
ccall((:cairo_surface_set_device_scale, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble, Cdouble),
surf.ptr, device_scaling_factor, device_scaling_factor)
ctx = Cairo.CairoContext(surf)
Cairo.set_antialias(ctx, antialias)
# Set the miter limit (when miter transitions to bezel) to mimic GLMakie behaviour
ccall((:cairo_set_miter_limit, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble), ctx.ptr, 2.0)
return CairoScreen(scene, surf, ctx, nothing)
end
function Base.delete!(screen::CairoScreen, scene::Scene, plot::AbstractPlot)
# Currently, we rerender every time, so nothing needs
# to happen here. However, in the event that changes,
# e.g. if we integrate a Gtk window, we may need to
# do something here.
end
"Convert a rendering type to a MIME type"
function to_mime(x::RenderType)
x == SVG && return MIME("image/svg+xml")
x == PDF && return MIME("application/pdf")
x == EPS && return MIME("application/postscript")
return MIME("image/png")
end
to_mime(x::CairoBackend) = to_mime(x.typ)
################################################################################
# Rendering pipeline #
################################################################################
########################################
# Drawing pipeline #
########################################
# The main entry point into the drawing pipeline
function cairo_draw(screen::CairoScreen, scene::Scene)
draw_background(screen, scene)
allplots = get_all_plots(scene)
zvals = Makie.zvalue2d.(allplots)
permute!(allplots, sortperm(zvals))
# If the backend is not a vector surface (i.e., PNG/ARGB),
# then there is no point in rasterizing twice.
should_rasterize = is_vector_backend(screen.surface)
last_scene = scene
Cairo.save(screen.context)
for p in allplots
to_value(get(p, :visible, true)) || continue
# only prepare for scene when it changes
# this should reduce the number of unnecessary clipping masks etc.
pparent = p.parent::Scene
pparent.visible[] || continue
if pparent != last_scene
Cairo.restore(screen.context)
Cairo.save(screen.context)
prepare_for_scene(screen, pparent)
last_scene = pparent
end
Cairo.save(screen.context)
# This is a bit of a hack for now. When a plot is too large to save with
# a reasonable file size on a vector backend, the user can choose to
# rasterize it when plotting to vector backends, by using the `rasterize`
# keyword argument. This can be set to a Bool or an Int which describes
# the density of rasterization (in terms of a direct scaling factor.)
if to_value(get(p, :rasterize, false)) != false && should_rasterize
draw_plot_as_image(pparent, screen, p, p[:rasterize][])
else # draw vector
draw_plot(pparent, screen, p)
end
Cairo.restore(screen.context)
end
return
end
function get_all_plots(scene, plots = AbstractPlot[])
append!(plots, scene.plots)
for c in scene.children
get_all_plots(c, plots)
end
plots
end
function prepare_for_scene(screen::CairoScreen, scene::Scene)
# get the root area to correct for its pixel size when translating
root_area = Makie.root(scene).px_area[]
root_area_height = widths(root_area)[2]
scene_area = pixelarea(scene)[]
scene_height = widths(scene_area)[2]
scene_x_origin, scene_y_origin = scene_area.origin
# we need to translate x by the origin, so distance from the left
# but y by the distance from the top, which is not the origin, but can
# be calculated using the parent's height, the scene's height and the y origin
# this is because y goes downwards in Cairo and upwards in Makie
top_offset = root_area_height - scene_height - scene_y_origin
Cairo.translate(screen.context, scene_x_origin, top_offset)
# clip the scene to its pixelarea
Cairo.rectangle(screen.context, 0, 0, widths(scene_area)...)
Cairo.clip(screen.context)
return
end
function draw_background(screen::CairoScreen, scene::Scene)
cr = screen.context
Cairo.save(cr)
if scene.clear[]
bg = to_color(theme(scene, :backgroundcolor)[])
Cairo.set_source_rgba(cr, red(bg), green(bg), blue(bg), alpha(bg));
r = pixelarea(scene)[]
Cairo.rectangle(cr, origin(r)..., widths(r)...) # background
fill(cr)
end
Cairo.restore(cr)
foreach(child_scene-> draw_background(screen, child_scene), scene.children)
end
function draw_plot(scene::Scene, screen::CairoScreen, primitive::Combined)
if to_value(get(primitive, :visible, true))
if isempty(primitive.plots)
Cairo.save(screen.context)
draw_atomic(scene, screen, primitive)
Cairo.restore(screen.context)
else
for plot in primitive.plots
draw_plot(scene, screen, plot)
end
end
end
return
end
# Possible improvements for this function:
# - Obtain the bbox of the plot and draw an image which tightly fits that bbox
# instead of the whole Scene
# - Recognize when a screen is an image surface, and set scale to render the plot
# at the scale of the device pixel
function draw_plot_as_image(scene::Scene, screen::CairoScreen, primitive::Combined, scale::Number = 1)
# you can provide `p.rasterize = scale::Int` or `p.rasterize = true`, both of which are numbers
# Extract scene width in pixels
w, h = Int.(scene.px_area[].widths)
# Create a new Screen which renders directly to an image surface,
# specifically for the plot's parent scene.
scr = CairoScreen(scene; device_scaling_factor = scale)
# Draw the plot to the screen, in the normal way
draw_plot(scene, scr, primitive)
# Now, we draw the rasterized plot to the main screen.
# Since it has already been prepared by `prepare_for_scene`,
# we can draw directly to the Screen.
Cairo.rectangle(screen.context, 0, 0, w, h)
Cairo.save(screen.context)
Cairo.translate(screen.context, 0, 0)
# Cairo.scale(screen.context, w / scr.surface.width, h / scr.surface.height)
Cairo.set_source_surface(screen.context, scr.surface, 0, 0)
p = Cairo.get_source(scr.context)
# this is needed to avoid blurry edges
Cairo.pattern_set_extend(p, Cairo.EXTEND_PAD)
# Set filter doesn't work!?
Cairo.pattern_set_filter(p, Cairo.FILTER_BILINEAR)
Cairo.fill(screen.context)
Cairo.restore(screen.context)
return
end
function draw_atomic(::Scene, ::CairoScreen, x)
@warn "$(typeof(x)) is not supported by cairo right now"
end
function clear(screen::CairoScreen)
ctx = screen.ctx
Cairo.save(ctx)
Cairo.set_operator(ctx, Cairo.OPERATOR_SOURCE)
Cairo.set_source_rgba(ctx, rgbatuple(screen.scene[:backgroundcolor])...);
Cairo.paint(ctx)
Cairo.restore(ctx)
end
#########################################
# Backend interface to Makie #
#########################################
function Makie.backend_display(x::CairoBackend, scene::Scene; kw...)
return open(x.path, "w") do io
Makie.backend_show(x, io, to_mime(x), scene)
end
end
Makie.backend_showable(x::CairoBackend, ::MIME"image/svg+xml", scene::Scene) = x.typ == SVG
Makie.backend_showable(x::CairoBackend, ::MIME"application/pdf", scene::Scene) = x.typ == PDF
Makie.backend_showable(x::CairoBackend, ::MIME"application/postscript", scene::Scene) = x.typ == EPS
Makie.backend_showable(x::CairoBackend, ::MIME"image/png", scene::Scene) = x.typ == PNG
function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"image/svg+xml", scene::Scene)
proxy_io = IOBuffer()
pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit)
antialias = get(io, :antialias, x.antialias)
screen = CairoScreen(scene, proxy_io, :svg; device_scaling_factor = pt_per_unit, antialias = antialias)
cairo_draw(screen, scene)
Cairo.flush(screen.surface)
Cairo.finish(screen.surface)
svg = String(take!(proxy_io))
# for some reason, in the svg, surfaceXXX ids keep counting up,
# even with the very same figure drawn again and again
# so we need to reset them to counting up from 1
# so that the same figure results in the same svg and in the same salt
surfaceids = sort(unique(collect(m.match for m in eachmatch(r"surface\d+", svg))))
for (i, id) in enumerate(surfaceids)
svg = replace(svg, id => "surface$i")
end
# salt svg ids with the first 8 characters of the base64 encoded
# sha512 hash to avoid collisions across svgs when embedding them on
# websites. the hash and therefore the salt will always be the same for the same file
# so the output is deterministic
salt = String(Base64.base64encode(SHA.sha512(svg)))[1:8]
ids = sort(unique(collect(m[1] for m in eachmatch(r"id\s*=\s*\"([^\"]*)\"", svg))))
for id in ids
svg = replace(svg, id => "$id-$salt")
end
print(io, svg)
return screen
end
function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"application/pdf", scene::Scene)
pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit)
antialias = get(io, :antialias, x.antialias)
screen = CairoScreen(scene, io, :pdf; device_scaling_factor = pt_per_unit, antialias = antialias)
cairo_draw(screen, scene)
Cairo.finish(screen.surface)
return screen
end
function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"application/postscript", scene::Scene)
pt_per_unit = get(io, :pt_per_unit, x.pt_per_unit)
antialias = get(io, :antialias, x.antialias)
screen = CairoScreen(scene, io, :eps; device_scaling_factor = pt_per_unit, antialias = antialias)
cairo_draw(screen, scene)
Cairo.finish(screen.surface)
return screen
end
function Makie.backend_show(x::CairoBackend, io::IO, ::MIME"image/png", scene::Scene)
# multiply the resolution of the png with this factor for more or less detail
# while relative line and font sizes are unaffected
px_per_unit = get(io, :px_per_unit, x.px_per_unit)
antialias = get(io, :antialias, x.antialias)
# create an ARGB surface, to speed up drawing ops.
screen = CairoScreen(scene; device_scaling_factor = px_per_unit, antialias = antialias)
cairo_draw(screen, scene)
Cairo.write_to_png(screen.surface, io)
return screen
end
########################################
# Fast colorbuffer for recording #
########################################
function Makie.colorbuffer(screen::CairoScreen)
# extract scene
scene = screen.scene
# get resolution
w, h = size(scene)
# preallocate an image matrix
img = Matrix{ARGB32}(undef, w, h)
# create an image surface to draw onto the image
surf = Cairo.CairoImageSurface(img)
# draw the scene onto the image matrix
ctx = Cairo.CairoContext(surf)
ccall((:cairo_set_miter_limit, Cairo.libcairo), Cvoid, (Ptr{Nothing}, Cdouble), ctx.ptr, 2.0)
scr = CairoScreen(scene, surf, ctx, nothing)
cairo_draw(scr, scene)
# x and y are flipped - return the transpose
return permutedims(img)
end