/src/harfbuzz/src/hb-vector-paint-pdf.cc
Line | Count | Source |
1 | | /* |
2 | | * Copyright © 2026 Behdad Esfahbod |
3 | | * |
4 | | * This is part of HarfBuzz, a text shaping library. |
5 | | * |
6 | | * Permission is hereby granted, without written agreement and without |
7 | | * license or royalty fees, to use, copy, modify, and distribute this |
8 | | * software and its documentation for any purpose, provided that the |
9 | | * above copyright notice and the following two paragraphs appear in |
10 | | * all copies of this software. |
11 | | * |
12 | | * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR |
13 | | * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES |
14 | | * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN |
15 | | * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH |
16 | | * DAMAGE. |
17 | | * |
18 | | * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, |
19 | | * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND |
20 | | * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS |
21 | | * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO |
22 | | * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
23 | | * |
24 | | * Author(s): Behdad Esfahbod |
25 | | */ |
26 | | |
27 | | #include "hb.hh" |
28 | | |
29 | | #include "hb-vector-paint.hh" |
30 | | #include "hb-vector-draw.hh" |
31 | | #include "hb-paint.hh" |
32 | | |
33 | | #include <math.h> |
34 | | #include <stdio.h> |
35 | | |
36 | | #ifdef HAVE_ZLIB |
37 | | #include <zlib.h> |
38 | | #endif |
39 | | |
40 | | /* PDF paint backend for COLRv0/v1 color font rendering. |
41 | | * |
42 | | * Supports: |
43 | | * - Solid colors with alpha (via ExtGState) |
44 | | * - Glyph and rectangle clipping |
45 | | * - Affine transforms |
46 | | * - Groups (save/restore) |
47 | | * - Linear gradients (PDF Type 2 axial shading) |
48 | | * - Radial gradients (PDF Type 3 radial shading) |
49 | | * - Sweep gradients (approximated via solid fallback) |
50 | | */ |
51 | | |
52 | | |
53 | | /* ---- PDF object collector ---- */ |
54 | | |
55 | | struct hb_pdf_obj_t |
56 | | { |
57 | | hb_vector_buf_t data; |
58 | | }; |
59 | | |
60 | | /* Collects extra PDF objects (shadings, functions, ExtGState) |
61 | | * during painting. Referenced from the content stream by name |
62 | | * (e.g. /SH0, /GS0) and emitted at render time. */ |
63 | | static bool |
64 | | hb_pdf_build_indexed_smask (hb_vector_buf_t *out, |
65 | | const char *idat_data, unsigned idat_len, |
66 | | unsigned width, unsigned height, |
67 | | const uint8_t *trns, unsigned trns_len); |
68 | | |
69 | | struct hb_pdf_resources_t |
70 | | { |
71 | | hb_vector_t<hb_pdf_obj_t> objects; /* extra objects, starting at id 5 */ |
72 | | hb_vector_buf_t extgstate_dict; /* /GS0 5 0 R /GS1 6 0 R ... */ |
73 | | hb_vector_buf_t shading_dict; /* /SH0 7 0 R ... */ |
74 | | hb_vector_buf_t xobject_dict; /* /Im0 8 0 R ... */ |
75 | | unsigned extgstate_count = 0; |
76 | | unsigned shading_count = 0; |
77 | | unsigned xobject_count = 0; |
78 | | |
79 | | unsigned add_object (hb_vector_buf_t &&obj_data) |
80 | 863k | { |
81 | 863k | unsigned id = 5 + objects.length; /* objects 1-4 are fixed */ |
82 | 863k | hb_pdf_obj_t obj; |
83 | 863k | obj.data = std::move (obj_data); |
84 | 863k | objects.push (std::move (obj)); |
85 | 863k | return id; |
86 | 863k | } |
87 | | |
88 | | /* Add ExtGState for fill opacity, return resource name index. */ |
89 | | unsigned add_extgstate_alpha (float alpha) |
90 | 18.9k | { |
91 | 18.9k | unsigned idx = extgstate_count++; |
92 | 18.9k | hb_vector_buf_t obj; |
93 | 18.9k | obj.append_str ("<< /Type /ExtGState /ca "); |
94 | 18.9k | obj.append_num (alpha, 4); |
95 | 18.9k | obj.append_str (" >>"); |
96 | 18.9k | unsigned obj_id = add_object (std::move (obj)); |
97 | | |
98 | 18.9k | extgstate_dict.append_str ("/GS"); |
99 | 18.9k | extgstate_dict.append_unsigned (idx); |
100 | 18.9k | extgstate_dict.append_c (' '); |
101 | 18.9k | extgstate_dict.append_unsigned (obj_id); |
102 | 18.9k | extgstate_dict.append_str (" 0 R "); |
103 | 18.9k | return idx; |
104 | 18.9k | } |
105 | | |
106 | | /* Add ExtGState for blend mode, return resource name index. */ |
107 | | unsigned add_extgstate_blend (const char *bm) |
108 | 1.95k | { |
109 | 1.95k | unsigned idx = extgstate_count++; |
110 | 1.95k | hb_vector_buf_t obj; |
111 | 1.95k | obj.append_str ("<< /Type /ExtGState /BM /"); |
112 | 1.95k | obj.append_str (bm); |
113 | 1.95k | obj.append_str (" >>"); |
114 | 1.95k | unsigned obj_id = add_object (std::move (obj)); |
115 | | |
116 | 1.95k | extgstate_dict.append_str ("/GS"); |
117 | 1.95k | extgstate_dict.append_unsigned (idx); |
118 | 1.95k | extgstate_dict.append_c (' '); |
119 | 1.95k | extgstate_dict.append_unsigned (obj_id); |
120 | 1.95k | extgstate_dict.append_str (" 0 R "); |
121 | 1.95k | return idx; |
122 | 1.95k | } |
123 | | |
124 | | /* Add ExtGState with an SMask (soft mask) referencing a Form XObject |
125 | | * that paints a DeviceGray shading. Returns the ExtGState resource index. */ |
126 | | unsigned add_extgstate_smask (unsigned alpha_shading_id, |
127 | | float bbox_x, float bbox_y, |
128 | | float bbox_w, float bbox_h, |
129 | | unsigned precision) |
130 | 22.6k | { |
131 | | /* Form XObject: transparency group painting the alpha shading. */ |
132 | 22.6k | hb_vector_buf_t form_stream; |
133 | 22.6k | form_stream.append_str ("/SHa sh\n"); |
134 | | |
135 | 22.6k | hb_vector_buf_t form; |
136 | 22.6k | form.append_str ("<< /Type /XObject /Subtype /Form\n"); |
137 | 22.6k | form.append_str ("/BBox ["); |
138 | 22.6k | form.append_num (bbox_x, precision); |
139 | 22.6k | form.append_c (' '); |
140 | 22.6k | form.append_num (bbox_y, precision); |
141 | 22.6k | form.append_c (' '); |
142 | 22.6k | form.append_num (bbox_x + bbox_w, precision); |
143 | 22.6k | form.append_c (' '); |
144 | 22.6k | form.append_num (bbox_y + bbox_h, precision); |
145 | 22.6k | form.append_str ("]\n/Group << /S /Transparency /CS /DeviceGray >>\n"); |
146 | 22.6k | form.append_str ("/Resources << /Shading << /SHa "); |
147 | 22.6k | form.append_unsigned (alpha_shading_id); |
148 | 22.6k | form.append_str (" 0 R >> >>\n"); |
149 | 22.6k | form.append_str ("/Length "); |
150 | 22.6k | form.append_unsigned (form_stream.length); |
151 | 22.6k | form.append_str (" >>\nstream\n"); |
152 | 22.6k | form.append_len (form_stream.arrayZ, form_stream.length); |
153 | 22.6k | form.append_str ("endstream"); |
154 | 22.6k | unsigned form_id = add_object (std::move (form)); |
155 | | |
156 | | /* ExtGState with luminosity soft mask. */ |
157 | 22.6k | unsigned idx = extgstate_count++; |
158 | 22.6k | hb_vector_buf_t gs; |
159 | 22.6k | gs.append_str ("<< /Type /ExtGState\n"); |
160 | 22.6k | gs.append_str ("/SMask << /Type /Mask /S /Luminosity /G "); |
161 | 22.6k | gs.append_unsigned (form_id); |
162 | 22.6k | gs.append_str (" 0 R >> >>"); |
163 | 22.6k | unsigned gs_id = add_object (std::move (gs)); |
164 | | |
165 | 22.6k | extgstate_dict.append_str ("/GS"); |
166 | 22.6k | extgstate_dict.append_unsigned (idx); |
167 | 22.6k | extgstate_dict.append_c (' '); |
168 | 22.6k | extgstate_dict.append_unsigned (gs_id); |
169 | 22.6k | extgstate_dict.append_str (" 0 R "); |
170 | 22.6k | return idx; |
171 | 22.6k | } |
172 | | |
173 | | /* Add a shading, return resource name index. */ |
174 | | unsigned add_shading (hb_vector_buf_t &&shading_data) |
175 | 60.3k | { |
176 | 60.3k | unsigned obj_id = add_object (std::move (shading_data)); |
177 | 60.3k | return add_shading_by_id (obj_id); |
178 | 60.3k | } |
179 | | |
180 | | /* Register an already-allocated object as a shading resource. */ |
181 | | unsigned add_shading_by_id (unsigned obj_id) |
182 | 62.7k | { |
183 | 62.7k | unsigned idx = shading_count++; |
184 | 62.7k | shading_dict.append_str ("/SH"); |
185 | 62.7k | shading_dict.append_unsigned (idx); |
186 | 62.7k | shading_dict.append_c (' '); |
187 | 62.7k | shading_dict.append_unsigned (obj_id); |
188 | 62.7k | shading_dict.append_str (" 0 R "); |
189 | 62.7k | return idx; |
190 | 62.7k | } |
191 | | |
192 | | /* Add an XObject image, return resource name index. |
193 | | * idat_data/idat_len is the concatenated PNG IDAT payload (zlib data). |
194 | | * colors is 1 (gray), 3 (RGB), or 4 (RGBA); for indexed, pass colors=1. |
195 | | * plte/plte_len is the PLTE chunk data for indexed images (may be null). */ |
196 | | unsigned add_xobject_png_image (const char *idat_data, unsigned idat_len, |
197 | | unsigned width, unsigned height, |
198 | | unsigned colors, bool has_alpha, |
199 | | const uint8_t *plte = nullptr, |
200 | | unsigned plte_len = 0, |
201 | | const uint8_t *trns = nullptr, |
202 | | unsigned trns_len = 0) |
203 | 40 | { |
204 | 40 | unsigned idx = xobject_count++; |
205 | 40 | hb_vector_buf_t obj; |
206 | 40 | obj.append_str ("<< /Type /XObject /Subtype /Image\n"); |
207 | 40 | obj.append_str ("/Width "); |
208 | 40 | obj.append_unsigned (width); |
209 | 40 | obj.append_str (" /Height "); |
210 | 40 | obj.append_unsigned (height); |
211 | 40 | obj.append_str ("\n/BitsPerComponent 8\n"); |
212 | | |
213 | 40 | if (plte && plte_len >= 3) |
214 | 38 | { |
215 | | /* Indexed color: /ColorSpace [/Indexed /DeviceRGB N <hex palette>] */ |
216 | 38 | unsigned n_entries = plte_len / 3; |
217 | 38 | obj.append_str ("/ColorSpace [/Indexed /DeviceRGB "); |
218 | 38 | obj.append_unsigned (n_entries - 1); |
219 | 38 | obj.append_str (" <"); |
220 | 9.86k | for (unsigned i = 0; i < n_entries * 3; i++) |
221 | 9.82k | { |
222 | 9.82k | obj.append_c ("0123456789ABCDEF"[plte[i] >> 4]); |
223 | 9.82k | obj.append_c ("0123456789ABCDEF"[plte[i] & 0xF]); |
224 | 9.82k | } |
225 | 38 | obj.append_str (">]\n"); |
226 | 38 | } |
227 | 2 | else |
228 | 2 | { |
229 | 2 | obj.append_str ("/ColorSpace "); |
230 | 2 | unsigned color_channels = has_alpha ? colors - 1 : colors; |
231 | 2 | obj.append_str (color_channels == 1 ? "/DeviceGray" : "/DeviceRGB"); |
232 | 2 | obj.append_c ('\n'); |
233 | 2 | } |
234 | | |
235 | | /* Build SMask for indexed images with tRNS transparency. */ |
236 | 40 | unsigned smask_id = 0; |
237 | 40 | if (plte && trns && trns_len) |
238 | 38 | { |
239 | 38 | hb_vector_buf_t smask_stream; |
240 | 38 | if (hb_pdf_build_indexed_smask (&smask_stream, idat_data, idat_len, |
241 | 38 | width, height, trns, trns_len)) |
242 | 25 | { |
243 | 25 | hb_vector_buf_t smask_obj; |
244 | 25 | smask_obj.append_str ("<< /Type /XObject /Subtype /Image\n"); |
245 | 25 | smask_obj.append_str ("/Width "); |
246 | 25 | smask_obj.append_unsigned (width); |
247 | 25 | smask_obj.append_str (" /Height "); |
248 | 25 | smask_obj.append_unsigned (height); |
249 | 25 | smask_obj.append_str ("\n/ColorSpace /DeviceGray /BitsPerComponent 8\n"); |
250 | 25 | smask_obj.append_str ("/Length "); |
251 | 25 | smask_obj.append_unsigned (smask_stream.length); |
252 | 25 | smask_obj.append_str (" >>\nstream\n"); |
253 | 25 | smask_obj.append_len (smask_stream.arrayZ, smask_stream.length); |
254 | 25 | smask_obj.append_str ("\nendstream"); |
255 | 25 | smask_id = add_object (std::move (smask_obj)); |
256 | 25 | } |
257 | 38 | } |
258 | | |
259 | 40 | if (smask_id) |
260 | 25 | { |
261 | 25 | obj.append_str ("/SMask "); |
262 | 25 | obj.append_unsigned (smask_id); |
263 | 25 | obj.append_str (" 0 R\n"); |
264 | 25 | } |
265 | | |
266 | 40 | obj.append_str ("/Filter /FlateDecode\n"); |
267 | 40 | obj.append_str ("/DecodeParms << /Predictor 15 /Colors "); |
268 | 40 | obj.append_unsigned (colors); |
269 | 40 | obj.append_str (" /BitsPerComponent 8 /Columns "); |
270 | 40 | obj.append_unsigned (width); |
271 | 40 | obj.append_str (" >>\n"); |
272 | 40 | obj.append_str ("/Length "); |
273 | 40 | obj.append_unsigned (idat_len); |
274 | 40 | obj.append_str (" >>\nstream\n"); |
275 | 40 | obj.append_len (idat_data, idat_len); |
276 | 40 | obj.append_str ("\nendstream"); |
277 | | |
278 | 40 | (void) has_alpha; |
279 | | |
280 | 40 | unsigned obj_id = add_object (std::move (obj)); |
281 | | |
282 | 40 | xobject_dict.append_str ("/Im"); |
283 | 40 | xobject_dict.append_unsigned (idx); |
284 | 40 | xobject_dict.append_c (' '); |
285 | 40 | xobject_dict.append_unsigned (obj_id); |
286 | 40 | xobject_dict.append_str (" 0 R "); |
287 | 40 | return idx; |
288 | 40 | } |
289 | | }; |
290 | | |
291 | | /* Store resources pointer in the paint struct's defs buffer |
292 | | * (repurposed — defs is unused for PDF). */ |
293 | | static hb_pdf_resources_t * |
294 | | hb_pdf_get_resources (hb_vector_paint_t *paint) |
295 | 102k | { |
296 | 102k | if (!paint->defs.length) |
297 | 12.2k | { |
298 | | /* First call: allocate and store. */ |
299 | 12.2k | auto *res = (hb_pdf_resources_t *) hb_calloc (1, sizeof (hb_pdf_resources_t)); |
300 | 12.2k | if (unlikely (!res)) return nullptr; |
301 | 11.7k | new (res) hb_pdf_resources_t (); |
302 | 11.7k | if (unlikely (!paint->defs.resize (sizeof (void *)))) |
303 | 4.85k | { |
304 | 4.85k | res->~hb_pdf_resources_t (); |
305 | 4.85k | hb_free (res); |
306 | 4.85k | return nullptr; |
307 | 4.85k | } |
308 | 6.92k | memcpy (paint->defs.arrayZ, &res, sizeof (void *)); |
309 | 6.92k | } |
310 | 97.1k | hb_pdf_resources_t *res; |
311 | 97.1k | memcpy (&res, paint->defs.arrayZ, sizeof (void *)); |
312 | 97.1k | return res; |
313 | 102k | } |
314 | | |
315 | | void |
316 | | hb_vector_paint_pdf_free_resources (hb_vector_paint_t *paint) |
317 | 34.5k | { |
318 | 34.5k | if (paint->defs.length >= sizeof (void *)) |
319 | 6.92k | { |
320 | 6.92k | hb_pdf_resources_t *res; |
321 | 6.92k | memcpy (&res, paint->defs.arrayZ, sizeof (void *)); |
322 | 6.92k | if (res) |
323 | 6.92k | { |
324 | 6.92k | res->~hb_pdf_resources_t (); |
325 | 6.92k | hb_free (res); |
326 | 6.92k | } |
327 | 6.92k | } |
328 | 34.5k | paint->defs.clear (); |
329 | 34.5k | } |
330 | | |
331 | | |
332 | | /* ---- helpers ---- */ |
333 | | |
334 | | |
335 | | /* Emit a glyph outline as PDF path operators into buf. */ |
336 | | static void |
337 | | hb_pdf_emit_glyph_path (hb_vector_paint_t *paint, |
338 | | hb_font_t *font, |
339 | | hb_codepoint_t glyph, |
340 | | hb_vector_buf_t *buf) |
341 | 505k | { |
342 | 505k | hb_vector_path_sink_t sink = {&paint->path, paint->get_precision (), |
343 | 505k | paint->x_scale_factor, paint->y_scale_factor}; |
344 | 505k | paint->path.clear (); |
345 | 505k | hb_font_draw_glyph (font, glyph, |
346 | 505k | hb_vector_pdf_path_draw_funcs_get (), |
347 | 505k | &sink); |
348 | 505k | buf->append_len (paint->path.arrayZ, paint->path.length); |
349 | 505k | paint->path.clear (); |
350 | 505k | } |
351 | | |
352 | | |
353 | | /* ---- paint callbacks ---- */ |
354 | | |
355 | | static void |
356 | | hb_pdf_paint_push_transform (hb_paint_funcs_t *, |
357 | | void *paint_data, |
358 | | float xx, float yx, |
359 | | float xy, float yy, |
360 | | float dx, float dy, |
361 | | void *) |
362 | 817k | { |
363 | 817k | auto *paint = (hb_vector_paint_t *) paint_data; |
364 | 817k | if (unlikely (!paint->ensure_initialized ())) |
365 | 197k | return; |
366 | | |
367 | 619k | auto &body = paint->current_body (); |
368 | 619k | unsigned sprec = body.scale_precision (); |
369 | 619k | body.append_str ("q\n"); |
370 | 619k | body.append_num (xx, sprec); |
371 | 619k | body.append_c (' '); |
372 | 619k | body.append_num (yx, sprec); |
373 | 619k | body.append_c (' '); |
374 | 619k | body.append_num (xy, sprec); |
375 | 619k | body.append_c (' '); |
376 | 619k | body.append_num (yy, sprec); |
377 | 619k | body.append_c (' '); |
378 | 619k | body.append_num (paint->sx (dx)); |
379 | 619k | body.append_c (' '); |
380 | 619k | body.append_num (paint->sy (dy)); |
381 | 619k | body.append_str (" cm\n"); |
382 | 619k | } |
383 | | |
384 | | static void |
385 | | hb_pdf_paint_pop_transform (hb_paint_funcs_t *, |
386 | | void *paint_data, |
387 | | void *) |
388 | 817k | { |
389 | 817k | auto *paint = (hb_vector_paint_t *) paint_data; |
390 | 817k | if (unlikely (!paint->ensure_initialized ())) |
391 | 198k | return; |
392 | 619k | paint->current_body ().append_str ("Q\n"); |
393 | 619k | } |
394 | | |
395 | | static void |
396 | | hb_pdf_paint_push_clip_glyph (hb_paint_funcs_t *, |
397 | | void *paint_data, |
398 | | hb_codepoint_t glyph, |
399 | | hb_font_t *font, |
400 | | void *) |
401 | 602k | { |
402 | 602k | auto *paint = (hb_vector_paint_t *) paint_data; |
403 | 602k | if (unlikely (!paint->ensure_initialized ())) |
404 | 97.7k | return; |
405 | | |
406 | 505k | auto &body = paint->current_body (); |
407 | 505k | body.append_str ("q\n"); |
408 | 505k | hb_pdf_emit_glyph_path (paint, font, glyph, &body); |
409 | 505k | body.append_str ("W n\n"); |
410 | 505k | } |
411 | | |
412 | | static void |
413 | | hb_pdf_paint_push_clip_rectangle (hb_paint_funcs_t *, |
414 | | void *paint_data, |
415 | | float xmin, float ymin, |
416 | | float xmax, float ymax, |
417 | | void *) |
418 | 3.47k | { |
419 | 3.47k | auto *paint = (hb_vector_paint_t *) paint_data; |
420 | 3.47k | if (unlikely (!paint->ensure_initialized ())) |
421 | 571 | return; |
422 | | |
423 | 2.90k | auto &body = paint->current_body (); |
424 | 2.90k | body.append_str ("q\n"); |
425 | 2.90k | body.append_num (paint->sx (xmin)); |
426 | 2.90k | body.append_c (' '); |
427 | 2.90k | body.append_num (paint->sy (ymin)); |
428 | 2.90k | body.append_c (' '); |
429 | 2.90k | body.append_num (paint->sx (xmax - xmin)); |
430 | 2.90k | body.append_c (' '); |
431 | 2.90k | body.append_num (paint->sy (ymax - ymin)); |
432 | 2.90k | body.append_str (" re W n\n"); |
433 | 2.90k | } |
434 | | |
435 | | static hb_draw_funcs_t * |
436 | | hb_pdf_paint_push_clip_path_start (hb_paint_funcs_t *, |
437 | | void *paint_data, |
438 | | void **draw_data, |
439 | | void *) |
440 | 0 | { |
441 | 0 | auto *paint = (hb_vector_paint_t *) paint_data; |
442 | 0 | if (unlikely (!paint->ensure_initialized ())) |
443 | 0 | { |
444 | 0 | *draw_data = nullptr; |
445 | 0 | return nullptr; |
446 | 0 | } |
447 | | |
448 | 0 | auto &body = paint->current_body (); |
449 | 0 | body.append_str ("q\n"); |
450 | | /* Stream path operators straight into the body; end() seals |
451 | | * the path with "W n" to turn it into the clip region. |
452 | | * Coordinates arrive in font-scale; the sink divides by |
453 | | * scale_factor so they land in output space. */ |
454 | 0 | paint->clip_path_sink = {&body, paint->get_precision (), |
455 | 0 | paint->x_scale_factor, |
456 | 0 | paint->y_scale_factor}; |
457 | 0 | *draw_data = &paint->clip_path_sink; |
458 | 0 | return hb_vector_pdf_path_draw_funcs_get (); |
459 | 0 | } |
460 | | |
461 | | static void |
462 | | hb_pdf_paint_push_clip_path_end (hb_paint_funcs_t *, |
463 | | void *paint_data, |
464 | | void *) |
465 | 0 | { |
466 | 0 | auto *paint = (hb_vector_paint_t *) paint_data; |
467 | 0 | if (unlikely (!paint->ensure_initialized ())) |
468 | 0 | return; |
469 | 0 | paint->current_body ().append_str ("W n\n"); |
470 | 0 | } |
471 | | |
472 | | static void |
473 | | hb_pdf_paint_pop_clip (hb_paint_funcs_t *, |
474 | | void *paint_data, |
475 | | void *) |
476 | 606k | { |
477 | 606k | auto *paint = (hb_vector_paint_t *) paint_data; |
478 | 606k | if (unlikely (!paint->ensure_initialized ())) |
479 | 98.5k | return; |
480 | 507k | paint->current_body ().append_str ("Q\n"); |
481 | 507k | } |
482 | | |
483 | | /* Paint a solid color, including alpha via ExtGState. */ |
484 | | static void |
485 | | hb_pdf_paint_solid_color (hb_vector_paint_t *paint, hb_color_t c) |
486 | 424k | { |
487 | 424k | auto &body = paint->current_body (); |
488 | | |
489 | 424k | float r = hb_color_get_red (c) / 255.f; |
490 | 424k | float g = hb_color_get_green (c) / 255.f; |
491 | 424k | float b = hb_color_get_blue (c) / 255.f; |
492 | 424k | float a = hb_color_get_alpha (c) / 255.f; |
493 | | |
494 | 424k | if (a < 1.f / 255.f) |
495 | 180k | return; |
496 | | |
497 | | /* Set alpha via ExtGState if needed. */ |
498 | 244k | if (a < 1.f - 1.f / 512.f) |
499 | 19.7k | { |
500 | 19.7k | auto *res = hb_pdf_get_resources (paint); |
501 | 19.7k | if (res) |
502 | 18.9k | { |
503 | 18.9k | unsigned gs_idx = res->add_extgstate_alpha (a); |
504 | 18.9k | body.append_str ("/GS"); |
505 | 18.9k | body.append_unsigned (gs_idx); |
506 | 18.9k | body.append_str (" gs\n"); |
507 | 18.9k | } |
508 | 19.7k | } |
509 | | |
510 | | /* Set fill color. */ |
511 | 244k | body.append_num (r, 4); |
512 | 244k | body.append_c (' '); |
513 | 244k | body.append_num (g, 4); |
514 | 244k | body.append_c (' '); |
515 | 244k | body.append_num (b, 4); |
516 | 244k | body.append_str (" rg\n"); |
517 | | |
518 | | /* Paint a huge rect (will be clipped). */ |
519 | 244k | body.append_str ("-32767 -32767 65534 65534 re f\n"); |
520 | 244k | } |
521 | | |
522 | | static void |
523 | | hb_pdf_paint_color (hb_paint_funcs_t *, |
524 | | void *paint_data, |
525 | | hb_bool_t, |
526 | | hb_color_t color, |
527 | | void *) |
528 | 491k | { |
529 | 491k | auto *paint = (hb_vector_paint_t *) paint_data; |
530 | 491k | if (unlikely (!paint->ensure_initialized ())) |
531 | 66.8k | return; |
532 | | |
533 | 424k | hb_pdf_paint_solid_color (paint, color); |
534 | 424k | } |
535 | | |
536 | | /* Build an uncompressed alpha mask from indexed PNG IDAT data + tRNS. |
537 | | * Decompresses IDAT, un-filters PNG rows, maps palette indices to alpha. */ |
538 | | static bool |
539 | | hb_pdf_build_indexed_smask (hb_vector_buf_t *out, |
540 | | const char *idat_data, unsigned idat_len, |
541 | | unsigned width, unsigned height, |
542 | | const uint8_t *trns, unsigned trns_len) |
543 | 38 | { |
544 | | #ifndef HAVE_ZLIB |
545 | | (void) out; (void) idat_data; (void) idat_len; |
546 | | (void) width; (void) height; (void) trns; (void) trns_len; |
547 | | return false; |
548 | | #else |
549 | | /* Decompress IDAT (zlib). */ |
550 | 38 | unsigned raw_len = (width + 1) * height; /* 1 filter byte per row + width bytes */ |
551 | 38 | uint8_t *raw = (uint8_t *) hb_malloc (raw_len); |
552 | 38 | if (!raw) return false; |
553 | 38 | HB_SCOPE_GUARD (hb_free (raw)); |
554 | | |
555 | 38 | z_stream stream = {}; |
556 | 38 | stream.next_in = (Bytef *) idat_data; |
557 | 38 | stream.avail_in = idat_len; |
558 | 38 | stream.next_out = (Bytef *) raw; |
559 | 38 | stream.avail_out = raw_len; |
560 | | |
561 | 38 | if (inflateInit (&stream) != Z_OK) |
562 | 0 | return false; |
563 | 38 | int status = inflate (&stream, Z_FINISH); |
564 | 38 | unsigned long total_out = stream.total_out; |
565 | 38 | inflateEnd (&stream); |
566 | 38 | if (status != Z_STREAM_END || total_out != raw_len) |
567 | 13 | return false; |
568 | | |
569 | | /* Un-filter and map to alpha. */ |
570 | 25 | if (!out->resize (width * height)) |
571 | 0 | return false; |
572 | | |
573 | 25 | uint8_t *unfiltered = (uint8_t *) hb_malloc (width); |
574 | 25 | if (!unfiltered) |
575 | 0 | return false; |
576 | 25 | HB_SCOPE_GUARD (hb_free (unfiltered)); |
577 | | |
578 | 25 | uint8_t *prev_unfiltered = (uint8_t *) hb_calloc (width, 1); |
579 | 25 | if (!prev_unfiltered) |
580 | 0 | return false; |
581 | 25 | HB_SCOPE_GUARD (hb_free (prev_unfiltered)); |
582 | | |
583 | 3.22k | for (unsigned y = 0; y < height; y++) |
584 | 3.20k | { |
585 | 3.20k | uint8_t *row = raw + y * (width + 1); |
586 | 3.20k | uint8_t filter = row[0]; |
587 | 3.20k | uint8_t *pixels = row + 1; |
588 | | |
589 | 438k | for (unsigned x = 0; x < width; x++) |
590 | 435k | { |
591 | 435k | uint8_t a = (x > 0) ? unfiltered[x - 1] : 0; |
592 | 435k | uint8_t b = prev_unfiltered[x]; |
593 | 435k | uint8_t c = (x > 0) ? prev_unfiltered[x - 1] : 0; |
594 | 435k | uint8_t val = pixels[x]; |
595 | | |
596 | 435k | switch (filter) |
597 | 435k | { |
598 | 402k | case 0: unfiltered[x] = val; break; /* None */ |
599 | 2.44k | case 1: unfiltered[x] = val + a; break; /* Sub */ |
600 | 22.0k | case 2: unfiltered[x] = val + b; break; /* Up */ |
601 | 0 | case 3: unfiltered[x] = val + (uint8_t) (((unsigned) a + b) / 2); break; /* Average */ |
602 | 8.16k | case 4: /* Paeth */ |
603 | 8.16k | { |
604 | 8.16k | int p = (int) a + (int) b - (int) c; |
605 | 8.16k | int pa = abs (p - (int) a); |
606 | 8.16k | int pb = abs (p - (int) b); |
607 | 8.16k | int pc = abs (p - (int) c); |
608 | 8.16k | uint8_t pr = (pa <= pb && pa <= pc) ? a : (pb <= pc) ? b : c; |
609 | 8.16k | unfiltered[x] = val + pr; |
610 | 8.16k | break; |
611 | 0 | } |
612 | 0 | default: unfiltered[x] = val; break; |
613 | 435k | } |
614 | | |
615 | | /* Map palette index to alpha. */ |
616 | 435k | uint8_t idx = unfiltered[x]; |
617 | 435k | out->arrayZ[y * width + x] = (idx < trns_len) ? trns[idx] : 0xFF; |
618 | 435k | } |
619 | | |
620 | | /* Swap buffers. */ |
621 | 3.20k | uint8_t *tmp = prev_unfiltered; |
622 | 3.20k | prev_unfiltered = unfiltered; |
623 | 3.20k | unfiltered = tmp; |
624 | 3.20k | } |
625 | | |
626 | 25 | return true; |
627 | 25 | #endif |
628 | 25 | } |
629 | | |
630 | | /* Read a big-endian uint32 from a byte pointer. */ |
631 | | static inline uint32_t |
632 | | hb_pdf_png_u32 (const uint8_t *p) |
633 | 560 | { |
634 | 560 | return ((uint32_t) p[0] << 24) | ((uint32_t) p[1] << 16) | |
635 | 560 | ((uint32_t) p[2] << 8) | (uint32_t) p[3]; |
636 | 560 | } |
637 | | |
638 | | static hb_bool_t |
639 | | hb_pdf_paint_image (hb_paint_funcs_t *, |
640 | | void *paint_data, |
641 | | hb_blob_t *image, |
642 | | unsigned width, |
643 | | unsigned height, |
644 | | hb_tag_t format, |
645 | | float slant HB_UNUSED, |
646 | | hb_glyph_extents_t *extents, |
647 | | void *) |
648 | 7.54k | { |
649 | 7.54k | if (format != HB_TAG ('p','n','g',' ')) |
650 | 7.45k | return false; |
651 | 88 | if (!extents || !width || !height) |
652 | 10 | return false; |
653 | | |
654 | 78 | auto *paint = (hb_vector_paint_t *) paint_data; |
655 | 78 | if (unlikely (!paint->ensure_initialized ())) |
656 | 1 | return false; |
657 | 77 | auto *res = hb_pdf_get_resources (paint); |
658 | 77 | if (unlikely (!res)) |
659 | 1 | return false; |
660 | | |
661 | 76 | unsigned len = 0; |
662 | 76 | const uint8_t *data = (const uint8_t *) hb_blob_get_data (image, &len); |
663 | 76 | if (!data || len < 8) |
664 | 6 | return false; |
665 | | |
666 | | /* Verify PNG signature. */ |
667 | 70 | static const uint8_t png_sig[8] = {137, 80, 78, 71, 13, 10, 26, 10}; |
668 | 70 | if (hb_memcmp (data, png_sig, 8) != 0) |
669 | 19 | return false; |
670 | | |
671 | | /* Parse PNG chunks: extract IHDR and concatenate IDAT payloads. */ |
672 | 51 | unsigned png_w = 0, png_h = 0; |
673 | 51 | unsigned colors = 3; /* default RGB */ |
674 | 51 | uint8_t color_type = 2; |
675 | 51 | bool has_alpha = false; |
676 | 51 | const uint8_t *plte_data = nullptr; |
677 | 51 | unsigned plte_len = 0; |
678 | 51 | const uint8_t *trns_data = nullptr; |
679 | 51 | unsigned trns_len = 0; |
680 | 51 | hb_vector_buf_t idat; |
681 | | |
682 | 51 | unsigned pos = 8; |
683 | 229 | while (pos + 12 <= len) |
684 | 229 | { |
685 | 229 | uint32_t chunk_len = hb_pdf_png_u32 (data + pos); |
686 | 229 | uint32_t chunk_type = hb_pdf_png_u32 (data + pos + 4); |
687 | 229 | if (pos + 12 + chunk_len > len) |
688 | 20 | break; |
689 | 209 | const uint8_t *chunk_data = data + pos + 8; |
690 | | |
691 | 209 | if (chunk_type == 0x49484452u) /* IHDR */ |
692 | 51 | { |
693 | 51 | if (chunk_len < 13) return false; |
694 | 51 | png_w = hb_pdf_png_u32 (chunk_data); |
695 | 51 | png_h = hb_pdf_png_u32 (chunk_data + 4); |
696 | 51 | uint8_t bit_depth = chunk_data[8]; |
697 | 51 | color_type = chunk_data[9]; |
698 | 51 | if (bit_depth != 8) return false; /* only 8-bit supported */ |
699 | | |
700 | 46 | switch (color_type) |
701 | 46 | { |
702 | 0 | case 0: colors = 1; has_alpha = false; break; /* Grayscale */ |
703 | 0 | case 2: colors = 3; has_alpha = false; break; /* RGB */ |
704 | 44 | case 3: colors = 1; has_alpha = false; break; /* Indexed */ |
705 | 0 | case 4: colors = 2; has_alpha = true; break; /* Gray+Alpha */ |
706 | 2 | case 6: colors = 4; has_alpha = true; break; /* RGBA */ |
707 | 0 | default: return false; |
708 | 46 | } |
709 | 46 | } |
710 | 158 | else if (chunk_type == 0x504C5445u) /* PLTE */ |
711 | 38 | { |
712 | 38 | plte_data = chunk_data; |
713 | 38 | plte_len = chunk_len; |
714 | 38 | } |
715 | 120 | else if (chunk_type == 0x74524E53u) /* tRNS */ |
716 | 38 | { |
717 | 38 | trns_data = chunk_data; |
718 | 38 | trns_len = chunk_len; |
719 | 38 | } |
720 | 82 | else if (chunk_type == 0x49444154u) /* IDAT */ |
721 | 40 | { |
722 | 40 | idat.append_len ((const char *) chunk_data, chunk_len); |
723 | 40 | } |
724 | 42 | else if (chunk_type == 0x49454E44u) /* IEND */ |
725 | 26 | break; |
726 | | |
727 | 178 | pos += 12 + chunk_len; |
728 | 178 | } |
729 | | |
730 | 46 | if (!png_w || !png_h || !idat.length) |
731 | 6 | return false; |
732 | | |
733 | 40 | unsigned im_idx = res->add_xobject_png_image (idat.arrayZ, idat.length, |
734 | 40 | png_w, png_h, |
735 | 40 | colors, has_alpha, |
736 | 40 | plte_data, plte_len, |
737 | 40 | trns_data, trns_len); |
738 | | |
739 | | /* Emit: save state, set CTM to map image (0,0)-(1,1) to extents, paint. */ |
740 | 40 | auto &body = paint->current_body (); |
741 | 40 | body.append_str ("q\n"); |
742 | | |
743 | | /* Image space: (0,0) at bottom-left, (1,1) at top-right. |
744 | | * We need to map to extents (x_bearing, y_bearing+height) .. (x_bearing+width, y_bearing). |
745 | | * Since font coords are Y-up like PDF, y_bearing is the top. */ |
746 | 40 | float ix = (float) extents->x_bearing; |
747 | 40 | float iy = (float) extents->y_bearing + (float) extents->height; |
748 | 40 | float iw = (float) extents->width; |
749 | 40 | float ih = (float) -extents->height; /* negative because image Y goes up but height is negative in extents */ |
750 | | |
751 | 40 | body.append_num (paint->sx (iw)); |
752 | 40 | body.append_str (" 0 0 "); |
753 | 40 | body.append_num (paint->sy (ih)); |
754 | 40 | body.append_c (' '); |
755 | 40 | body.append_num (paint->sx (ix)); |
756 | 40 | body.append_c (' '); |
757 | 40 | body.append_num (paint->sy (iy)); |
758 | 40 | body.append_str (" cm\n"); |
759 | | |
760 | 40 | body.append_str ("/Im"); |
761 | 40 | body.append_unsigned (im_idx); |
762 | 40 | body.append_str (" Do\nQ\n"); |
763 | | |
764 | 40 | return true; |
765 | 46 | } |
766 | | |
767 | | /* ---- Gradient helpers ---- */ |
768 | | |
769 | | static bool |
770 | | hb_pdf_gradient_needs_alpha (hb_array_t<const hb_color_stop_t> stops) |
771 | 63.0k | { |
772 | 63.0k | for (const auto &s : stops) |
773 | 135k | if (hb_color_get_alpha (s.color) != 255) |
774 | 22.9k | return true; |
775 | 40.1k | return false; |
776 | 63.0k | } |
777 | | |
778 | | /* Build a PDF Type 2 (exponential interpolation) function for |
779 | | * a single alpha stop pair (DeviceGray, scalar output). */ |
780 | | static void |
781 | | hb_pdf_build_alpha_interpolation_function (hb_vector_buf_t *obj, |
782 | | float a0, float a1) |
783 | 307k | { |
784 | 307k | obj->append_str ("<< /FunctionType 2 /Domain [0 1] /N 1\n"); |
785 | 307k | obj->append_str ("/C0 ["); |
786 | 307k | obj->append_num (a0, 4); |
787 | 307k | obj->append_str ("]\n/C1 ["); |
788 | 307k | obj->append_num (a1, 4); |
789 | 307k | obj->append_str ("] >>"); |
790 | 307k | } |
791 | | |
792 | | /* Build a stitching function (Type 3) for the alpha channel of |
793 | | * pre-populated paint->color_stops_scratch (already sorted+normalized). */ |
794 | | static unsigned |
795 | | hb_pdf_build_alpha_gradient_function_from_stops (hb_pdf_resources_t *res, |
796 | | hb_vector_paint_t *paint) |
797 | 20.9k | { |
798 | 20.9k | unsigned count = paint->color_stops_scratch.length; |
799 | | |
800 | 20.9k | if (count < 2) |
801 | 131 | { |
802 | 131 | float a = count ? hb_color_get_alpha (paint->color_stops_scratch.arrayZ[0].color) / 255.f : 1.f; |
803 | 131 | hb_vector_buf_t obj; |
804 | 131 | hb_pdf_build_alpha_interpolation_function (&obj, a, a); |
805 | 131 | return res->add_object (std::move (obj)); |
806 | 131 | } |
807 | | |
808 | 20.8k | if (count == 2) |
809 | 10.3k | { |
810 | 10.3k | hb_vector_buf_t obj; |
811 | 10.3k | hb_pdf_build_alpha_interpolation_function (&obj, |
812 | 10.3k | hb_color_get_alpha (paint->color_stops_scratch.arrayZ[0].color) / 255.f, |
813 | 10.3k | hb_color_get_alpha (paint->color_stops_scratch.arrayZ[1].color) / 255.f); |
814 | 10.3k | return res->add_object (std::move (obj)); |
815 | 10.3k | } |
816 | | |
817 | 10.4k | hb_vector_t<unsigned> sub_func_ids; |
818 | 307k | for (unsigned i = 0; i + 1 < count; i++) |
819 | 297k | { |
820 | 297k | hb_vector_buf_t sub; |
821 | 297k | hb_pdf_build_alpha_interpolation_function (&sub, |
822 | 297k | hb_color_get_alpha (paint->color_stops_scratch.arrayZ[i].color) / 255.f, |
823 | 297k | hb_color_get_alpha (paint->color_stops_scratch.arrayZ[i + 1].color) / 255.f); |
824 | 297k | sub_func_ids.push (res->add_object (std::move (sub))); |
825 | 297k | } |
826 | | |
827 | 10.4k | hb_vector_buf_t obj; |
828 | 10.4k | obj.append_str ("<< /FunctionType 3 /Domain [0 1]\n"); |
829 | 10.4k | obj.append_str ("/Functions ["); |
830 | 259k | for (unsigned i = 0; i < sub_func_ids.length; i++) |
831 | 248k | { |
832 | 248k | if (i) obj.append_c (' '); |
833 | 248k | obj.append_unsigned (sub_func_ids.arrayZ[i]); |
834 | 248k | obj.append_str (" 0 R"); |
835 | 248k | } |
836 | 10.4k | obj.append_str ("]\n/Bounds ["); |
837 | 297k | for (unsigned i = 1; i + 1 < count; i++) |
838 | 286k | { |
839 | 286k | if (i > 1) obj.append_c (' '); |
840 | 286k | obj.append_num (paint->color_stops_scratch.arrayZ[i].offset, 4); |
841 | 286k | } |
842 | 10.4k | obj.append_str ("]\n/Encode ["); |
843 | 307k | for (unsigned i = 0; i + 1 < count; i++) |
844 | 297k | { |
845 | 297k | if (i) obj.append_c (' '); |
846 | 297k | obj.append_str ("0 1"); |
847 | 297k | } |
848 | 10.4k | obj.append_str ("] >>"); |
849 | 10.4k | return res->add_object (std::move (obj)); |
850 | 20.8k | } |
851 | | |
852 | | /* Build a PDF Type 2 (exponential interpolation) function for |
853 | | * a single color stop pair. */ |
854 | | static void |
855 | | hb_pdf_build_interpolation_function (hb_vector_buf_t *obj, |
856 | | hb_color_t c0, hb_color_t c1) |
857 | 372k | { |
858 | 372k | obj->append_str ("<< /FunctionType 2 /Domain [0 1] /N 1\n"); |
859 | 372k | obj->append_str ("/C0 ["); |
860 | 372k | obj->append_num (hb_color_get_red (c0) / 255.f, 4); |
861 | 372k | obj->append_c (' '); |
862 | 372k | obj->append_num (hb_color_get_green (c0) / 255.f, 4); |
863 | 372k | obj->append_c (' '); |
864 | 372k | obj->append_num (hb_color_get_blue (c0) / 255.f, 4); |
865 | 372k | obj->append_str ("]\n/C1 ["); |
866 | 372k | obj->append_num (hb_color_get_red (c1) / 255.f, 4); |
867 | 372k | obj->append_c (' '); |
868 | 372k | obj->append_num (hb_color_get_green (c1) / 255.f, 4); |
869 | 372k | obj->append_c (' '); |
870 | 372k | obj->append_num (hb_color_get_blue (c1) / 255.f, 4); |
871 | 372k | obj->append_str ("] >>"); |
872 | 372k | } |
873 | | |
874 | | /* Build a stitching function (Type 3) from pre-populated |
875 | | * paint->color_stops_scratch (already sorted+normalized). */ |
876 | | static unsigned |
877 | | hb_pdf_build_gradient_function_from_stops (hb_pdf_resources_t *res, |
878 | | hb_vector_paint_t *paint) |
879 | 60.3k | { |
880 | 60.3k | unsigned count = paint->color_stops_scratch.length; |
881 | | |
882 | 60.3k | if (count < 2) |
883 | 139 | { |
884 | | /* Single stop: constant function. */ |
885 | 139 | hb_color_t c = count ? paint->color_stops_scratch.arrayZ[0].color |
886 | 139 | : HB_COLOR (0, 0, 0, 255); |
887 | 139 | hb_vector_buf_t obj; |
888 | 139 | hb_pdf_build_interpolation_function (&obj, c, c); |
889 | 139 | return res->add_object (std::move (obj)); |
890 | 139 | } |
891 | | |
892 | | /* Sort by offset. */ |
893 | 60.2k | paint->color_stops_scratch.as_array ().qsort ( |
894 | 60.2k | [] (const hb_color_stop_t &a, const hb_color_stop_t &b) |
895 | 1.59M | { return (a.offset > b.offset) - (a.offset < b.offset); }); |
896 | | |
897 | 60.2k | if (count == 2) |
898 | 39.0k | { |
899 | | /* Two stops: single interpolation function. */ |
900 | 39.0k | hb_vector_buf_t obj; |
901 | 39.0k | hb_pdf_build_interpolation_function (&obj, |
902 | 39.0k | paint->color_stops_scratch.arrayZ[0].color, |
903 | 39.0k | paint->color_stops_scratch.arrayZ[1].color); |
904 | 39.0k | return res->add_object (std::move (obj)); |
905 | 39.0k | } |
906 | | |
907 | | /* Multiple stops: create sub-functions and stitch. */ |
908 | 21.1k | hb_vector_t<unsigned> sub_func_ids; |
909 | 354k | for (unsigned i = 0; i + 1 < count; i++) |
910 | 333k | { |
911 | 333k | hb_vector_buf_t sub; |
912 | 333k | hb_pdf_build_interpolation_function (&sub, |
913 | 333k | paint->color_stops_scratch.arrayZ[i].color, |
914 | 333k | paint->color_stops_scratch.arrayZ[i + 1].color); |
915 | 333k | sub_func_ids.push (res->add_object (std::move (sub))); |
916 | 333k | } |
917 | | |
918 | | /* Stitching function (Type 3). */ |
919 | 21.1k | hb_vector_buf_t obj; |
920 | 21.1k | obj.append_str ("<< /FunctionType 3 /Domain [0 1]\n"); |
921 | | |
922 | | /* Functions array. */ |
923 | 21.1k | obj.append_str ("/Functions ["); |
924 | 328k | for (unsigned i = 0; i < sub_func_ids.length; i++) |
925 | 307k | { |
926 | 307k | if (i) obj.append_c (' '); |
927 | 307k | obj.append_unsigned (sub_func_ids.arrayZ[i]); |
928 | 307k | obj.append_str (" 0 R"); |
929 | 307k | } |
930 | 21.1k | obj.append_str ("]\n"); |
931 | | |
932 | | /* Bounds. */ |
933 | 21.1k | obj.append_str ("/Bounds ["); |
934 | 333k | for (unsigned i = 1; i + 1 < count; i++) |
935 | 312k | { |
936 | 312k | if (i > 1) obj.append_c (' '); |
937 | 312k | obj.append_num (paint->color_stops_scratch.arrayZ[i].offset, 4); |
938 | 312k | } |
939 | 21.1k | obj.append_str ("]\n"); |
940 | | |
941 | | /* Encode array. */ |
942 | 21.1k | obj.append_str ("/Encode ["); |
943 | 354k | for (unsigned i = 0; i + 1 < count; i++) |
944 | 333k | { |
945 | 333k | if (i) obj.append_c (' '); |
946 | 333k | obj.append_str ("0 1"); |
947 | 333k | } |
948 | 21.1k | obj.append_str ("] >>"); |
949 | | |
950 | 21.1k | return res->add_object (std::move (obj)); |
951 | 60.2k | } |
952 | | |
953 | | static void |
954 | | hb_pdf_paint_linear_gradient (hb_paint_funcs_t *, |
955 | | void *paint_data, |
956 | | hb_color_line_t *color_line, |
957 | | float x0, float y0, |
958 | | float x1, float y1, |
959 | | float x2 HB_UNUSED, float y2 HB_UNUSED, |
960 | | void *) |
961 | 50.1k | { |
962 | 50.1k | auto *paint = (hb_vector_paint_t *) paint_data; |
963 | 50.1k | if (unlikely (!paint->ensure_initialized ())) |
964 | 14.4k | return; |
965 | 35.7k | auto *res = hb_pdf_get_resources (paint); |
966 | 35.7k | if (unlikely (!res)) |
967 | 1.79k | return; |
968 | | |
969 | | /* Fetch, normalize stops to [0,1], and adjust coordinates. */ |
970 | 33.9k | if (!paint->fetch_color_stops (color_line)) |
971 | 1.41k | return; |
972 | 32.5k | hb_vector_t<hb_color_stop_t> &stops = paint->color_stops_scratch; |
973 | | |
974 | 32.5k | float mn, mx; |
975 | 32.5k | hb_paint_normalize_color_line (stops.arrayZ, stops.length, &mn, &mx); |
976 | 32.5k | float gx0 = x0 + mn * (x1 - x0); |
977 | 32.5k | float gy0 = y0 + mn * (y1 - y0); |
978 | 32.5k | float gx1 = x0 + mx * (x1 - x0); |
979 | 32.5k | float gy1 = y0 + mx * (y1 - y0); |
980 | | |
981 | 32.5k | unsigned func_id = hb_pdf_build_gradient_function_from_stops (res, paint); |
982 | | |
983 | 32.5k | hb_paint_extend_t extend = hb_color_line_get_extend (color_line); |
984 | 32.5k | const char *extend_str = (extend == HB_PAINT_EXTEND_PAD) |
985 | 32.5k | ? "/Extend [true true]\n" : ""; |
986 | | |
987 | | /* Build Type 2 (axial) shading — color only. */ |
988 | 32.5k | hb_vector_buf_t sh; |
989 | 32.5k | sh.append_str ("<< /ShadingType 2 /ColorSpace /DeviceRGB\n"); |
990 | 32.5k | sh.append_str ("/Coords ["); |
991 | 32.5k | sh.append_num (paint->sx (gx0)); |
992 | 32.5k | sh.append_c (' '); |
993 | 32.5k | sh.append_num (paint->sy (gy0)); |
994 | 32.5k | sh.append_c (' '); |
995 | 32.5k | sh.append_num (paint->sx (gx1)); |
996 | 32.5k | sh.append_c (' '); |
997 | 32.5k | sh.append_num (paint->sy (gy1)); |
998 | 32.5k | sh.append_str ("]\n/Function "); |
999 | 32.5k | sh.append_unsigned (func_id); |
1000 | 32.5k | sh.append_str (" 0 R\n"); |
1001 | 32.5k | sh.append_str (extend_str); |
1002 | 32.5k | sh.append_str (">>"); |
1003 | | |
1004 | 32.5k | unsigned sh_idx = res->add_shading (std::move (sh)); |
1005 | | |
1006 | 32.5k | auto &body = paint->current_body (); |
1007 | | |
1008 | 32.5k | bool needs_alpha = hb_pdf_gradient_needs_alpha (stops); |
1009 | 32.5k | if (needs_alpha) |
1010 | 12.2k | { |
1011 | 12.2k | unsigned alpha_func_id = hb_pdf_build_alpha_gradient_function_from_stops (res, paint); |
1012 | | |
1013 | 12.2k | hb_vector_buf_t ash; |
1014 | 12.2k | ash.append_str ("<< /ShadingType 2 /ColorSpace /DeviceGray\n"); |
1015 | 12.2k | ash.append_str ("/Coords ["); |
1016 | 12.2k | ash.append_num (paint->sx (gx0)); |
1017 | 12.2k | ash.append_c (' '); |
1018 | 12.2k | ash.append_num (paint->sy (gy0)); |
1019 | 12.2k | ash.append_c (' '); |
1020 | 12.2k | ash.append_num (paint->sx (gx1)); |
1021 | 12.2k | ash.append_c (' '); |
1022 | 12.2k | ash.append_num (paint->sy (gy1)); |
1023 | 12.2k | ash.append_str ("]\n/Function "); |
1024 | 12.2k | ash.append_unsigned (alpha_func_id); |
1025 | 12.2k | ash.append_str (" 0 R\n"); |
1026 | 12.2k | ash.append_str (extend_str); |
1027 | 12.2k | ash.append_str (">>"); |
1028 | 12.2k | unsigned alpha_sh_id = res->add_object (std::move (ash)); |
1029 | | |
1030 | 12.2k | unsigned gs_idx = res->add_extgstate_smask (alpha_sh_id, |
1031 | 12.2k | paint->sx (gx0), paint->sy (gy0), |
1032 | 12.2k | paint->sx (gx1 - gx0), paint->sy (gy1 - gy0), |
1033 | 12.2k | paint->get_precision ()); |
1034 | 12.2k | body.append_str ("/GS"); |
1035 | 12.2k | body.append_unsigned (gs_idx); |
1036 | 12.2k | body.append_str (" gs\n"); |
1037 | 12.2k | } |
1038 | | |
1039 | 32.5k | body.append_str ("/SH"); |
1040 | 32.5k | body.append_unsigned (sh_idx); |
1041 | 32.5k | body.append_str (" sh\n"); |
1042 | 32.5k | } |
1043 | | |
1044 | | static void |
1045 | | hb_pdf_paint_radial_gradient (hb_paint_funcs_t *, |
1046 | | void *paint_data, |
1047 | | hb_color_line_t *color_line, |
1048 | | float x0, float y0, float r0, |
1049 | | float x1, float y1, float r1, |
1050 | | void *) |
1051 | 47.2k | { |
1052 | 47.2k | auto *paint = (hb_vector_paint_t *) paint_data; |
1053 | 47.2k | if (unlikely (!paint->ensure_initialized ())) |
1054 | 12.9k | return; |
1055 | 34.2k | auto *res = hb_pdf_get_resources (paint); |
1056 | 34.2k | if (unlikely (!res)) |
1057 | 2.27k | return; |
1058 | | |
1059 | | /* Fetch, normalize stops to [0,1], and adjust coordinates. */ |
1060 | 31.9k | if (!paint->fetch_color_stops (color_line)) |
1061 | 4.14k | return; |
1062 | 27.8k | hb_vector_t<hb_color_stop_t> &stops = paint->color_stops_scratch; |
1063 | | |
1064 | 27.8k | float mn, mx; |
1065 | 27.8k | hb_paint_normalize_color_line (stops.arrayZ, stops.length, &mn, &mx); |
1066 | 27.8k | float gx0 = x0 + mn * (x1 - x0); |
1067 | 27.8k | float gy0 = y0 + mn * (y1 - y0); |
1068 | 27.8k | float gr0 = r0 + mn * (r1 - r0); |
1069 | 27.8k | float gx1 = x0 + mx * (x1 - x0); |
1070 | 27.8k | float gy1 = y0 + mx * (y1 - y0); |
1071 | 27.8k | float gr1 = r0 + mx * (r1 - r0); |
1072 | | |
1073 | 27.8k | unsigned func_id = hb_pdf_build_gradient_function_from_stops (res, paint); |
1074 | | |
1075 | 27.8k | hb_paint_extend_t extend = hb_color_line_get_extend (color_line); |
1076 | 27.8k | const char *extend_str = (extend == HB_PAINT_EXTEND_PAD) |
1077 | 27.8k | ? "/Extend [true true]\n" : ""; |
1078 | | |
1079 | | /* Build Type 3 (radial) shading — color only. */ |
1080 | 27.8k | hb_vector_buf_t sh; |
1081 | 27.8k | sh.append_str ("<< /ShadingType 3 /ColorSpace /DeviceRGB\n"); |
1082 | 27.8k | sh.append_str ("/Coords ["); |
1083 | 27.8k | sh.append_num (paint->sx (gx0)); |
1084 | 27.8k | sh.append_c (' '); |
1085 | 27.8k | sh.append_num (paint->sy (gy0)); |
1086 | 27.8k | sh.append_c (' '); |
1087 | 27.8k | sh.append_num (paint->sx (gr0)); |
1088 | 27.8k | sh.append_c (' '); |
1089 | 27.8k | sh.append_num (paint->sx (gx1)); |
1090 | 27.8k | sh.append_c (' '); |
1091 | 27.8k | sh.append_num (paint->sy (gy1)); |
1092 | 27.8k | sh.append_c (' '); |
1093 | 27.8k | sh.append_num (paint->sx (gr1)); |
1094 | 27.8k | sh.append_str ("]\n/Function "); |
1095 | 27.8k | sh.append_unsigned (func_id); |
1096 | 27.8k | sh.append_str (" 0 R\n"); |
1097 | 27.8k | sh.append_str (extend_str); |
1098 | 27.8k | sh.append_str (">>"); |
1099 | | |
1100 | 27.8k | unsigned sh_idx = res->add_shading (std::move (sh)); |
1101 | | |
1102 | 27.8k | auto &body = paint->current_body (); |
1103 | | |
1104 | 27.8k | bool needs_alpha = hb_pdf_gradient_needs_alpha (stops); |
1105 | 27.8k | if (needs_alpha) |
1106 | 8.72k | { |
1107 | 8.72k | unsigned alpha_func_id = hb_pdf_build_alpha_gradient_function_from_stops (res, paint); |
1108 | | |
1109 | 8.72k | hb_vector_buf_t ash; |
1110 | 8.72k | ash.append_str ("<< /ShadingType 3 /ColorSpace /DeviceGray\n"); |
1111 | 8.72k | ash.append_str ("/Coords ["); |
1112 | 8.72k | ash.append_num (paint->sx (gx0)); |
1113 | 8.72k | ash.append_c (' '); |
1114 | 8.72k | ash.append_num (paint->sy (gy0)); |
1115 | 8.72k | ash.append_c (' '); |
1116 | 8.72k | ash.append_num (paint->sx (gr0)); |
1117 | 8.72k | ash.append_c (' '); |
1118 | 8.72k | ash.append_num (paint->sx (gx1)); |
1119 | 8.72k | ash.append_c (' '); |
1120 | 8.72k | ash.append_num (paint->sy (gy1)); |
1121 | 8.72k | ash.append_c (' '); |
1122 | 8.72k | ash.append_num (paint->sx (gr1)); |
1123 | 8.72k | ash.append_str ("]\n/Function "); |
1124 | 8.72k | ash.append_unsigned (alpha_func_id); |
1125 | 8.72k | ash.append_str (" 0 R\n"); |
1126 | 8.72k | ash.append_str (extend_str); |
1127 | 8.72k | ash.append_str (">>"); |
1128 | 8.72k | unsigned alpha_sh_id = res->add_object (std::move (ash)); |
1129 | | |
1130 | | /* BBox: enclosing square of the outer circle. */ |
1131 | 8.72k | float cx = (gr1 >= gr0) ? gx1 : gx0; |
1132 | 8.72k | float cy = (gr1 >= gr0) ? gy1 : gy0; |
1133 | 8.72k | float rr = hb_max (gr0, gr1); |
1134 | 8.72k | unsigned gs_idx = res->add_extgstate_smask (alpha_sh_id, |
1135 | 8.72k | paint->sx (cx - rr), paint->sy (cy - rr), |
1136 | 8.72k | paint->sx (2 * rr), paint->sy (2 * rr), |
1137 | 8.72k | paint->get_precision ()); |
1138 | 8.72k | body.append_str ("/GS"); |
1139 | 8.72k | body.append_unsigned (gs_idx); |
1140 | 8.72k | body.append_str (" gs\n"); |
1141 | 8.72k | } |
1142 | | |
1143 | 27.8k | body.append_str ("/SH"); |
1144 | 27.8k | body.append_unsigned (sh_idx); |
1145 | 27.8k | body.append_str (" sh\n"); |
1146 | 27.8k | } |
1147 | | |
1148 | | |
1149 | | /* Encode a 16-bit big-endian unsigned value into buf. */ |
1150 | | static void |
1151 | | hb_pdf_encode_u16 (hb_vector_buf_t *buf, uint16_t v) |
1152 | 136M | { |
1153 | 136M | char bytes[2] = {(char) (v >> 8), (char) (v & 0xFF)}; |
1154 | 136M | buf->append_len (bytes, 2); |
1155 | 136M | } |
1156 | | |
1157 | | /* Encode a coordinate as 16-bit value relative to Decode range. */ |
1158 | | static void |
1159 | | hb_pdf_encode_coord (hb_vector_buf_t *buf, |
1160 | | float val, float lo, float hi) |
1161 | 136M | { |
1162 | 136M | float t = (val - lo) / (hi - lo); |
1163 | 136M | t = hb_clamp (t, 0.f, 1.f); |
1164 | 136M | hb_pdf_encode_u16 (buf, (uint16_t) (t * 65535.f + 0.5f)); |
1165 | 136M | } |
1166 | | |
1167 | | /* Encode RGB from hb_color_t as 3 bytes. */ |
1168 | | static void |
1169 | | hb_pdf_encode_color_rgb (hb_vector_buf_t *buf, hb_color_t c) |
1170 | 11.3M | { |
1171 | 11.3M | char rgb[3] = {(char) hb_color_get_red (c), |
1172 | 11.3M | (char) hb_color_get_green (c), |
1173 | 11.3M | (char) hb_color_get_blue (c)}; |
1174 | 11.3M | buf->append_len (rgb, 3); |
1175 | 11.3M | } |
1176 | | |
1177 | | /* Encode alpha from hb_color_t as 1 byte (gray). */ |
1178 | | static void |
1179 | | hb_pdf_encode_color_alpha (hb_vector_buf_t *buf, hb_color_t c) |
1180 | 11.3M | { |
1181 | 11.3M | char a = (char) hb_color_get_alpha (c); |
1182 | 11.3M | buf->append_len (&a, 1); |
1183 | 11.3M | } |
1184 | | |
1185 | | /* Encode one Coons patch control point. */ |
1186 | | static void |
1187 | | hb_pdf_encode_point (hb_vector_buf_t *buf, |
1188 | | float x, float y, |
1189 | | float xlo, float xhi, |
1190 | | float ylo, float yhi) |
1191 | 68.2M | { |
1192 | 68.2M | hb_pdf_encode_coord (buf, x, xlo, xhi); |
1193 | 68.2M | hb_pdf_encode_coord (buf, y, ylo, yhi); |
1194 | 68.2M | } |
1195 | | |
1196 | | /* Emit one Coons patch sector into the mesh stream(s). |
1197 | | * Splits large arcs into sub-patches of max 90°. |
1198 | | * If alpha_mesh is non-null, emits a parallel DeviceGray |
1199 | | * patch with the alpha channel. */ |
1200 | | static void |
1201 | | hb_pdf_add_sweep_patch (hb_vector_buf_t *mesh, |
1202 | | hb_vector_buf_t *alpha_mesh, |
1203 | | float cx, float cy, |
1204 | | float xlo, float xhi, float ylo, float yhi, |
1205 | | float a0, hb_color_t c0_in, |
1206 | | float a1, hb_color_t c1_in) |
1207 | 2.84M | { |
1208 | 2.84M | const float R = 32767.f; |
1209 | 2.84M | const float eps = 0.5f; |
1210 | 2.84M | const float MAX_SECTOR = (float) M_PI / 2.f; |
1211 | | |
1212 | 2.84M | int num_splits = (int) ceilf (fabsf (a1 - a0) / MAX_SECTOR); |
1213 | 2.84M | if (num_splits < 1) num_splits = 1; |
1214 | | |
1215 | 5.69M | for (int s = 0; s < num_splits; s++) |
1216 | 2.84M | { |
1217 | 2.84M | float k0 = (float) s / num_splits; |
1218 | 2.84M | float k1 = (float) (s + 1) / num_splits; |
1219 | 2.84M | float sa0 = a0 + k0 * (a1 - a0); |
1220 | 2.84M | float sa1 = a0 + k1 * (a1 - a0); |
1221 | 2.84M | hb_color_t sc0 = hb_color_lerp (c0_in, c1_in, k0); |
1222 | 2.84M | hb_color_t sc1 = hb_color_lerp (c0_in, c1_in, k1); |
1223 | | |
1224 | 2.84M | float da = sa1 - sa0; |
1225 | 2.84M | float kappa = (4.f / 3.f) * tanf (da / 4.f); |
1226 | | |
1227 | 2.84M | float cos0 = cosf (sa0), sin0 = sinf (sa0); |
1228 | 2.84M | float cos1 = cosf (sa1), sin1 = sinf (sa1); |
1229 | | |
1230 | 2.84M | float p0x = cx + eps * cos0, p0y = cy + eps * sin0; |
1231 | 2.84M | float p3x = cx + R * cos0, p3y = cy + R * sin0; |
1232 | 2.84M | float p6x = cx + R * cos1, p6y = cy + R * sin1; |
1233 | 2.84M | float p9x = cx + eps * cos1, p9y = cy + eps * sin1; |
1234 | | |
1235 | | /* Edge 1: p0→p3, radial straight line. */ |
1236 | 2.84M | float e1_1x = p0x + (p3x - p0x) / 3.f; |
1237 | 2.84M | float e1_1y = p0y + (p3y - p0y) / 3.f; |
1238 | 2.84M | float e1_2x = p0x + 2.f * (p3x - p0x) / 3.f; |
1239 | 2.84M | float e1_2y = p0y + 2.f * (p3y - p0y) / 3.f; |
1240 | | |
1241 | | /* Edge 2: p3→p6, outer arc. */ |
1242 | 2.84M | float e2_1x = p3x + kappa * R * (-sin0); |
1243 | 2.84M | float e2_1y = p3y + kappa * R * ( cos0); |
1244 | 2.84M | float e2_2x = p6x - kappa * R * (-sin1); |
1245 | 2.84M | float e2_2y = p6y - kappa * R * ( cos1); |
1246 | | |
1247 | | /* Edge 3: p6→p9, radial straight line. */ |
1248 | 2.84M | float e3_1x = p6x + (p9x - p6x) / 3.f; |
1249 | 2.84M | float e3_1y = p6y + (p9y - p6y) / 3.f; |
1250 | 2.84M | float e3_2x = p6x + 2.f * (p9x - p6x) / 3.f; |
1251 | 2.84M | float e3_2y = p6y + 2.f * (p9y - p6y) / 3.f; |
1252 | | |
1253 | | /* Edge 4: p9→p0, inner arc. */ |
1254 | 2.84M | float e4_1x = p9x + kappa * eps * (-sin1); |
1255 | 2.84M | float e4_1y = p9y + kappa * eps * ( cos1); |
1256 | 2.84M | float e4_2x = p0x - kappa * eps * (-sin0); |
1257 | 2.84M | float e4_2y = p0y - kappa * eps * ( cos0); |
1258 | | |
1259 | 2.84M | mesh->append_c ('\0'); /* flag = 0, new patch */ |
1260 | | |
1261 | 2.84M | hb_pdf_encode_point (mesh, p0x, p0y, xlo, xhi, ylo, yhi); |
1262 | 2.84M | hb_pdf_encode_point (mesh, e1_1x, e1_1y, xlo, xhi, ylo, yhi); |
1263 | 2.84M | hb_pdf_encode_point (mesh, e1_2x, e1_2y, xlo, xhi, ylo, yhi); |
1264 | 2.84M | hb_pdf_encode_point (mesh, p3x, p3y, xlo, xhi, ylo, yhi); |
1265 | 2.84M | hb_pdf_encode_point (mesh, e2_1x, e2_1y, xlo, xhi, ylo, yhi); |
1266 | 2.84M | hb_pdf_encode_point (mesh, e2_2x, e2_2y, xlo, xhi, ylo, yhi); |
1267 | 2.84M | hb_pdf_encode_point (mesh, p6x, p6y, xlo, xhi, ylo, yhi); |
1268 | 2.84M | hb_pdf_encode_point (mesh, e3_1x, e3_1y, xlo, xhi, ylo, yhi); |
1269 | 2.84M | hb_pdf_encode_point (mesh, e3_2x, e3_2y, xlo, xhi, ylo, yhi); |
1270 | 2.84M | hb_pdf_encode_point (mesh, p9x, p9y, xlo, xhi, ylo, yhi); |
1271 | 2.84M | hb_pdf_encode_point (mesh, e4_1x, e4_1y, xlo, xhi, ylo, yhi); |
1272 | 2.84M | hb_pdf_encode_point (mesh, e4_2x, e4_2y, xlo, xhi, ylo, yhi); |
1273 | | |
1274 | 2.84M | hb_pdf_encode_color_rgb (mesh, sc0); /* inner start */ |
1275 | 2.84M | hb_pdf_encode_color_rgb (mesh, sc0); /* outer start */ |
1276 | 2.84M | hb_pdf_encode_color_rgb (mesh, sc1); /* outer end */ |
1277 | 2.84M | hb_pdf_encode_color_rgb (mesh, sc1); /* inner end */ |
1278 | | |
1279 | 2.84M | if (alpha_mesh) |
1280 | 2.84M | { |
1281 | 2.84M | alpha_mesh->append_c ('\0'); |
1282 | | |
1283 | 2.84M | hb_pdf_encode_point (alpha_mesh, p0x, p0y, xlo, xhi, ylo, yhi); |
1284 | 2.84M | hb_pdf_encode_point (alpha_mesh, e1_1x, e1_1y, xlo, xhi, ylo, yhi); |
1285 | 2.84M | hb_pdf_encode_point (alpha_mesh, e1_2x, e1_2y, xlo, xhi, ylo, yhi); |
1286 | 2.84M | hb_pdf_encode_point (alpha_mesh, p3x, p3y, xlo, xhi, ylo, yhi); |
1287 | 2.84M | hb_pdf_encode_point (alpha_mesh, e2_1x, e2_1y, xlo, xhi, ylo, yhi); |
1288 | 2.84M | hb_pdf_encode_point (alpha_mesh, e2_2x, e2_2y, xlo, xhi, ylo, yhi); |
1289 | 2.84M | hb_pdf_encode_point (alpha_mesh, p6x, p6y, xlo, xhi, ylo, yhi); |
1290 | 2.84M | hb_pdf_encode_point (alpha_mesh, e3_1x, e3_1y, xlo, xhi, ylo, yhi); |
1291 | 2.84M | hb_pdf_encode_point (alpha_mesh, e3_2x, e3_2y, xlo, xhi, ylo, yhi); |
1292 | 2.84M | hb_pdf_encode_point (alpha_mesh, p9x, p9y, xlo, xhi, ylo, yhi); |
1293 | 2.84M | hb_pdf_encode_point (alpha_mesh, e4_1x, e4_1y, xlo, xhi, ylo, yhi); |
1294 | 2.84M | hb_pdf_encode_point (alpha_mesh, e4_2x, e4_2y, xlo, xhi, ylo, yhi); |
1295 | | |
1296 | 2.84M | hb_pdf_encode_color_alpha (alpha_mesh, sc0); |
1297 | 2.84M | hb_pdf_encode_color_alpha (alpha_mesh, sc0); |
1298 | 2.84M | hb_pdf_encode_color_alpha (alpha_mesh, sc1); |
1299 | 2.84M | hb_pdf_encode_color_alpha (alpha_mesh, sc1); |
1300 | 2.84M | } |
1301 | 2.84M | } |
1302 | 2.84M | } |
1303 | | |
1304 | | /* Callback context + trampoline for hb_paint_sweep_gradient_tiles. */ |
1305 | | struct hb_pdf_sweep_ctx_t { |
1306 | | hb_vector_buf_t *mesh; |
1307 | | hb_vector_buf_t *alpha_mesh; |
1308 | | float cx, cy, xlo, xhi, ylo, yhi; |
1309 | | }; |
1310 | | |
1311 | | static void |
1312 | | hb_pdf_sweep_emit_patch (float a0, hb_color_t c0, |
1313 | | float a1, hb_color_t c1, |
1314 | | void *user_data) |
1315 | 2.84M | { |
1316 | 2.84M | auto *ctx = (hb_pdf_sweep_ctx_t *) user_data; |
1317 | 2.84M | hb_pdf_add_sweep_patch (ctx->mesh, ctx->alpha_mesh, |
1318 | 2.84M | ctx->cx, ctx->cy, |
1319 | 2.84M | ctx->xlo, ctx->xhi, ctx->ylo, ctx->yhi, |
1320 | 2.84M | a0, c0, a1, c1); |
1321 | 2.84M | } |
1322 | | |
1323 | | static void |
1324 | | hb_pdf_paint_sweep_gradient (hb_paint_funcs_t *, |
1325 | | void *paint_data, |
1326 | | hb_color_line_t *color_line, |
1327 | | float cx, float cy, |
1328 | | float start_angle, |
1329 | | float end_angle, |
1330 | | void *) |
1331 | 4.82k | { |
1332 | 4.82k | auto *paint = (hb_vector_paint_t *) paint_data; |
1333 | 4.82k | if (unlikely (!paint->ensure_initialized ())) |
1334 | 1.21k | return; |
1335 | 3.61k | auto *res = hb_pdf_get_resources (paint); |
1336 | 3.61k | if (unlikely (!res)) |
1337 | 10 | return; |
1338 | | |
1339 | | /* Get and sort color stops. */ |
1340 | 3.60k | if (!paint->fetch_color_stops (color_line)) |
1341 | 920 | return; |
1342 | 2.68k | hb_vector_t<hb_color_stop_t> &stops = paint->color_stops_scratch; |
1343 | 2.68k | stops.as_array ().qsort ( |
1344 | 2.68k | [] (const hb_color_stop_t &a, const hb_color_stop_t &b) |
1345 | 678k | { return (a.offset > b.offset) - (a.offset < b.offset); }); |
1346 | | |
1347 | 2.68k | hb_paint_extend_t extend = hb_color_line_get_extend (color_line); |
1348 | | |
1349 | 2.68k | const float R = 32767.f; |
1350 | 2.68k | float scx = paint->sx (cx), scy = paint->sy (cy); |
1351 | 2.68k | float xlo = scx - R - 1, xhi = scx + R + 1; |
1352 | 2.68k | float ylo = scy - R - 1, yhi = scy + R + 1; |
1353 | | |
1354 | 2.68k | bool needs_alpha = hb_pdf_gradient_needs_alpha (stops); |
1355 | | |
1356 | 2.68k | hb_vector_buf_t mesh; |
1357 | 2.68k | hb_vector_buf_t alpha_mesh; |
1358 | 2.68k | mesh.alloc (256); |
1359 | 2.68k | if (needs_alpha) |
1360 | 1.96k | alpha_mesh.alloc (256); |
1361 | | |
1362 | 2.68k | hb_pdf_sweep_ctx_t ctx { &mesh, needs_alpha ? &alpha_mesh : nullptr, |
1363 | 2.68k | scx, scy, xlo, xhi, ylo, yhi }; |
1364 | 2.68k | hb_paint_sweep_gradient_tiles (stops.arrayZ, stops.length, extend, |
1365 | 2.68k | start_angle, end_angle, |
1366 | 2.68k | hb_pdf_sweep_emit_patch, &ctx); |
1367 | | |
1368 | 2.68k | if (!mesh.length) |
1369 | 256 | return; |
1370 | | |
1371 | 2.43k | auto hb_pdf_build_mesh_shading = [&] (hb_vector_buf_t &m, |
1372 | 2.43k | const char *cs, |
1373 | 2.43k | const char *decode_suffix) -> unsigned |
1374 | 4.12k | { |
1375 | 4.12k | hb_vector_buf_t sh; |
1376 | 4.12k | sh.append_str ("<< /ShadingType 6 /ColorSpace /"); |
1377 | 4.12k | sh.append_str (cs); |
1378 | 4.12k | sh.append_str ("\n/BitsPerCoordinate 16 /BitsPerComponent 8 /BitsPerFlag 8\n"); |
1379 | 4.12k | sh.append_str ("/Decode ["); |
1380 | 4.12k | sh.append_num (xlo, 2); |
1381 | 4.12k | sh.append_c (' '); |
1382 | 4.12k | sh.append_num (xhi, 2); |
1383 | 4.12k | sh.append_c (' '); |
1384 | 4.12k | sh.append_num (ylo, 2); |
1385 | 4.12k | sh.append_c (' '); |
1386 | 4.12k | sh.append_num (yhi, 2); |
1387 | 4.12k | sh.append_str (decode_suffix); |
1388 | 4.12k | sh.append_str ("]\n/Length "); |
1389 | 4.12k | sh.append_unsigned (m.length); |
1390 | 4.12k | sh.append_str (" >>\nstream\n"); |
1391 | 4.12k | sh.append_len (m.arrayZ, m.length); |
1392 | 4.12k | sh.append_str ("\nendstream"); |
1393 | 4.12k | return res->add_object (std::move (sh)); |
1394 | 4.12k | }; |
1395 | | |
1396 | 2.43k | unsigned sh_obj_id = hb_pdf_build_mesh_shading (mesh, "DeviceRGB", |
1397 | 2.43k | " 0 1 0 1 0 1"); |
1398 | 2.43k | unsigned sh_idx = res->add_shading_by_id (sh_obj_id); |
1399 | | |
1400 | 2.43k | auto &body = paint->current_body (); |
1401 | | |
1402 | 2.43k | if (needs_alpha && alpha_mesh.length) |
1403 | 1.69k | { |
1404 | 1.69k | unsigned alpha_sh_id = hb_pdf_build_mesh_shading (alpha_mesh, "DeviceGray", |
1405 | 1.69k | " 0 1"); |
1406 | 1.69k | unsigned gs_idx = res->add_extgstate_smask (alpha_sh_id, |
1407 | 1.69k | xlo, ylo, |
1408 | 1.69k | xhi - xlo, yhi - ylo, |
1409 | 1.69k | paint->get_precision ()); |
1410 | 1.69k | body.append_str ("/GS"); |
1411 | 1.69k | body.append_unsigned (gs_idx); |
1412 | 1.69k | body.append_str (" gs\n"); |
1413 | 1.69k | } |
1414 | | |
1415 | 2.43k | body.append_str ("/SH"); |
1416 | 2.43k | body.append_unsigned (sh_idx); |
1417 | 2.43k | body.append_str (" sh\n"); |
1418 | 2.43k | } |
1419 | | |
1420 | | static const char * |
1421 | | hb_pdf_blend_mode_name (hb_paint_composite_mode_t mode) |
1422 | 17.0k | { |
1423 | 17.0k | switch (mode) |
1424 | 17.0k | { |
1425 | 252 | case HB_PAINT_COMPOSITE_MODE_MULTIPLY: return "Multiply"; |
1426 | 58 | case HB_PAINT_COMPOSITE_MODE_SCREEN: return "Screen"; |
1427 | 73 | case HB_PAINT_COMPOSITE_MODE_OVERLAY: return "Overlay"; |
1428 | 42 | case HB_PAINT_COMPOSITE_MODE_DARKEN: return "Darken"; |
1429 | 226 | case HB_PAINT_COMPOSITE_MODE_LIGHTEN: return "Lighten"; |
1430 | 98 | case HB_PAINT_COMPOSITE_MODE_COLOR_DODGE: return "ColorDodge"; |
1431 | 70 | case HB_PAINT_COMPOSITE_MODE_COLOR_BURN: return "ColorBurn"; |
1432 | 70 | case HB_PAINT_COMPOSITE_MODE_HARD_LIGHT: return "HardLight"; |
1433 | 70 | case HB_PAINT_COMPOSITE_MODE_SOFT_LIGHT: return "SoftLight"; |
1434 | 72 | case HB_PAINT_COMPOSITE_MODE_DIFFERENCE: return "Difference"; |
1435 | 66 | case HB_PAINT_COMPOSITE_MODE_EXCLUSION: return "Exclusion"; |
1436 | 308 | case HB_PAINT_COMPOSITE_MODE_HSL_HUE: return "Hue"; |
1437 | 224 | case HB_PAINT_COMPOSITE_MODE_HSL_SATURATION: return "Saturation"; |
1438 | 223 | case HB_PAINT_COMPOSITE_MODE_HSL_COLOR: return "Color"; |
1439 | 202 | case HB_PAINT_COMPOSITE_MODE_HSL_LUMINOSITY: return "Luminosity"; |
1440 | | /* Porter-Duff modes have no PDF blend-mode equivalent; approximate |
1441 | | * the two that have a plausible color-blend analog, and let the |
1442 | | * rest fall through to Normal (SRC_OVER). */ |
1443 | 47 | case HB_PAINT_COMPOSITE_MODE_PLUS: return "Screen"; |
1444 | 38 | case HB_PAINT_COMPOSITE_MODE_XOR: return "Difference"; |
1445 | 4.74k | case HB_PAINT_COMPOSITE_MODE_CLEAR: |
1446 | 4.92k | case HB_PAINT_COMPOSITE_MODE_SRC: |
1447 | 5.23k | case HB_PAINT_COMPOSITE_MODE_DEST: |
1448 | 13.9k | case HB_PAINT_COMPOSITE_MODE_SRC_OVER: |
1449 | 14.1k | case HB_PAINT_COMPOSITE_MODE_DEST_OVER: |
1450 | 14.3k | case HB_PAINT_COMPOSITE_MODE_SRC_IN: |
1451 | 14.4k | case HB_PAINT_COMPOSITE_MODE_DEST_IN: |
1452 | 14.6k | case HB_PAINT_COMPOSITE_MODE_SRC_OUT: |
1453 | 14.8k | case HB_PAINT_COMPOSITE_MODE_DEST_OUT: |
1454 | 14.8k | case HB_PAINT_COMPOSITE_MODE_SRC_ATOP: |
1455 | 14.8k | case HB_PAINT_COMPOSITE_MODE_DEST_ATOP: |
1456 | 14.8k | default: return nullptr; /* Normal */ |
1457 | 17.0k | } |
1458 | 17.0k | } |
1459 | | |
1460 | | static void |
1461 | | hb_pdf_paint_push_group (hb_paint_funcs_t *, |
1462 | | void *paint_data, |
1463 | | void *) |
1464 | 0 | { |
1465 | 0 | auto *paint = (hb_vector_paint_t *) paint_data; |
1466 | 0 | if (unlikely (!paint->ensure_initialized ())) |
1467 | 0 | return; |
1468 | 0 | paint->current_body ().append_str ("q\n"); |
1469 | 0 | } |
1470 | | |
1471 | | static void |
1472 | | hb_pdf_paint_push_group_for (hb_paint_funcs_t *, |
1473 | | void *paint_data, |
1474 | | hb_paint_composite_mode_t mode, |
1475 | | void *) |
1476 | 25.0k | { |
1477 | 25.0k | auto *paint = (hb_vector_paint_t *) paint_data; |
1478 | 25.0k | if (unlikely (!paint->ensure_initialized ())) |
1479 | 8.03k | return; |
1480 | | |
1481 | 17.0k | auto &body = paint->current_body (); |
1482 | 17.0k | body.append_str ("q\n"); |
1483 | | |
1484 | 17.0k | const char *bm = hb_pdf_blend_mode_name (mode); |
1485 | 17.0k | if (bm) |
1486 | 2.13k | { |
1487 | 2.13k | auto *res = hb_pdf_get_resources (paint); |
1488 | 2.13k | if (likely (res)) |
1489 | 1.95k | { |
1490 | 1.95k | unsigned gs_idx = res->add_extgstate_blend (bm); |
1491 | 1.95k | body.append_str ("/GS"); |
1492 | 1.95k | body.append_unsigned (gs_idx); |
1493 | 1.95k | body.append_str (" gs\n"); |
1494 | 1.95k | } |
1495 | 2.13k | } |
1496 | 17.0k | } |
1497 | | |
1498 | | static void |
1499 | | hb_pdf_paint_pop_group (hb_paint_funcs_t *, |
1500 | | void *paint_data, |
1501 | | hb_paint_composite_mode_t mode HB_UNUSED, |
1502 | | void *) |
1503 | 25.0k | { |
1504 | 25.0k | auto *paint = (hb_vector_paint_t *) paint_data; |
1505 | 25.0k | if (unlikely (!paint->ensure_initialized ())) |
1506 | 8.05k | return; |
1507 | 16.9k | paint->current_body ().append_str ("Q\n"); |
1508 | 16.9k | } |
1509 | | |
1510 | | |
1511 | | /* ---- lazy loader for paint funcs ---- */ |
1512 | | |
1513 | | static inline void free_static_pdf_paint_funcs (); |
1514 | | |
1515 | | static struct hb_pdf_paint_funcs_lazy_loader_t |
1516 | | : hb_paint_funcs_lazy_loader_t<hb_pdf_paint_funcs_lazy_loader_t> |
1517 | | { |
1518 | | static hb_paint_funcs_t *create () |
1519 | 1 | { |
1520 | 1 | hb_paint_funcs_t *funcs = hb_paint_funcs_create (); |
1521 | 1 | hb_paint_funcs_set_push_transform_func (funcs, (hb_paint_push_transform_func_t) hb_pdf_paint_push_transform, nullptr, nullptr); |
1522 | 1 | hb_paint_funcs_set_pop_transform_func (funcs, (hb_paint_pop_transform_func_t) hb_pdf_paint_pop_transform, nullptr, nullptr); |
1523 | 1 | hb_paint_funcs_set_push_clip_glyph_func (funcs, (hb_paint_push_clip_glyph_func_t) hb_pdf_paint_push_clip_glyph, nullptr, nullptr); |
1524 | 1 | hb_paint_funcs_set_push_clip_rectangle_func (funcs, (hb_paint_push_clip_rectangle_func_t) hb_pdf_paint_push_clip_rectangle, nullptr, nullptr); |
1525 | 1 | hb_paint_funcs_set_push_clip_path_start_func (funcs, (hb_paint_push_clip_path_start_func_t) hb_pdf_paint_push_clip_path_start, nullptr, nullptr); |
1526 | 1 | hb_paint_funcs_set_push_clip_path_end_func (funcs, (hb_paint_push_clip_path_end_func_t) hb_pdf_paint_push_clip_path_end, nullptr, nullptr); |
1527 | 1 | hb_paint_funcs_set_pop_clip_func (funcs, (hb_paint_pop_clip_func_t) hb_pdf_paint_pop_clip, nullptr, nullptr); |
1528 | 1 | hb_paint_funcs_set_color_func (funcs, (hb_paint_color_func_t) hb_pdf_paint_color, nullptr, nullptr); |
1529 | 1 | hb_paint_funcs_set_image_func (funcs, (hb_paint_image_func_t) hb_pdf_paint_image, nullptr, nullptr); |
1530 | 1 | hb_paint_funcs_set_linear_gradient_func (funcs, (hb_paint_linear_gradient_func_t) hb_pdf_paint_linear_gradient, nullptr, nullptr); |
1531 | 1 | hb_paint_funcs_set_radial_gradient_func (funcs, (hb_paint_radial_gradient_func_t) hb_pdf_paint_radial_gradient, nullptr, nullptr); |
1532 | 1 | hb_paint_funcs_set_sweep_gradient_func (funcs, (hb_paint_sweep_gradient_func_t) hb_pdf_paint_sweep_gradient, nullptr, nullptr); |
1533 | 1 | hb_paint_funcs_set_push_group_func (funcs, (hb_paint_push_group_func_t) hb_pdf_paint_push_group, nullptr, nullptr); |
1534 | 1 | hb_paint_funcs_set_push_group_for_func (funcs, (hb_paint_push_group_for_func_t) hb_pdf_paint_push_group_for, nullptr, nullptr); |
1535 | 1 | hb_paint_funcs_set_pop_group_func (funcs, (hb_paint_pop_group_func_t) hb_pdf_paint_pop_group, nullptr, nullptr); |
1536 | 1 | hb_paint_funcs_make_immutable (funcs); |
1537 | 1 | hb_atexit (free_static_pdf_paint_funcs); |
1538 | 1 | return funcs; |
1539 | 1 | } |
1540 | | } static_pdf_paint_funcs; |
1541 | | |
1542 | | static inline void |
1543 | | free_static_pdf_paint_funcs () |
1544 | 1 | { |
1545 | 1 | static_pdf_paint_funcs.free_instance (); |
1546 | 1 | } |
1547 | | |
1548 | | hb_paint_funcs_t * |
1549 | | hb_vector_paint_pdf_funcs_get () |
1550 | 164k | { |
1551 | 164k | return static_pdf_paint_funcs.get_unconst (); |
1552 | 164k | } |
1553 | | |
1554 | | |
1555 | | /* ---- render ---- */ |
1556 | | |
1557 | | hb_blob_t * |
1558 | | hb_vector_paint_render_pdf (hb_vector_paint_t *paint) |
1559 | 27.2k | { |
1560 | 27.2k | if (!paint->has_extents) |
1561 | 20.2k | return nullptr; |
1562 | 7.05k | if (!paint->group_stack.length || |
1563 | 6.90k | !paint->group_stack.arrayZ[0].length) |
1564 | 151 | return nullptr; |
1565 | | |
1566 | 6.90k | hb_vector_buf_t &content = paint->group_stack.arrayZ[0]; |
1567 | 6.90k | hb_pdf_resources_t *res = hb_pdf_get_resources (paint); |
1568 | | |
1569 | 6.90k | float ex = paint->extents.x; |
1570 | 6.90k | float ey = paint->extents.y; |
1571 | 6.90k | float ew = paint->extents.width; |
1572 | 6.90k | float eh = paint->extents.height; |
1573 | | |
1574 | 6.90k | unsigned num_extra = res ? res->objects.length : 0; |
1575 | 6.90k | unsigned total_objects = 4 + num_extra; /* 1-based: 1..total_objects */ |
1576 | | |
1577 | | /* Build PDF. */ |
1578 | 6.90k | hb_vector_buf_t out; |
1579 | 6.90k | hb_buf_recover_recycled (paint->recycled_blob, &out); |
1580 | 6.90k | out.alloc (content.length + num_extra * 128 + 1024); |
1581 | | |
1582 | 6.90k | hb_vector_t<unsigned> offsets; |
1583 | 6.90k | if (unlikely (!offsets.resize (total_objects))) |
1584 | 123 | return nullptr; |
1585 | | |
1586 | 6.78k | out.append_str ("%PDF-1.4\n%\xC0\xC1\xC2\xC3\n"); |
1587 | | |
1588 | | /* Object 1: Catalog */ |
1589 | 6.78k | offsets.arrayZ[0] = out.length; |
1590 | 6.78k | out.append_str ("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"); |
1591 | | |
1592 | | /* Object 2: Pages */ |
1593 | 6.78k | offsets.arrayZ[1] = out.length; |
1594 | 6.78k | out.append_str ("2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"); |
1595 | | |
1596 | | /* Object 3: Page */ |
1597 | 6.78k | offsets.arrayZ[2] = out.length; |
1598 | 6.78k | out.append_str ("3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox ["); |
1599 | 6.78k | out.append_num (ex); |
1600 | 6.78k | out.append_c (' '); |
1601 | 6.78k | out.append_num (-(ey + eh)); |
1602 | 6.78k | out.append_c (' '); |
1603 | 6.78k | out.append_num (ex + ew); |
1604 | 6.78k | out.append_c (' '); |
1605 | 6.78k | out.append_num (-ey); |
1606 | 6.78k | out.append_str ("]\n/Contents 4 0 R"); |
1607 | | |
1608 | | /* Resources. */ |
1609 | 6.78k | bool has_resources = res && |
1610 | 6.55k | (res->extgstate_dict.length || res->shading_dict.length || res->xobject_dict.length); |
1611 | 6.78k | if (has_resources) |
1612 | 836 | { |
1613 | 836 | out.append_str ("\n/Resources <<"); |
1614 | 836 | if (res->extgstate_dict.length) |
1615 | 778 | { |
1616 | 778 | out.append_str (" /ExtGState << "); |
1617 | 778 | out.append_len (res->extgstate_dict.arrayZ, res->extgstate_dict.length); |
1618 | 778 | out.append_str (">>"); |
1619 | 778 | } |
1620 | 836 | if (res->shading_dict.length) |
1621 | 756 | { |
1622 | 756 | out.append_str (" /Shading << "); |
1623 | 756 | out.append_len (res->shading_dict.arrayZ, res->shading_dict.length); |
1624 | 756 | out.append_str (">>"); |
1625 | 756 | } |
1626 | 836 | if (res->xobject_dict.length) |
1627 | 20 | { |
1628 | 20 | out.append_str (" /XObject << "); |
1629 | 20 | out.append_len (res->xobject_dict.arrayZ, res->xobject_dict.length); |
1630 | 20 | out.append_str (">>"); |
1631 | 20 | } |
1632 | 836 | out.append_str (" >>"); |
1633 | 836 | } |
1634 | | |
1635 | 6.78k | out.append_str (" >>\nendobj\n"); |
1636 | | |
1637 | | /* Build content stream: optional background rect + glyph content. */ |
1638 | 6.78k | hb_vector_buf_t bg_prefix; |
1639 | 6.78k | if (hb_color_get_alpha (paint->background)) |
1640 | 0 | { |
1641 | 0 | float r = hb_color_get_red (paint->background) / 255.f; |
1642 | 0 | float g = hb_color_get_green (paint->background) / 255.f; |
1643 | 0 | float b = hb_color_get_blue (paint->background) / 255.f; |
1644 | 0 | float a = hb_color_get_alpha (paint->background) / 255.f; |
1645 | 0 | if (a < 1.f - 1.f / 512.f) |
1646 | 0 | { |
1647 | 0 | if (res) |
1648 | 0 | { |
1649 | 0 | unsigned gs_idx = res->add_extgstate_alpha (a); |
1650 | 0 | bg_prefix.append_str ("/GS"); |
1651 | 0 | bg_prefix.append_unsigned (gs_idx); |
1652 | 0 | bg_prefix.append_str (" gs\n"); |
1653 | 0 | } |
1654 | 0 | } |
1655 | 0 | bg_prefix.append_num (r, 4); |
1656 | 0 | bg_prefix.append_c (' '); |
1657 | 0 | bg_prefix.append_num (g, 4); |
1658 | 0 | bg_prefix.append_c (' '); |
1659 | 0 | bg_prefix.append_num (b, 4); |
1660 | 0 | bg_prefix.append_str (" rg\n"); |
1661 | 0 | bg_prefix.append_num (ex); |
1662 | 0 | bg_prefix.append_c (' '); |
1663 | 0 | bg_prefix.append_num (-(ey + eh)); |
1664 | 0 | bg_prefix.append_c (' '); |
1665 | 0 | bg_prefix.append_num (ew); |
1666 | 0 | bg_prefix.append_c (' '); |
1667 | 0 | bg_prefix.append_num (eh); |
1668 | 0 | bg_prefix.append_str (" re f\n"); |
1669 | 0 | } |
1670 | 6.78k | unsigned stream_len = bg_prefix.length + content.length; |
1671 | | |
1672 | | /* Object 4: Content stream */ |
1673 | 6.78k | offsets.arrayZ[3] = out.length; |
1674 | 6.78k | out.append_str ("4 0 obj\n<< /Length "); |
1675 | 6.78k | out.append_unsigned (stream_len); |
1676 | 6.78k | out.append_str (" >>\nstream\n"); |
1677 | 6.78k | out.append_len (bg_prefix.arrayZ, bg_prefix.length); |
1678 | 6.78k | out.append_len (content.arrayZ, content.length); |
1679 | 6.78k | out.append_str ("endstream\nendobj\n"); |
1680 | | |
1681 | | /* Extra objects (functions, shadings, ExtGState). */ |
1682 | 661k | for (unsigned i = 0; i < num_extra; i++) |
1683 | 654k | { |
1684 | 654k | offsets.arrayZ[4 + i] = out.length; |
1685 | 654k | out.append_unsigned (5 + i); |
1686 | 654k | out.append_str (" 0 obj\n"); |
1687 | 654k | auto &obj = res->objects.arrayZ[i]; |
1688 | 654k | out.append_len (obj.data.arrayZ, obj.data.length); |
1689 | 654k | out.append_str ("\nendobj\n"); |
1690 | 654k | } |
1691 | | |
1692 | | /* Cross-reference table */ |
1693 | 6.78k | unsigned xref_offset = out.length; |
1694 | 6.78k | out.append_str ("xref\n0 "); |
1695 | 6.78k | out.append_unsigned (total_objects + 1); |
1696 | 6.78k | out.append_str ("\n0000000000 65535 f \n"); |
1697 | 688k | for (unsigned i = 0; i < total_objects; i++) |
1698 | 681k | { |
1699 | 681k | char tmp[21]; |
1700 | 681k | snprintf (tmp, sizeof (tmp), "%010u 00000 n \n", offsets.arrayZ[i]); |
1701 | 681k | out.append_len (tmp, 20); |
1702 | 681k | } |
1703 | | |
1704 | | /* Trailer */ |
1705 | 6.78k | out.append_str ("trailer\n<< /Size "); |
1706 | 6.78k | out.append_unsigned (total_objects + 1); |
1707 | 6.78k | out.append_str (" /Root 1 0 R >>\nstartxref\n"); |
1708 | 6.78k | out.append_unsigned (xref_offset); |
1709 | 6.78k | out.append_str ("\n%%EOF\n"); |
1710 | | |
1711 | 6.78k | hb_blob_t *blob = hb_buf_blob_from (&paint->recycled_blob, &out); |
1712 | | |
1713 | 6.78k | hb_vector_paint_clear (paint); |
1714 | | |
1715 | 6.78k | return blob; |
1716 | 6.90k | } |