-
-
Notifications
You must be signed in to change notification settings - Fork 290
/
screen.jl
313 lines (268 loc) · 11.1 KB
/
screen.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
using Base.Docs: doc
@enum RenderType SVG IMAGE PDF EPS
Base.convert(::Type{RenderType}, ::MIME{SYM}) where SYM = mime_to_rendertype(SYM)
function Base.convert(::Type{RenderType}, type::String)
if type == "png"
return IMAGE
elseif type == "svg"
return SVG
elseif type == "pdf"
return PDF
elseif type == "eps"
return EPS
else
error("Unsupported cairo render type: $type")
end
end
"Convert a rendering type to a MIME type"
function to_mime(type::RenderType)
type == SVG && return MIME("image/svg+xml")
type == PDF && return MIME("application/pdf")
type == EPS && return MIME("application/postscript")
return MIME("image/png")
end
"convert a mime to a RenderType"
function mime_to_rendertype(mime::Symbol)::RenderType
if mime == Symbol("image/png")
return IMAGE
elseif mime == Symbol("image/svg+xml")
return SVG
elseif mime == Symbol("application/pdf")
return PDF
elseif mime == Symbol("application/postscript")
return EPS
else
error("Unsupported mime: $mime")
end
end
function surface_from_output_type(mime::MIME{M}, io, w, h) where M
surface_from_output_type(M, io, w, h)
end
function surface_from_output_type(mime::Symbol, io, w, h)
surface_from_output_type(mime_to_rendertype(mime), io, w, h)
end
function surface_from_output_type(type::RenderType, io, w, h)
if type === SVG
return Cairo.CairoSVGSurface(io, w, h)
elseif type === PDF
return Cairo.CairoPDFSurface(io, w, h)
elseif type === EPS
return Cairo.CairoEPSSurface(io, w, h)
elseif type === IMAGE
img = fill(ARGB32(0, 0, 0, 0), w, h)
return Cairo.CairoImageSurface(img)
else
error("No available Cairo surface for mode $type")
end
end
"""
Supported options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]`
"""
function to_cairo_antialias(sym::Symbol)
sym === :best && return Cairo.ANTIALIAS_BEST
sym === :good && return Cairo.ANTIALIAS_GOOD
sym === :subpixel && return Cairo.ANTIALIAS_SUBPIXEL
sym === :none && return Cairo.ANTIALIAS_NONE
error("Wrong antialias setting: $(sym). Allowed: :best, :good, :subpixel, :none")
end
to_cairo_antialias(aa::Int) = aa
"""
* `px_per_unit = 1.0`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html).
* `pt_per_unit = 0.75`: see [figure size docs](https://docs.makie.org/v0.17.13/documentation/figure_size/index.html).
* `antialias::Union{Symbol, Int} = :best`: antialias modus Cairo uses to draw. Applicable options: `[:best => Cairo.ANTIALIAS_BEST, :good => Cairo.ANTIALIAS_GOOD, :subpixel => Cairo.ANTIALIAS_SUBPIXEL, :none => Cairo.ANTIALIAS_NONE]`.
* `visible::Bool`: if true, a browser/image viewer will open to display rendered output.
"""
struct ScreenConfig
px_per_unit::Float64
pt_per_unit::Float64
antialias::Symbol
visible::Bool
start_renderloop::Bool # Only used to satisfy the interface for record using `Screen(...; start_renderloop=false)` for GLMakie
end
function device_scaling_factor(rendertype, sc::ScreenConfig)
isv = is_vector_backend(convert(RenderType, rendertype))
return isv ? sc.pt_per_unit : sc.px_per_unit
end
function device_scaling_factor(surface::Cairo.CairoSurface, sc::ScreenConfig)
return is_vector_backend(surface) ? sc.pt_per_unit : sc.px_per_unit
end
const LAST_INLINE = Ref{Union{Makie.Automatic,Bool}}(Makie.automatic)
"""
CairoMakie.activate!(; screen_config...)
Sets CairoMakie as the currently active backend and also allows to quickly set the `screen_config`.
Note, that the `screen_config` can also be set permanently via `Makie.set_theme!(CairoMakie=(screen_config...,))`.
# Arguments one can pass via `screen_config`:
$(Base.doc(ScreenConfig))
"""
function activate!(; inline=LAST_INLINE[], type="png", screen_config...)
Makie.inline!(inline)
LAST_INLINE[] = inline
Makie.set_screen_config!(CairoMakie, screen_config)
if type == "png"
# So this is a bit counter intuitive, since the display system doesn't let us prefer a mime.
# Instead, any IDE with rich output usually has a priority list of mimes, which it iterates to figure out the best mime.
# So, if we want to prefer the png mime, we disable the mimes that are usually higher up in the stack.
disable_mime!("svg", "pdf")
elseif type == "svg"
# SVG is usually pretty high up the priority, so we can just enable all mimes
# If we implement html display for CairoMakie, we might need to disable that.
disable_mime!()
else
enable_only_mime!(type)
end
Makie.set_active_backend!(CairoMakie)
return
end
"""
Screen(; screen_config...)
# Arguments one can pass via `screen_config`:
$(Base.doc(ScreenConfig))
# Constructors:
$(Base.doc(MakieScreen))
"""
mutable struct Screen{SurfaceRenderType} <: Makie.MakieScreen
scene::Scene
surface::Cairo.CairoSurface
context::Cairo.CairoContext
device_scaling_factor::Float64
antialias::Int # cairo_antialias_t
visible::Bool
config::ScreenConfig
end
function Base.empty!(screen::Screen)
isopen(screen) || return
ctx = screen.context
Cairo.save(ctx)
bg = rgbatuple(screen.scene.backgroundcolor[])
Cairo.set_source_rgba(ctx, bg...)
Cairo.set_operator(ctx, Cairo.OPERATOR_CLEAR)
Cairo.rectangle(ctx, 0, 0, size(screen)...)
Cairo.paint_with_alpha(ctx, 1.0)
Cairo.restore(ctx)
end
Base.close(screen::Screen) = empty!(screen)
function destroy!(screen::Screen)
Cairo.destroy(screen.surface)
Cairo.destroy(screen.context)
end
function Base.isopen(screen::Screen)
return !(screen.surface.ptr == C_NULL || screen.context.ptr == C_NULL)
end
Base.size(screen::Screen) = round.(Int, (screen.surface.width, screen.surface.height))
# we render the scene directly, since we have
# no screen dependent state like in e.g. opengl
Base.insert!(screen::Screen, scene::Scene, plot) = nothing
function Base.delete!(screen::Screen, 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
function Base.show(io::IO, ::MIME"text/plain", screen::Screen{S}) where S
println(io, "CairoMakie.Screen{$S}")
end
function path_to_type(path)
type = splitext(path)[2][2:end]
return convert(RenderType, type)
end
to_mime(screen::Screen) = to_mime(screen.typ)
########################################
# Constructor #
########################################
function apply_config!(screen::Screen, config::ScreenConfig)
empty!(screen)
surface = screen.surface
context = screen.context
dsf = device_scaling_factor(surface, config)
surface_set_device_scale(surface, dsf)
aa = to_cairo_antialias(config.antialias)
Cairo.set_antialias(context, aa)
set_miter_limit(context, 2.0)
screen.antialias = aa
screen.device_scaling_factor = dsf
screen.config = config
return screen
end
function scaled_scene_resolution(typ::RenderType, config::ScreenConfig, scene::Scene)
dsf = device_scaling_factor(typ, config)
return round.(Int, size(scene) .* dsf)
end
function Makie.apply_screen_config!(
screen::Screen{SCREEN_RT}, config::ScreenConfig, scene::Scene, io::Union{Nothing, IO}, m::MIME{SYM}) where {SYM, SCREEN_RT}
# the surface size is the scene size scaled by the device scaling factor
new_rendertype = mime_to_rendertype(SYM)
# we need to re-create the screen if the rendertype changes, or for all vector backends
# since they need to use the new IO, or if the resolution changed!
new_resolution = scaled_scene_resolution(new_rendertype, config, scene)
if SCREEN_RT !== new_rendertype || is_vector_backend(new_rendertype) || size(screen) != new_resolution
old_screen = screen
surface = surface_from_output_type(new_rendertype, io, new_resolution...)
screen = Screen(scene, config, surface)
@assert new_resolution == size(screen)
destroy!(old_screen)
end
apply_config!(screen, config)
return screen
end
function Makie.apply_screen_config!(screen::Screen, config::ScreenConfig, scene::Scene, args...)
# No mime as an argument implies we want an image based surface
Makie.apply_screen_config!(screen, config, scene, nothing, MIME"image/png"())
end
function Screen(scene::Scene; screen_config...)
config = Makie.merge_screen_config(ScreenConfig, screen_config)
return Screen(scene, config)
end
Screen(scene::Scene, config::ScreenConfig) = Screen(scene, config, nothing, IMAGE)
function Screen(screen::Screen, io_or_path::Union{Nothing, String, IO}, typ::Union{MIME, Symbol, RenderType})
rtype = convert(RenderType, typ)
# the resolution may change between rendertypes, so, we can't just use `size(screen)` here for recreating the Screen:
w, h = scaled_scene_resolution(rtype, screen.config, screen.scene)
surface = surface_from_output_type(rtype, io_or_path, w, h)
return Screen(screen.scene, screen.config, surface)
end
function Screen(scene::Scene, config::ScreenConfig, io_or_path::Union{Nothing, String, IO}, typ::Union{MIME, Symbol, RenderType})
rtype = convert(RenderType, typ)
w, h = scaled_scene_resolution(rtype, config, scene)
surface = surface_from_output_type(rtype, io_or_path, w, h)
return Screen(scene, config, surface)
end
function Screen(scene::Scene, config::ScreenConfig, ::Makie.ImageStorageFormat)
w, h = scaled_scene_resolution(IMAGE, config, scene)
# create an image surface to draw onto the image
img = fill(ARGB32(0, 0, 0, 0), w, h)
surface = Cairo.CairoImageSurface(img)
return Screen(scene, config, surface)
end
function Screen(scene::Scene, config::ScreenConfig, surface::Cairo.CairoSurface)
# the surface size is the scene size scaled by the device scaling factor
dsf = device_scaling_factor(surface, config)
surface_set_device_scale(surface, dsf)
ctx = Cairo.CairoContext(surface)
aa = to_cairo_antialias(config.antialias)
Cairo.set_antialias(ctx, aa)
set_miter_limit(ctx, 2.0)
return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible, config)
end
########################################
# Fast colorbuffer for recording #
########################################
function Makie.colorbuffer(screen::Screen)
# extract scene
scene = screen.scene
# get resolution
w, h = size(screen)
# preallocate an image matrix
img = fill(ARGB32(0, 0, 0, 0), w, h)
# create an image surface to draw onto the image
surf = Cairo.CairoImageSurface(img)
s = Screen(scene, screen.config, surf)
return Makie.colorbuffer(s)
end
function Makie.colorbuffer(screen::Screen{IMAGE})
empty!(screen)
cairo_draw(screen, screen.scene)
return PermutedDimsArray(screen.surface.data, (2, 1))
end
is_vector_backend(ctx::Cairo.CairoContext) = is_vector_backend(ctx.surface)
is_vector_backend(surf::Cairo.CairoSurface) = is_vector_backend(get_render_type(surf))
is_vector_backend(rt::RenderType) = rt in (PDF, EPS, SVG)