Skip to content

Commit 0c74b11

Browse files
authored
fix(CairoMakie): Batch glyphs in PDF/SVG text export (#5561)
1 parent 0643dc4 commit 0c74b11

File tree

3 files changed

+72
-30
lines changed

3 files changed

+72
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- CairoMakie now batches glyphs from the same text string into a single PDF/SVG text object, so that text can be selected and edited as a unit in vector editors like Inkscape and Illustrator [#5561](https://github.com/MakieOrg/Makie.jl/pull/5561)
56
- Fixed `annotation` not showing lines/arrows when `text` is blank [#5560](https://github.com/MakieOrg/Makie.jl/pull/5560)
67

78
## [0.24.9] - 2026-03-04

CairoMakie/src/cairo-extension.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ function show_glyph(ctx, glyph, x, y)
6363
)
6464
end
6565

66+
function show_glyphs(ctx, glyphs::Vector{CairoGlyph})
67+
return ccall(
68+
(:cairo_show_glyphs, Cairo.libcairo),
69+
Nothing, (Ptr{Nothing}, Ptr{CairoGlyph}, Cint),
70+
ctx.ptr, glyphs, length(glyphs)
71+
)
72+
end
73+
6674
function glyph_path(ctx, glyph, x, y)
6775
cg = Ref(CairoGlyph(glyph, x, y))
6876
return ccall(
@@ -72,6 +80,14 @@ function glyph_path(ctx, glyph, x, y)
7280
)
7381
end
7482

83+
function glyphs_path(ctx, glyphs::Vector{CairoGlyph})
84+
return ccall(
85+
(:cairo_glyph_path, Cairo.libcairo),
86+
Nothing, (Ptr{Nothing}, Ptr{CairoGlyph}, Cint),
87+
ctx.ptr, glyphs, length(glyphs)
88+
)
89+
end
90+
7591
function surface_get_device_scale(surf)
7692
x = Ref(0.0)
7793
y = Ref(0.0)

CairoMakie/src/scatter.jl

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,34 @@ function draw_atomic_scatter(ctx, attr::NamedTuple)
113113
return
114114
end
115115

116+
function flush_glyph_batch!(ctx, glyph_buffer, font, color, mat, strokewidth, strokecolor)
117+
isempty(glyph_buffer) && return
118+
119+
cairoface = set_ft_font(ctx, font)
120+
old_matrix = get_font_matrix(ctx)
121+
122+
Cairo.save(ctx)
123+
Cairo.set_source_rgba(ctx, rgbatuple(color)...)
124+
set_font_matrix(ctx, mat)
125+
show_glyphs(ctx, glyph_buffer)
126+
Cairo.restore(ctx)
127+
128+
if strokewidth > 0 && strokecolor != RGBAf(0, 0, 0, 0)
129+
Cairo.save(ctx)
130+
set_font_matrix(ctx, mat)
131+
glyphs_path(ctx, glyph_buffer)
132+
Cairo.set_source_rgba(ctx, rgbatuple(strokecolor)...)
133+
Cairo.set_line_width(ctx, strokewidth)
134+
Cairo.stroke(ctx)
135+
Cairo.restore(ctx)
136+
end
137+
138+
cairo_font_face_destroy(cairoface)
139+
set_font_matrix(ctx, old_matrix)
140+
empty!(glyph_buffer)
141+
return
142+
end
143+
116144
function draw_text(ctx, attr::NamedTuple)
117145
positions = attr.positions_in_markerspace
118146
text_blocks = attr.text_blocks
@@ -134,15 +162,20 @@ function draw_text(ctx, attr::NamedTuple)
134162
view = attr.cam_view,
135163
)
136164

165+
glyph_buffer = CairoGlyph[]
166+
137167
for (block_idx, glyph_indices) in enumerate(text_blocks)
138-
Cairo.save(ctx) # Block save
168+
Cairo.save(ctx)
139169

140170
glyph_pos = positions[block_idx]
171+
local batch_font, batch_color, batch_mat, batch_strokewidth, batch_strokecolor
141172

142173
for glyph_idx in glyph_indices
143174
glyph_idx in valid_indices || continue
144175

145176
glyph = glyphindices[glyph_idx]
177+
glyph == 0 && continue
178+
146179
offset = marker_offset[glyph_idx]
147180
font = font_per_char[glyph_idx]
148181
rotation = Makie.sv_getindex(text_rotation, glyph_idx)
@@ -151,44 +184,36 @@ function draw_text(ctx, attr::NamedTuple)
151184
strokecolor = Makie.sv_getindex(text_strokecolor, glyph_idx)
152185
scale = Makie.sv_getindex(text_scales, glyph_idx)
153186

154-
# Not renderable by font (e.g. '\n')
155-
glyph == 0 && continue
156-
157-
# offsets and scale apply in markerspace
158187
gp3 = glyph_pos .+ size_model * offset
159-
160188
any(isnan, gp3) && continue
161189

162190
glyphpos, mat, _ = project_marker(cam, markerspace, Point3d(gp3), scale, rotation, size_model)
163191

164-
cairoface = set_ft_font(ctx, font)
165-
old_matrix = get_font_matrix(ctx)
166-
167-
Cairo.save(ctx) # Glyph save
168-
Cairo.set_source_rgba(ctx, rgbatuple(color)...)
169-
170-
Cairo.save(ctx) # Glyph rendering save
171-
set_font_matrix(ctx, mat)
172-
show_glyph(ctx, glyph, glyphpos...)
173-
Cairo.restore(ctx) # Glyph rendering restore
174-
175-
if strokewidth > 0 && strokecolor != RGBAf(0, 0, 0, 0)
176-
Cairo.save(ctx) # Stroke save
177-
Cairo.move_to(ctx, glyphpos...)
178-
set_font_matrix(ctx, mat)
179-
glyph_path(ctx, glyph, glyphpos...)
180-
Cairo.set_source_rgba(ctx, rgbatuple(strokecolor)...)
181-
Cairo.set_line_width(ctx, strokewidth)
182-
Cairo.stroke(ctx)
183-
Cairo.restore(ctx) # Stroke restore
192+
if !isempty(glyph_buffer) && (
193+
font !== batch_font ||
194+
color != batch_color ||
195+
mat != batch_mat ||
196+
strokewidth != batch_strokewidth ||
197+
strokecolor != batch_strokecolor
198+
)
199+
flush_glyph_batch!(ctx, glyph_buffer, batch_font, batch_color, batch_mat, batch_strokewidth, batch_strokecolor)
184200
end
185201

186-
Cairo.restore(ctx) # Glyph restore (matches glyph save above)
187-
cairo_font_face_destroy(cairoface)
188-
set_font_matrix(ctx, old_matrix)
202+
if isempty(glyph_buffer)
203+
batch_font = font
204+
batch_color = color
205+
batch_mat = mat
206+
batch_strokewidth = strokewidth
207+
batch_strokecolor = strokecolor
208+
end
209+
210+
push!(glyph_buffer, CairoGlyph(glyph, glyphpos[1], glyphpos[2]))
189211
end
190212

191-
Cairo.restore(ctx) # Block restore
213+
if !isempty(glyph_buffer)
214+
flush_glyph_batch!(ctx, glyph_buffer, batch_font, batch_color, batch_mat, batch_strokewidth, batch_strokecolor)
215+
end
216+
Cairo.restore(ctx)
192217
end
193218
return
194219
end

0 commit comments

Comments
 (0)