/work/workdir/UnpackedTarball/harfbuzz/src/hb-subset-cff2.cc
Line | Count | Source |
1 | | /* |
2 | | * Copyright © 2018 Adobe Inc. |
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 | | * Adobe Author(s): Michiharu Ariza |
25 | | */ |
26 | | |
27 | | #include "hb.hh" |
28 | | |
29 | | #ifndef HB_NO_SUBSET_CFF |
30 | | |
31 | | #include "hb-open-type.hh" |
32 | | #include "hb-ot-cff2-table.hh" |
33 | | #include "hb-ot-var-common.hh" |
34 | | #include "hb-set.h" |
35 | | #include "hb-subset-plan.hh" |
36 | | #include "hb-subset-cff-common.hh" |
37 | | #include "hb-cff2-interp-cs.hh" |
38 | | #include "hb-subset-cff2-to-cff1.hh" |
39 | | |
40 | | using namespace CFF; |
41 | | |
42 | | namespace CFF { |
43 | | |
44 | | /* |
45 | | * Partial instancing (some axes pinned or limited, others kept). |
46 | | * |
47 | | * This is a port of fontTools' instantiateCFF2 (fonttools/fonttools#3506), |
48 | | * with one simplification: the variation-store instancing solver acts on |
49 | | * regions independently of the per-blend delta values, so instead of lifting |
50 | | * every blend's deltas into the store, instancing it, and reading the rows |
51 | | * back, we instance each VarData once with identity delta rows. The |
52 | | * instantiated tuples then give, per VarData, a fixed linear map from old |
53 | | * per-region deltas to new per-region deltas. The contribution that folds |
54 | | * into each blend's default value is the old store's region scalars |
55 | | * evaluated at the new default location (plan->normalized_coords); the |
56 | | * surviving rebased regions all evaluate to zero there. |
57 | | * |
58 | | * Use: create (), then note_use () for every blend the retained charstrings |
59 | | * and private dicts hold, then finalize (); only VarDatas that are both |
60 | | * still variable and actually used survive into the new store. |
61 | | */ |
62 | | struct cff2_instancing_plan_t |
63 | | { |
64 | | struct vardata_transform_t |
65 | | { |
66 | | /* new_deltas[j] = Σ_i matrix[j][i] * old_deltas[i] */ |
67 | | hb_vector_t<hb_vector_t<float>> matrix; |
68 | | /* Default-value fold-in scalars, one per old region: the old store's |
69 | | * region scalars at the new default location. */ |
70 | | hb_vector_t<float> gains; |
71 | | /* Surviving regions, parallel to matrix rows. */ |
72 | | hb_vector_t<hb_hashmap_t<hb_tag_t, Triple>> regions; |
73 | | /* Index into the new global region list, per new region (finalize). */ |
74 | | hb_vector_t<unsigned> region_indices; |
75 | | unsigned new_major = 0; |
76 | | bool alive_ = false; |
77 | 0 | bool alive () const { return alive_; } |
78 | | |
79 | | /* One value's delta for new region j, from its old per-region deltas. */ |
80 | | double transform_delta (unsigned j, hb_array_t<const number_t> deltas) const |
81 | 0 | { |
82 | 0 | const hb_vector_t<float> &col = matrix.arrayZ[j]; |
83 | 0 | double d = 0.; |
84 | 0 | unsigned count = hb_min (col.length, deltas.length); |
85 | 0 | for (unsigned c = 0; c < count; c++) |
86 | 0 | d += (double) col.arrayZ[c] * deltas.arrayZ[c].to_real (); |
87 | 0 | return d; |
88 | 0 | } |
89 | | }; |
90 | | |
91 | | hb_vector_t<vardata_transform_t> transforms; /* per old VarData (vsindex) */ |
92 | | hb_vector_t<hb_hashmap_t<hb_tag_t, Triple>> new_regions; /* global region list */ |
93 | | hb_set_t used_majors; |
94 | | unsigned new_major_count = 0; |
95 | | |
96 | 0 | bool has_variations () const { return new_major_count; } |
97 | | |
98 | | const vardata_transform_t *get_transform (unsigned major) const |
99 | 0 | { return major < transforms.length ? &transforms.arrayZ[major] : nullptr; } |
100 | | |
101 | | /* Record that a retained blend references this VarData. */ |
102 | 0 | void note_use (unsigned major) { used_majors.add (major); } |
103 | | |
104 | | /* The new vsindex a private dict's (or charstring's) old ivs maps to; |
105 | | * dead VarDatas fall back to 0, their blends having become static. */ |
106 | | unsigned remap_ivs (unsigned old_ivs) const |
107 | 0 | { |
108 | 0 | const vardata_transform_t *t = get_transform (old_ivs); |
109 | 0 | return (t && t->alive ()) ? t->new_major : 0; |
110 | 0 | } |
111 | | |
112 | | double fold_deltas (unsigned old_ivs, hb_array_t<const number_t> deltas) const |
113 | 0 | { |
114 | 0 | const vardata_transform_t *t = get_transform (old_ivs); |
115 | 0 | if (unlikely (!t || t->gains.length != deltas.length)) return 0.; |
116 | 0 | double v = 0.; |
117 | 0 | for (unsigned i = 0; i < deltas.length; i++) |
118 | 0 | v += (double) t->gains.arrayZ[i] * deltas.arrayZ[i].to_real (); |
119 | 0 | return v; |
120 | 0 | } |
121 | | |
122 | | bool create (const CFF2ItemVariationStore *cff2VarStore, |
123 | | const hb_subset_plan_t *plan) |
124 | 0 | { |
125 | 0 | const OT::ItemVariationStore &varStore = cff2VarStore->varStore; |
126 | 0 | unsigned major_count = varStore.get_sub_table_count (); |
127 | 0 | if (!transforms.resize (major_count)) return false; |
128 | | |
129 | 0 | if (!varStore.get_region_list ().get_var_regions (plan->axes_old_index_tag_map, orig_regions)) |
130 | 0 | return false; |
131 | | |
132 | | /* Instance each VarData with identity delta rows; the surviving tuples' |
133 | | * delta columns are the old-region → new-region transform. */ |
134 | 0 | OT::optimize_scratch_t scratch; |
135 | 0 | for (unsigned m = 0; m < major_count; m++) |
136 | 0 | { |
137 | 0 | const OT::VarData &var_data = varStore.get_sub_table (m); |
138 | 0 | unsigned k = var_data.get_region_index_count (); |
139 | 0 | vardata_transform_t &t = transforms.arrayZ[m]; |
140 | |
|
141 | 0 | if (!t.gains.resize_exact (k)) return false; |
142 | 0 | varStore.get_region_scalars (m, plan->normalized_coords.arrayZ, |
143 | 0 | plan->normalized_coords.length, |
144 | 0 | t.gains.arrayZ, k); |
145 | |
|
146 | 0 | OT::tuple_variations_t tuples; |
147 | 0 | for (unsigned r = 0; r < k; r++) |
148 | 0 | { |
149 | 0 | OT::tuple_delta_t tuple; |
150 | 0 | if (!tuple.deltas_x.resize (k) || |
151 | 0 | !tuple.indices.resize (k)) |
152 | 0 | return false; |
153 | 0 | for (unsigned i = 0; i < k; i++) |
154 | 0 | tuple.indices.arrayZ[i] = true; |
155 | 0 | tuple.deltas_x.arrayZ[r] = 1.f; |
156 | 0 | unsigned region_index = var_data.get_region_index (r); |
157 | 0 | if (unlikely (region_index >= orig_regions.length)) return false; |
158 | 0 | tuple.axis_tuples = orig_regions.arrayZ[region_index]; |
159 | 0 | if (unlikely (tuple.axis_tuples.in_error ())) return false; |
160 | 0 | tuples.tuple_vars.push (std::move (tuple)); |
161 | 0 | } |
162 | 0 | if (unlikely (tuples.tuple_vars.in_error ())) return false; |
163 | | |
164 | 0 | if (!tuples.instantiate (plan->axes_location, plan->axes_triple_distances, scratch)) |
165 | 0 | return false; |
166 | | |
167 | 0 | for (auto &tuple : tuples.tuple_vars) |
168 | 0 | { |
169 | 0 | if (unlikely (tuple.deltas_x.length != k)) return false; |
170 | 0 | hb_vector_t<float> col; |
171 | 0 | if (unlikely (!col.resize_exact (k))) return false; |
172 | 0 | for (unsigned i = 0; i < k; i++) |
173 | 0 | col.arrayZ[i] = tuple.deltas_x.arrayZ[i]; |
174 | 0 | t.matrix.push (std::move (col)); |
175 | 0 | t.regions.push (std::move (tuple.axis_tuples)); |
176 | 0 | } |
177 | 0 | if (unlikely (t.matrix.in_error () || t.regions.in_error ())) return false; |
178 | 0 | } |
179 | | |
180 | 0 | return true; |
181 | 0 | } |
182 | | |
183 | | /* Decide which VarDatas survive, number them, and build the new global |
184 | | * region list: surviving original regions first, in their original order, |
185 | | * then solver-split new regions in order of appearance (fontTools' |
186 | | * rebuildRegions + prune_regions). */ |
187 | | bool finalize () |
188 | 0 | { |
189 | 0 | for (unsigned m = 0; m < transforms.length; m++) |
190 | 0 | { |
191 | 0 | vardata_transform_t &t = transforms.arrayZ[m]; |
192 | 0 | t.alive_ = t.matrix.length && used_majors.has (m); |
193 | 0 | if (t.alive_) |
194 | 0 | t.new_major = new_major_count++; |
195 | 0 | } |
196 | | |
197 | | /* Keys hash by content, so pointers into different owners compare fine. */ |
198 | 0 | hb_hashmap_t<const hb_hashmap_t<hb_tag_t, Triple>*, unsigned> region_idx_map; |
199 | 0 | hb_vector_t<const hb_hashmap_t<hb_tag_t, Triple>*> region_ptrs; |
200 | 0 | for (const vardata_transform_t &t : transforms) |
201 | 0 | { |
202 | 0 | if (!t.alive ()) continue; |
203 | 0 | for (const auto ®ion : t.regions) |
204 | 0 | if (!region_idx_map.has (®ion)) |
205 | 0 | if (unlikely (!region_idx_map.set (®ion, (unsigned) -1))) |
206 | 0 | return false; |
207 | 0 | } |
208 | 0 | for (const auto &r : orig_regions) |
209 | 0 | { |
210 | 0 | unsigned *idx; |
211 | 0 | if (region_idx_map.has (&r, &idx) && *idx == (unsigned) -1) |
212 | 0 | { |
213 | 0 | if (unlikely (!region_idx_map.set (&r, region_ptrs.length))) return false; |
214 | 0 | region_ptrs.push (&r); |
215 | 0 | } |
216 | 0 | } |
217 | 0 | for (const vardata_transform_t &t : transforms) |
218 | 0 | { |
219 | 0 | if (!t.alive ()) continue; |
220 | 0 | for (const auto ®ion : t.regions) |
221 | 0 | { |
222 | 0 | unsigned *idx; |
223 | 0 | if (region_idx_map.has (®ion, &idx) && *idx == (unsigned) -1) |
224 | 0 | { |
225 | 0 | if (unlikely (!region_idx_map.set (®ion, region_ptrs.length))) return false; |
226 | 0 | region_ptrs.push (®ion); |
227 | 0 | } |
228 | 0 | } |
229 | 0 | } |
230 | 0 | if (unlikely (region_ptrs.in_error ())) return false; |
231 | | |
232 | 0 | for (vardata_transform_t &t : transforms) |
233 | 0 | { |
234 | 0 | if (!t.alive ()) continue; |
235 | 0 | for (const auto ®ion : t.regions) |
236 | 0 | { |
237 | 0 | unsigned *idx; |
238 | 0 | if (unlikely (!region_idx_map.has (®ion, &idx))) return false; |
239 | 0 | t.region_indices.push (*idx); |
240 | 0 | } |
241 | 0 | if (unlikely (t.region_indices.in_error ())) return false; |
242 | 0 | } |
243 | | |
244 | 0 | if (!new_regions.resize (region_ptrs.length)) return false; |
245 | 0 | for (unsigned i = 0; i < region_ptrs.length; i++) |
246 | 0 | { |
247 | 0 | new_regions.arrayZ[i] = *region_ptrs.arrayZ[i]; |
248 | 0 | if (unlikely (new_regions.arrayZ[i].in_error ())) return false; |
249 | 0 | } |
250 | | |
251 | 0 | return true; |
252 | 0 | } |
253 | | |
254 | | /* Serialize the instanced store: a CFF2ItemVariationStore whose VarDatas |
255 | | * carry no items, just region indexes (blend deltas live in charstrings). |
256 | | * Laid out manually since all sizes are known up front. */ |
257 | | bool serialize_var_store (hb_serialize_context_t *c, |
258 | | const hb_vector_t<hb_tag_t> &axis_tags) const |
259 | 0 | { |
260 | 0 | unsigned data_count = new_major_count; |
261 | 0 | unsigned header_size = 2 + 4 + 2 + 4 * data_count; |
262 | |
|
263 | 0 | uint64_t offset = header_size; |
264 | 0 | hb_vector_t<unsigned> data_offsets; |
265 | 0 | for (const vardata_transform_t &t : transforms) |
266 | 0 | { |
267 | 0 | if (!t.alive ()) continue; |
268 | 0 | data_offsets.push ((unsigned) offset); |
269 | 0 | offset += 6 + 2 * (uint64_t) t.region_indices.length; |
270 | 0 | } |
271 | 0 | if (unlikely (data_offsets.in_error () || data_offsets.length != data_count)) |
272 | 0 | return false; |
273 | 0 | unsigned regions_offset = (unsigned) offset; |
274 | 0 | uint64_t total = offset + 4 + (uint64_t) new_regions.length * axis_tags.length * 6; |
275 | 0 | if (unlikely (total > 0xFFFFu)) return false; |
276 | | |
277 | 0 | HBUINT16 *size_field = c->allocate_size<HBUINT16> (HBUINT16::static_size); |
278 | 0 | if (unlikely (!size_field)) return false; |
279 | 0 | *size_field = total; |
280 | |
|
281 | 0 | char *header = c->allocate_size<char> (header_size); |
282 | 0 | if (unlikely (!header)) return false; |
283 | 0 | * (HBUINT16 *) (header + 0) = 1; /* format */ |
284 | 0 | * (HBUINT32 *) (header + 2) = regions_offset; |
285 | 0 | * (HBUINT16 *) (header + 6) = data_count; |
286 | 0 | for (unsigned i = 0; i < data_count; i++) |
287 | 0 | * (HBUINT32 *) (header + 8 + 4 * i) = data_offsets.arrayZ[i]; |
288 | |
|
289 | 0 | for (const vardata_transform_t &t : transforms) |
290 | 0 | { |
291 | 0 | if (!t.alive ()) continue; |
292 | 0 | unsigned k = t.region_indices.length; |
293 | 0 | HBUINT16 *v = (HBUINT16 *) c->allocate_size<char> (6 + 2 * k); |
294 | 0 | if (unlikely (!v)) return false; |
295 | 0 | v[0] = 0; /* itemCount */ |
296 | 0 | v[1] = 0; /* wordSizeCount */ |
297 | 0 | v[2] = k; /* regionIndices.len */ |
298 | 0 | for (unsigned j = 0; j < k; j++) |
299 | 0 | v[3 + j] = t.region_indices.arrayZ[j]; |
300 | 0 | } |
301 | | |
302 | 0 | hb_vector_t<const hb_hashmap_t<hb_tag_t, Triple>*> region_ptrs; |
303 | 0 | if (unlikely (!region_ptrs.alloc_exact (new_regions.length))) return false; |
304 | 0 | for (const auto &r : new_regions) |
305 | 0 | region_ptrs.push (&r); |
306 | 0 | auto *region_list = c->start_embed<OT::VarRegionList> (); |
307 | 0 | return region_list->serialize (c, axis_tags, region_ptrs); |
308 | 0 | } |
309 | | |
310 | | private: |
311 | | hb_vector_t<hb_hashmap_t<hb_tag_t, Triple>> orig_regions; |
312 | | }; |
313 | | |
314 | | /* Usage-collection pass for partial instancing: record which VarDatas the |
315 | | * retained charstrings' and private dicts' blends reference. Mirrors the |
316 | | * emission pass: blends consumed by dropped hint ops don't count. */ |
317 | | struct cff2_usage_param_t |
318 | | { |
319 | 0 | void init () {} |
320 | | |
321 | | cff2_instancing_plan_t *instancing = nullptr; |
322 | | bool drop_hints = false; |
323 | | unsigned ivs = 0; /* dict-side current vsindex */ |
324 | | }; |
325 | | |
326 | | struct cff2_cs_opset_usage_t : cff2_cs_opset_t<cff2_cs_opset_usage_t, cff2_usage_param_t, blend_arg_t> |
327 | | { |
328 | | static void flush_args_and_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, cff2_usage_param_t& param) |
329 | 0 | { |
330 | 0 | bool counts = true; |
331 | 0 | switch (op) |
332 | 0 | { |
333 | 0 | case OpCode_return: |
334 | 0 | case OpCode_endchar: |
335 | 0 | counts = false; |
336 | 0 | break; |
337 | | |
338 | 0 | case OpCode_hstem: |
339 | 0 | case OpCode_hstemhm: |
340 | 0 | case OpCode_vstem: |
341 | 0 | case OpCode_vstemhm: |
342 | 0 | case OpCode_hintmask: |
343 | 0 | case OpCode_cntrmask: |
344 | 0 | if (param.drop_hints) |
345 | 0 | counts = false; |
346 | 0 | break; |
347 | | |
348 | 0 | default: |
349 | 0 | break; |
350 | 0 | } |
351 | | |
352 | 0 | if (counts) |
353 | 0 | for (unsigned int i = 0; i < env.argStack.get_count (); i++) |
354 | 0 | if (env.argStack[i].blending ()) |
355 | 0 | { |
356 | 0 | param.instancing->note_use (env.get_ivs ()); |
357 | 0 | break; |
358 | 0 | } |
359 | |
|
360 | 0 | env.clear_args (); |
361 | 0 | } |
362 | | }; |
363 | | |
364 | | struct cff2_private_dict_usage_opset_t : dict_opset_t |
365 | | { |
366 | | static void process_op (op_code_t op, cff2_priv_dict_interp_env_t& env, cff2_usage_param_t& param) |
367 | 0 | { |
368 | 0 | switch (op) { |
369 | 0 | case OpCode_vsindexdict: |
370 | 0 | env.process_vsindex (); |
371 | 0 | param.ivs = env.get_ivs (); |
372 | 0 | env.clear_args (); |
373 | 0 | return; |
374 | 0 | case OpCode_blenddict: |
375 | 0 | param.instancing->note_use (param.ivs); |
376 | 0 | env.clear_args (); |
377 | 0 | return; |
378 | 0 | default: |
379 | 0 | dict_opset_t::process_op (op, env); |
380 | 0 | if (!env.argStack.is_empty ()) return; |
381 | 0 | env.clear_args (); |
382 | 0 | return; |
383 | 0 | } |
384 | 0 | } |
385 | | }; |
386 | | |
387 | | } /* namespace CFF */ |
388 | | |
389 | | struct cff2_sub_table_info_t : cff_sub_table_info_t |
390 | | { |
391 | | cff2_sub_table_info_t () |
392 | 0 | : cff_sub_table_info_t (), |
393 | 0 | var_store_link (0) |
394 | 0 | {} |
395 | | |
396 | | objidx_t var_store_link; |
397 | | }; |
398 | | |
399 | | struct cff2_top_dict_op_serializer_t : cff_top_dict_op_serializer_t<> |
400 | | { |
401 | | bool serialize (hb_serialize_context_t *c, |
402 | | const op_str_t &opstr, |
403 | | const cff2_sub_table_info_t &info) const |
404 | 0 | { |
405 | 0 | TRACE_SERIALIZE (this); |
406 | |
|
407 | 0 | switch (opstr.op) |
408 | 0 | { |
409 | 0 | case OpCode_vstore: |
410 | 0 | if (info.var_store_link) |
411 | 0 | return_trace (FontDict::serialize_link4_op(c, opstr.op, info.var_store_link)); |
412 | 0 | else |
413 | 0 | return_trace (true); |
414 | | |
415 | 0 | default: |
416 | 0 | return_trace (cff_top_dict_op_serializer_t<>::serialize (c, opstr, info)); |
417 | 0 | } |
418 | 0 | } |
419 | | }; |
420 | | |
421 | | struct cff2_cs_opset_flatten_t : cff2_cs_opset_t<cff2_cs_opset_flatten_t, flatten_param_t, blend_arg_t> |
422 | | { |
423 | | static void flush_args_and_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) |
424 | 0 | { |
425 | | /* Optionally capture command for specialization (before flushing, to preserve args) */ |
426 | 0 | if (param.commands) |
427 | 0 | { |
428 | 0 | bool skip_command = false; |
429 | |
|
430 | 0 | switch (op) |
431 | 0 | { |
432 | 0 | case OpCode_return: |
433 | 0 | case OpCode_endchar: |
434 | 0 | skip_command = true; |
435 | 0 | break; |
436 | | |
437 | 0 | case OpCode_hstem: |
438 | 0 | case OpCode_hstemhm: |
439 | 0 | case OpCode_vstem: |
440 | 0 | case OpCode_vstemhm: |
441 | 0 | case OpCode_hintmask: |
442 | 0 | case OpCode_cntrmask: |
443 | 0 | if (param.drop_hints) |
444 | 0 | skip_command = true; |
445 | 0 | break; |
446 | | |
447 | 0 | default: |
448 | 0 | break; |
449 | 0 | } |
450 | | |
451 | 0 | if (!skip_command) |
452 | 0 | { |
453 | 0 | cs_command_t cmd (op); |
454 | | /* Capture resolved blend values */ |
455 | 0 | for (unsigned int i = 0; i < env.argStack.get_count ();) |
456 | 0 | { |
457 | 0 | const blend_arg_t &arg = env.argStack[i]; |
458 | 0 | if (arg.blending ()) |
459 | 0 | { |
460 | | /* For blend args, capture only the resolved default value */ |
461 | 0 | cmd.args.push (arg); |
462 | | /* Skip over the multiple blend values */ |
463 | 0 | i += arg.numValues; |
464 | 0 | } |
465 | 0 | else |
466 | 0 | { |
467 | 0 | cmd.args.push (arg); |
468 | 0 | i++; |
469 | 0 | } |
470 | 0 | } |
471 | 0 | param.commands->push (cmd); |
472 | 0 | } |
473 | 0 | } |
474 | | |
475 | 0 | switch (op) |
476 | 0 | { |
477 | 0 | case OpCode_return: |
478 | 0 | case OpCode_endchar: |
479 | | /* dummy opcodes in CFF2. ignore */ |
480 | 0 | if (op == OpCode_endchar && param.instancer) |
481 | 0 | emit_vsindex_prefix (env, param); |
482 | 0 | break; |
483 | | |
484 | 0 | case OpCode_hstem: |
485 | 0 | case OpCode_hstemhm: |
486 | 0 | case OpCode_vstem: |
487 | 0 | case OpCode_vstemhm: |
488 | 0 | case OpCode_hintmask: |
489 | 0 | case OpCode_cntrmask: |
490 | 0 | if (param.drop_hints) |
491 | 0 | { |
492 | 0 | env.clear_args (); |
493 | 0 | return; |
494 | 0 | } |
495 | 0 | HB_FALLTHROUGH; |
496 | |
|
497 | 0 | default: |
498 | 0 | SUPER::flush_args_and_op (op, env, param); |
499 | 0 | break; |
500 | 0 | } |
501 | 0 | } |
502 | | |
503 | | static void flush_args (cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) |
504 | 0 | { |
505 | 0 | for (unsigned int i = 0; i < env.argStack.get_count ();) |
506 | 0 | { |
507 | 0 | const blend_arg_t &arg = env.argStack[i]; |
508 | 0 | if (arg.blending ()) |
509 | 0 | { |
510 | 0 | if (unlikely (!((arg.numValues > 0) && (env.argStack.get_count () >= arg.numValues)))) |
511 | 0 | { |
512 | 0 | env.set_error (); |
513 | 0 | return; |
514 | 0 | } |
515 | 0 | if (param.instancer) |
516 | 0 | rewrite_blends (arg, i, env, param); |
517 | 0 | else |
518 | 0 | flatten_blends (arg, i, env, param); |
519 | 0 | i += arg.numValues; |
520 | 0 | } |
521 | 0 | else |
522 | 0 | { |
523 | 0 | str_encoder_t encoder (param.flatStr); |
524 | 0 | encoder.encode_num_cs (arg); |
525 | 0 | i++; |
526 | 0 | } |
527 | 0 | } |
528 | 0 | SUPER::flush_args (env, param); |
529 | 0 | } |
530 | | |
531 | | static void flatten_blends (const blend_arg_t &arg, unsigned int i, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) |
532 | 0 | { |
533 | | /* flatten the default values */ |
534 | 0 | str_encoder_t encoder (param.flatStr); |
535 | 0 | for (unsigned int j = 0; j < arg.numValues; j++) |
536 | 0 | { |
537 | 0 | const blend_arg_t &arg1 = env.argStack[i + j]; |
538 | 0 | if (unlikely (!((arg1.blending () && (arg.numValues == arg1.numValues) && (arg1.valueIndex == j) && |
539 | 0 | (arg1.deltas.length == env.get_region_count ()))))) |
540 | 0 | { |
541 | 0 | env.set_error (); |
542 | 0 | return; |
543 | 0 | } |
544 | 0 | encoder.encode_num_cs (arg1); |
545 | 0 | } |
546 | | /* flatten deltas for each value */ |
547 | 0 | for (unsigned int j = 0; j < arg.numValues; j++) |
548 | 0 | { |
549 | 0 | const blend_arg_t &arg1 = env.argStack[i + j]; |
550 | 0 | for (unsigned int k = 0; k < arg1.deltas.length; k++) |
551 | 0 | encoder.encode_num_cs (arg1.deltas[k]); |
552 | 0 | } |
553 | | /* flatten the number of values followed by blend operator */ |
554 | 0 | encoder.encode_int (arg.numValues); |
555 | 0 | encoder.encode_op (OpCode_blendcs); |
556 | 0 | } |
557 | | |
558 | | /* Partial instancing: rewrite one blend group against the instanced store. |
559 | | * Each value's default absorbs the folded (pinned-away) contribution, and |
560 | | * its deltas are mapped onto the new region list. */ |
561 | | static void rewrite_blends (const blend_arg_t &arg, unsigned int i, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) |
562 | 0 | { |
563 | 0 | const cff2_instancing_plan_t &instancer = *param.instancer; |
564 | 0 | unsigned ivs = env.get_ivs (); |
565 | 0 | const cff2_instancing_plan_t::vardata_transform_t *t = instancer.get_transform (ivs); |
566 | 0 | if (unlikely (!t)) |
567 | 0 | { |
568 | 0 | env.set_error (); |
569 | 0 | return; |
570 | 0 | } |
571 | | |
572 | 0 | unsigned n = arg.numValues; |
573 | 0 | for (unsigned int j = 0; j < n; j++) |
574 | 0 | { |
575 | 0 | const blend_arg_t &arg1 = env.argStack[i + j]; |
576 | 0 | if (unlikely (!(arg1.blending () && (arg1.numValues == n) && (arg1.valueIndex == j) && |
577 | 0 | (arg1.deltas.length == env.get_region_count ()) && |
578 | 0 | (arg1.deltas.length == t->gains.length)))) |
579 | 0 | { |
580 | 0 | env.set_error (); |
581 | 0 | return; |
582 | 0 | } |
583 | 0 | } |
584 | | |
585 | 0 | str_encoder_t encoder (param.flatStr); |
586 | 0 | unsigned k_new = t->alive () ? t->matrix.length : 0; |
587 | |
|
588 | 0 | if (!k_new) |
589 | 0 | { |
590 | | /* All of this VarData's regions died; blends dissolve into statics. */ |
591 | 0 | for (unsigned int j = 0; j < n; j++) |
592 | 0 | { |
593 | 0 | const blend_arg_t &arg1 = env.argStack[i + j]; |
594 | 0 | number_t v; |
595 | 0 | v.set_real (arg1.to_real () + round (instancer.fold_deltas (ivs, arg1.deltas.as_array ()))); |
596 | 0 | encoder.encode_num_cs (v); |
597 | 0 | } |
598 | 0 | return; |
599 | 0 | } |
600 | | |
601 | | /* Emit in chunks so the output charstring stays within the argument |
602 | | * stack limit: values already emitted for this op, plus this chunk's |
603 | | * defaults, deltas and count, must not exceed 513. */ |
604 | 0 | unsigned done = 0; |
605 | 0 | while (done < n) |
606 | 0 | { |
607 | 0 | unsigned avail = 512 > (i + done) ? 512 - (i + done) : 0; |
608 | 0 | unsigned chunk = hb_min (avail / (k_new + 1), n - done); |
609 | 0 | if (unlikely (!chunk)) |
610 | 0 | { |
611 | 0 | env.set_error (); |
612 | 0 | return; |
613 | 0 | } |
614 | | |
615 | 0 | for (unsigned int j = done; j < done + chunk; j++) |
616 | 0 | { |
617 | 0 | const blend_arg_t &arg1 = env.argStack[i + j]; |
618 | 0 | number_t v; |
619 | 0 | v.set_real (arg1.to_real () + round (instancer.fold_deltas (ivs, arg1.deltas.as_array ()))); |
620 | 0 | encoder.encode_num_cs (v); |
621 | 0 | } |
622 | 0 | for (unsigned int j = done; j < done + chunk; j++) |
623 | 0 | { |
624 | 0 | const blend_arg_t &arg1 = env.argStack[i + j]; |
625 | 0 | for (unsigned r = 0; r < k_new; r++) |
626 | 0 | { |
627 | 0 | number_t v; |
628 | 0 | v.set_int ((int) round (t->transform_delta (r, arg1.deltas.as_array ()))); |
629 | 0 | encoder.encode_num_cs (v); |
630 | 0 | } |
631 | 0 | } |
632 | 0 | encoder.encode_int (chunk); |
633 | 0 | encoder.encode_op (OpCode_blendcs); |
634 | 0 | done += chunk; |
635 | 0 | } |
636 | 0 | param.emitted_blend = true; |
637 | 0 | } |
638 | | |
639 | | /* Partial instancing: prepend a vsindex op when the charstring's |
640 | | * (remapped) ivs differs from its FD's (remapped) private-dict ivs. */ |
641 | | static void emit_vsindex_prefix (cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) |
642 | 0 | { |
643 | 0 | if (!param.emitted_blend) return; |
644 | 0 | const cff2_instancing_plan_t &instancer = *param.instancer; |
645 | 0 | unsigned new_ivs = instancer.remap_ivs (env.get_ivs ()); |
646 | 0 | unsigned fd_ivs = instancer.remap_ivs (env.get_orig_ivs ()); |
647 | 0 | if (new_ivs == fd_ivs) return; |
648 | | |
649 | 0 | str_buff_t prefix; |
650 | 0 | str_encoder_t enc (prefix); |
651 | 0 | enc.encode_int ((int) new_ivs); |
652 | 0 | enc.encode_op (OpCode_vsindexcs); |
653 | |
|
654 | 0 | str_buff_t &buf = param.flatStr; |
655 | 0 | unsigned old_len = buf.length; |
656 | 0 | if (unlikely (prefix.in_error () || !buf.resize (old_len + prefix.length))) |
657 | 0 | { |
658 | 0 | env.set_error (); |
659 | 0 | return; |
660 | 0 | } |
661 | 0 | memmove (buf.arrayZ + prefix.length, buf.arrayZ, old_len); |
662 | 0 | hb_memcpy (buf.arrayZ, prefix.arrayZ, prefix.length); |
663 | 0 | } |
664 | | |
665 | | static void flush_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) |
666 | 0 | { |
667 | 0 | switch (op) |
668 | 0 | { |
669 | 0 | case OpCode_return: |
670 | 0 | case OpCode_endchar: |
671 | 0 | return; |
672 | 0 | default: |
673 | 0 | str_encoder_t encoder (param.flatStr); |
674 | 0 | encoder.encode_op (op); |
675 | 0 | } |
676 | 0 | } |
677 | | |
678 | | static void flush_hintmask (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) |
679 | 0 | { |
680 | 0 | SUPER::flush_hintmask (op, env, param); |
681 | | /* Preserve hintmask payload in captured commands for specializer re-encoding. */ |
682 | 0 | if (param.commands && !param.drop_hints && param.commands->length > 0) |
683 | 0 | { |
684 | 0 | auto &cmd = param.commands->tail (); |
685 | 0 | if (cmd.op == op) |
686 | 0 | { |
687 | 0 | cmd.mask_bytes.resize (env.hintmask_size); |
688 | 0 | if (unlikely (cmd.mask_bytes.in_error ())) |
689 | 0 | { |
690 | 0 | env.set_error (); |
691 | 0 | return; |
692 | 0 | } |
693 | 0 | for (unsigned int i = 0; i < env.hintmask_size; i++) |
694 | 0 | cmd.mask_bytes[i] = env.str_ref[i]; |
695 | 0 | } |
696 | 0 | } |
697 | 0 | if (!param.drop_hints) |
698 | 0 | { |
699 | 0 | str_encoder_t encoder (param.flatStr); |
700 | 0 | for (unsigned int i = 0; i < env.hintmask_size; i++) |
701 | 0 | encoder.encode_byte (env.str_ref[i]); |
702 | 0 | } |
703 | 0 | } |
704 | | |
705 | | private: |
706 | | typedef cff2_cs_opset_t<cff2_cs_opset_flatten_t, flatten_param_t, blend_arg_t> SUPER; |
707 | | typedef cs_opset_t<blend_arg_t, cff2_cs_opset_flatten_t, cff2_cs_opset_flatten_t, cff2_cs_interp_env_t<blend_arg_t>, flatten_param_t> CSOPSET; |
708 | | }; |
709 | | |
710 | | struct cff2_cs_opset_subr_subset_t : cff2_cs_opset_t<cff2_cs_opset_subr_subset_t, subr_subset_param_t, blend_arg_t> |
711 | | { |
712 | | static void process_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, subr_subset_param_t& param) |
713 | 0 | { |
714 | 0 | switch (op) { |
715 | | |
716 | 0 | case OpCode_return: |
717 | 0 | param.current_parsed_str->flush_coalesced (env.str_ref); |
718 | 0 | param.current_parsed_str->set_parsed (); |
719 | 0 | env.return_from_subr (); |
720 | 0 | param.set_current_str (env, false); |
721 | 0 | break; |
722 | | |
723 | 0 | case OpCode_endchar: |
724 | 0 | param.current_parsed_str->flush_coalesced (env.str_ref); |
725 | 0 | param.current_parsed_str->set_parsed (); |
726 | 0 | SUPER::process_op (op, env, param); |
727 | 0 | break; |
728 | | |
729 | 0 | case OpCode_callsubr: |
730 | 0 | process_call_subr (op, CSType_LocalSubr, env, param, env.localSubrs, param.local_closure); |
731 | 0 | break; |
732 | | |
733 | 0 | case OpCode_callgsubr: |
734 | 0 | process_call_subr (op, CSType_GlobalSubr, env, param, env.globalSubrs, param.global_closure); |
735 | 0 | break; |
736 | | |
737 | 0 | default: |
738 | 0 | SUPER::process_op (op, env, param); |
739 | 0 | param.current_parsed_str->add_op (op, env.str_ref); |
740 | 0 | break; |
741 | 0 | } |
742 | 0 | } |
743 | | |
744 | | protected: |
745 | | static void process_call_subr (op_code_t op, cs_type_t type, |
746 | | cff2_cs_interp_env_t<blend_arg_t> &env, subr_subset_param_t& param, |
747 | | cff2_biased_subrs_t& subrs, hb_set_t *closure) |
748 | 0 | { |
749 | 0 | byte_str_ref_t str_ref = env.str_ref; |
750 | 0 | env.call_subr (subrs, type); |
751 | 0 | param.current_parsed_str->add_call_op (op, str_ref, env.context.subr_num); |
752 | 0 | closure->add (env.context.subr_num); |
753 | 0 | param.set_current_str (env, true); |
754 | 0 | } |
755 | | |
756 | | private: |
757 | | typedef cff2_cs_opset_t<cff2_cs_opset_subr_subset_t, subr_subset_param_t, blend_arg_t> SUPER; |
758 | | }; |
759 | | |
760 | | struct cff2_subr_subsetter_t : subr_subsetter_t<cff2_subr_subsetter_t, CFF2Subrs, const OT::cff2::accelerator_subset_t, cff2_cs_interp_env_t<blend_arg_t>, cff2_cs_opset_subr_subset_t> |
761 | | { |
762 | | cff2_subr_subsetter_t (const OT::cff2::accelerator_subset_t &acc_, const hb_subset_plan_t *plan_) |
763 | 0 | : subr_subsetter_t (acc_, plan_) {} |
764 | | |
765 | | static void complete_parsed_str (cff2_cs_interp_env_t<blend_arg_t> &env, subr_subset_param_t& param, parsed_cs_str_t &charstring) |
766 | 0 | { |
767 | | /* vsindex is inserted at the beginning of the charstring as necessary */ |
768 | 0 | if (env.seen_vsindex ()) |
769 | 0 | { |
770 | 0 | number_t ivs; |
771 | 0 | ivs.set_int ((int)env.get_ivs ()); |
772 | 0 | charstring.set_prefix (ivs, OpCode_vsindexcs); |
773 | 0 | } |
774 | 0 | } |
775 | | }; |
776 | | |
777 | | struct cff2_private_blend_encoder_param_t |
778 | | { |
779 | | cff2_private_blend_encoder_param_t (hb_serialize_context_t *c, |
780 | | const CFF2ItemVariationStore *varStore, |
781 | | hb_array_t<int> normalized_coords) : |
782 | 0 | c (c), varStore (varStore), normalized_coords (normalized_coords) {} |
783 | | |
784 | 0 | void init () {} |
785 | | |
786 | | void process_blend () |
787 | 0 | { |
788 | 0 | if (!seen_blend) |
789 | 0 | { |
790 | 0 | region_count = varStore->varStore.get_region_index_count (ivs); |
791 | 0 | if (unlikely (!scalars.resize_exact (region_count))) |
792 | 0 | { |
793 | 0 | if (c) c->err (HB_SERIALIZE_ERROR_OTHER); |
794 | 0 | seen_blend = true; |
795 | 0 | return; |
796 | 0 | } |
797 | 0 | varStore->varStore.get_region_scalars (ivs, normalized_coords.arrayZ, normalized_coords.length, |
798 | 0 | &scalars[0], region_count); |
799 | 0 | seen_blend = true; |
800 | 0 | } |
801 | 0 | } |
802 | | |
803 | | double blend_deltas (hb_array_t<const number_t> deltas) const |
804 | 0 | { |
805 | 0 | double v = 0; |
806 | 0 | if (likely (scalars.length == deltas.length)) |
807 | 0 | { |
808 | 0 | unsigned count = scalars.length; |
809 | 0 | for (unsigned i = 0; i < count; i++) |
810 | 0 | v += (double) scalars.arrayZ[i] * deltas.arrayZ[i].to_real (); |
811 | 0 | } |
812 | 0 | return v; |
813 | 0 | } |
814 | | |
815 | | |
816 | | hb_serialize_context_t *c = nullptr; |
817 | | bool seen_blend = false; |
818 | | unsigned ivs = 0; |
819 | | unsigned region_count = 0; |
820 | | hb_vector_t<float> scalars; |
821 | | const CFF2ItemVariationStore *varStore = nullptr; |
822 | | hb_array_t<int> normalized_coords; |
823 | | |
824 | | /* CFF2 partial instancing: when set, blends are rewritten against the |
825 | | * instanced variation store instead of being flattened. */ |
826 | | const cff2_instancing_plan_t *instancer = nullptr; |
827 | | /* Error-compensated fold rounding state, per dict operator. */ |
828 | | double fold_exact = 0.; |
829 | | double fold_emitted = 0.; |
830 | | }; |
831 | | |
832 | | struct cff2_private_dict_blend_opset_t : dict_opset_t |
833 | | { |
834 | | static void process_arg_blend (cff2_private_blend_encoder_param_t& param, |
835 | | number_t &arg, |
836 | | const hb_array_t<const number_t> blends, |
837 | | unsigned n, unsigned i) |
838 | 0 | { |
839 | 0 | arg.set_int (round (arg.to_real () + param.blend_deltas (blends))); |
840 | 0 | } |
841 | | |
842 | | /* The fold added to a value, error-compensated across the operator: most |
843 | | * blended private-dict values (BlueValues, StemSnap*, ...) are |
844 | | * delta-encoded, so independently rounded folds would accumulate in the |
845 | | * decoded absolutes; keeping the emitted cumulative fold equal to the |
846 | | * rounded exact cumulative fold bounds every absolute within ±0.5. */ |
847 | | static double fold (cff2_private_blend_encoder_param_t& param, |
848 | | const hb_array_t<const number_t> blends) |
849 | 0 | { |
850 | 0 | double f = param.blend_deltas (blends); |
851 | 0 | double emit = round (param.fold_exact + f) - param.fold_emitted; |
852 | 0 | param.fold_exact += f; |
853 | 0 | param.fold_emitted += emit; |
854 | 0 | return emit; |
855 | 0 | } |
856 | | |
857 | | /* Partial instancing: emit the args preceding this blend group, then the |
858 | | * group itself rewritten against the instanced store, and clear the |
859 | | * stack. The op's remaining args are flushed at process_op time. */ |
860 | | static void rewrite_blends (cff2_priv_dict_interp_env_t& env, |
861 | | cff2_private_blend_encoder_param_t& param, |
862 | | unsigned start, unsigned n, unsigned k) |
863 | 0 | { |
864 | 0 | const cff2_instancing_plan_t &instancer = *param.instancer; |
865 | 0 | const cff2_instancing_plan_t::vardata_transform_t *t = instancer.get_transform (param.ivs); |
866 | 0 | if (unlikely (!t || t->gains.length != k)) |
867 | 0 | { |
868 | 0 | env.set_error (); |
869 | 0 | return; |
870 | 0 | } |
871 | | |
872 | 0 | str_buff_t str; |
873 | 0 | str_encoder_t encoder (str); |
874 | |
|
875 | 0 | for (unsigned idx = 0; idx < start; idx++) |
876 | 0 | encoder.encode_num_tp (env.argStack[idx]); |
877 | |
|
878 | 0 | unsigned k_new = t->alive () ? t->matrix.length : 0; |
879 | 0 | if (!k_new) |
880 | 0 | { |
881 | | /* This VarData died (or is unused by any retained charstring, which |
882 | | * can happen for FDs kept only for retain-gids holes); blends |
883 | | * dissolve into statics. */ |
884 | 0 | for (unsigned j = 0; j < n; j++) |
885 | 0 | { |
886 | 0 | const hb_array_t<const number_t> blends = env.argStack.sub_array (start + n + (j * k), k); |
887 | 0 | number_t v; |
888 | 0 | v.set_real (env.argStack[start + j].to_real () + fold (param, blends)); |
889 | 0 | encoder.encode_num_tp (v); |
890 | 0 | } |
891 | 0 | } |
892 | 0 | else |
893 | 0 | { |
894 | 0 | unsigned done = 0; |
895 | 0 | while (done < n) |
896 | 0 | { |
897 | 0 | unsigned avail = 512 > (start + done) ? 512 - (start + done) : 0; |
898 | 0 | unsigned chunk = hb_min (avail / (k_new + 1), n - done); |
899 | 0 | if (unlikely (!chunk)) |
900 | 0 | { |
901 | 0 | env.set_error (); |
902 | 0 | return; |
903 | 0 | } |
904 | | |
905 | 0 | for (unsigned j = done; j < done + chunk; j++) |
906 | 0 | { |
907 | 0 | const hb_array_t<const number_t> blends = env.argStack.sub_array (start + n + (j * k), k); |
908 | 0 | number_t v; |
909 | 0 | v.set_real (env.argStack[start + j].to_real () + fold (param, blends)); |
910 | 0 | encoder.encode_num_tp (v); |
911 | 0 | } |
912 | 0 | for (unsigned j = done; j < done + chunk; j++) |
913 | 0 | { |
914 | 0 | const hb_array_t<const number_t> blends = env.argStack.sub_array (start + n + (j * k), k); |
915 | 0 | for (unsigned r = 0; r < k_new; r++) |
916 | 0 | { |
917 | 0 | number_t v; |
918 | 0 | v.set_int ((int) round (t->transform_delta (r, blends))); |
919 | 0 | encoder.encode_num_tp (v); |
920 | 0 | } |
921 | 0 | } |
922 | 0 | encoder.encode_int (chunk); |
923 | 0 | encoder.encode_op (OpCode_blenddict); |
924 | 0 | done += chunk; |
925 | 0 | } |
926 | 0 | } |
927 | | |
928 | 0 | auto bytes = str.as_bytes (); |
929 | 0 | param.c->embed (&bytes, bytes.length); |
930 | |
|
931 | 0 | env.clear_args (); |
932 | 0 | } |
933 | | |
934 | | static void process_blend (cff2_priv_dict_interp_env_t& env, cff2_private_blend_encoder_param_t& param) |
935 | 0 | { |
936 | 0 | unsigned int n, k; |
937 | |
|
938 | 0 | param.process_blend (); |
939 | 0 | k = param.region_count; |
940 | 0 | n = env.argStack.pop_uint (); |
941 | |
|
942 | 0 | unsigned count = env.argStack.get_count (); |
943 | 0 | unsigned int total; |
944 | 0 | if (unlikely (hb_unsigned_mul_overflows (k + 1, n, &total) || total > count)) |
945 | 0 | { |
946 | 0 | env.set_error (); |
947 | 0 | return; |
948 | 0 | } |
949 | | |
950 | | /* copy the blend values into blend array of the default values */ |
951 | 0 | unsigned int start = count - total; |
952 | | /* let an obvious error case fail, but note CFF2 spec doesn't forbid n==0 */ |
953 | 0 | if (unlikely (start > count)) |
954 | 0 | { |
955 | 0 | env.set_error (); |
956 | 0 | return; |
957 | 0 | } |
958 | | |
959 | 0 | if (param.instancer && k) |
960 | 0 | { |
961 | 0 | rewrite_blends (env, param, start, n, k); |
962 | 0 | return; |
963 | 0 | } |
964 | | |
965 | 0 | for (unsigned int i = 0; i < n; i++) |
966 | 0 | { |
967 | 0 | const hb_array_t<const number_t> blends = env.argStack.sub_array (start + n + (i * k), k); |
968 | 0 | process_arg_blend (param, env.argStack[start + i], blends, n, i); |
969 | 0 | } |
970 | | |
971 | | /* pop off blend values leaving default values now adorned with blend values */ |
972 | 0 | env.argStack.pop (k * n); |
973 | 0 | } |
974 | | |
975 | | static void process_op (op_code_t op, cff2_priv_dict_interp_env_t& env, cff2_private_blend_encoder_param_t& param) |
976 | 0 | { |
977 | 0 | switch (op) { |
978 | 0 | case OpCode_StdHW: |
979 | 0 | case OpCode_StdVW: |
980 | 0 | case OpCode_BlueScale: |
981 | 0 | case OpCode_BlueShift: |
982 | 0 | case OpCode_BlueFuzz: |
983 | 0 | case OpCode_ExpansionFactor: |
984 | 0 | case OpCode_LanguageGroup: |
985 | 0 | case OpCode_BlueValues: |
986 | 0 | case OpCode_OtherBlues: |
987 | 0 | case OpCode_FamilyBlues: |
988 | 0 | case OpCode_FamilyOtherBlues: |
989 | 0 | case OpCode_StemSnapH: |
990 | 0 | case OpCode_StemSnapV: |
991 | 0 | break; |
992 | 0 | case OpCode_vsindexdict: |
993 | 0 | env.process_vsindex (); |
994 | 0 | param.ivs = env.get_ivs (); |
995 | 0 | if (param.instancer) |
996 | 0 | { |
997 | | /* Re-emit with the remapped vsindex; 0 is the dict default. */ |
998 | 0 | unsigned new_ivs = param.instancer->remap_ivs (param.ivs); |
999 | 0 | if (new_ivs) |
1000 | 0 | { |
1001 | 0 | str_buff_t str; |
1002 | 0 | str_encoder_t encoder (str); |
1003 | 0 | encoder.encode_int ((int) new_ivs); |
1004 | 0 | encoder.encode_op (OpCode_vsindexdict); |
1005 | 0 | auto bytes = str.as_bytes (); |
1006 | 0 | param.c->embed (&bytes, bytes.length); |
1007 | 0 | } |
1008 | 0 | } |
1009 | 0 | env.clear_args (); |
1010 | 0 | return; |
1011 | 0 | case OpCode_blenddict: |
1012 | 0 | process_blend (env, param); |
1013 | 0 | return; |
1014 | | |
1015 | 0 | default: |
1016 | 0 | dict_opset_t::process_op (op, env); |
1017 | 0 | if (!env.argStack.is_empty ()) return; |
1018 | 0 | break; |
1019 | 0 | } |
1020 | | |
1021 | 0 | if (unlikely (env.in_error ())) return; |
1022 | | |
1023 | | // Write args then op |
1024 | | |
1025 | 0 | str_buff_t str; |
1026 | 0 | str_encoder_t encoder (str); |
1027 | |
|
1028 | 0 | unsigned count = env.argStack.get_count (); |
1029 | 0 | for (unsigned i = 0; i < count; i++) |
1030 | 0 | encoder.encode_num_tp (env.argStack[i]); |
1031 | |
|
1032 | 0 | encoder.encode_op (op); |
1033 | |
|
1034 | 0 | auto bytes = str.as_bytes (); |
1035 | 0 | param.c->embed (&bytes, bytes.length); |
1036 | |
|
1037 | 0 | env.clear_args (); |
1038 | 0 | param.fold_exact = param.fold_emitted = 0.; |
1039 | 0 | } |
1040 | | }; |
1041 | | |
1042 | | struct cff2_private_dict_op_serializer_t : op_serializer_t |
1043 | | { |
1044 | | cff2_private_dict_op_serializer_t (bool desubroutinize_, bool drop_hints_, bool pinned_, |
1045 | | const CFF::CFF2ItemVariationStore* varStore_, |
1046 | | hb_array_t<int> normalized_coords_, |
1047 | | const cff2_instancing_plan_t *instancer_ = nullptr) |
1048 | 0 | : desubroutinize (desubroutinize_), drop_hints (drop_hints_), pinned (pinned_), |
1049 | 0 | param (nullptr, varStore_, normalized_coords_) |
1050 | 0 | { param.instancer = instancer_; } |
1051 | | |
1052 | | bool serialize (hb_serialize_context_t *c, |
1053 | | const op_str_t &opstr, |
1054 | | objidx_t subrs_link) const |
1055 | 0 | { |
1056 | 0 | TRACE_SERIALIZE (this); |
1057 | |
|
1058 | 0 | if (drop_hints && dict_opset_t::is_hint_op (opstr.op)) |
1059 | 0 | return_trace (true); |
1060 | | |
1061 | 0 | if (opstr.op == OpCode_Subrs) |
1062 | 0 | { |
1063 | 0 | if (desubroutinize || !subrs_link) |
1064 | 0 | return_trace (true); |
1065 | 0 | else |
1066 | 0 | return_trace (FontDict::serialize_link2_op (c, opstr.op, subrs_link)); |
1067 | 0 | } |
1068 | | |
1069 | 0 | if (pinned || param.instancer) |
1070 | 0 | { |
1071 | | /* Reinterpret opstr and process blends. The param persists across |
1072 | | * this dict's opstrs, so that an ivs set by a vsindexdict opstr is |
1073 | | * still in effect when a later opstr's blends are processed. */ |
1074 | 0 | cff2_priv_dict_interp_env_t env {hb_ubytes_t (opstr.ptr, opstr.length)}; |
1075 | 0 | param.c = c; |
1076 | 0 | dict_interpreter_t<cff2_private_dict_blend_opset_t, cff2_private_blend_encoder_param_t, cff2_priv_dict_interp_env_t> interp (env); |
1077 | 0 | return_trace (interp.interpret (param)); |
1078 | 0 | } |
1079 | | |
1080 | 0 | return_trace (copy_opstr (c, opstr)); |
1081 | 0 | } |
1082 | | |
1083 | | protected: |
1084 | | const bool desubroutinize; |
1085 | | const bool drop_hints; |
1086 | | const bool pinned; |
1087 | | mutable cff2_private_blend_encoder_param_t param; |
1088 | | }; |
1089 | | |
1090 | | |
1091 | | |
1092 | | namespace OT { |
1093 | | struct cff2_subset_plan |
1094 | | { |
1095 | | bool create (const OT::cff2::accelerator_subset_t &acc, |
1096 | | hb_subset_plan_t *plan) |
1097 | 0 | { |
1098 | | /* make sure notdef is first */ |
1099 | 0 | hb_codepoint_t old_glyph; |
1100 | 0 | if (!plan->old_gid_for_new_gid (0, &old_glyph) || (old_glyph != 0)) return false; |
1101 | | |
1102 | 0 | num_glyphs = plan->num_output_glyphs (); |
1103 | 0 | orig_fdcount = acc.fdArray->count; |
1104 | |
|
1105 | 0 | drop_hints = plan->flags & HB_SUBSET_FLAGS_NO_HINTING; |
1106 | 0 | bool instancing_requested = (bool) plan->normalized_coords; |
1107 | 0 | pinned = instancing_requested && plan->all_axes_pinned; |
1108 | 0 | normalized_coords = plan->normalized_coords; |
1109 | 0 | head_maxp_info = plan->head_maxp_info; |
1110 | 0 | hmtx_map = &plan->hmtx_map; |
1111 | 0 | desubroutinize = plan->flags & HB_SUBSET_FLAGS_DESUBROUTINIZE || |
1112 | 0 | instancing_requested; // For instancing we need this path |
1113 | | |
1114 | | /* Partial instancing: some axes pinned or limited, others kept. */ |
1115 | 0 | partial_instancing = instancing_requested && !pinned && |
1116 | 0 | acc.varStore != &Null (CFF2ItemVariationStore); |
1117 | 0 | if (partial_instancing && |
1118 | 0 | unlikely (!instancing.create (acc.varStore, plan))) |
1119 | 0 | return false; |
1120 | 0 | axis_tags = &plan->axis_tags; |
1121 | | |
1122 | | /* Enable command capture for CFF2→CFF1 conversion (for specialization) */ |
1123 | 0 | capture_commands = pinned; |
1124 | |
|
1125 | | #ifdef HB_EXPERIMENTAL_API |
1126 | | min_charstrings_off_size = (plan->flags & HB_SUBSET_FLAGS_IFTB_REQUIREMENTS) ? 4 : 0; |
1127 | | #else |
1128 | 0 | min_charstrings_off_size = 0; |
1129 | 0 | #endif |
1130 | |
|
1131 | 0 | if (desubroutinize) |
1132 | 0 | { |
1133 | 0 | if (partial_instancing) |
1134 | 0 | { |
1135 | | /* Pass 1: collect which VarDatas the retained blends reference, so |
1136 | | * unused ones get pruned and the survivors renumbered. */ |
1137 | 0 | hb_set_t used_fds; |
1138 | 0 | for (unsigned int i = 0; i < num_glyphs; i++) |
1139 | 0 | { |
1140 | 0 | hb_codepoint_t glyph; |
1141 | 0 | if (!plan->old_gid_for_new_gid (i, &glyph)) |
1142 | 0 | continue; |
1143 | 0 | const hb_ubytes_t str = (*acc.charStrings)[glyph]; |
1144 | 0 | unsigned int fd = acc.fdSelect->get_fd (glyph); |
1145 | 0 | if (unlikely (fd >= acc.fdCount)) |
1146 | 0 | return false; |
1147 | 0 | used_fds.add (fd); |
1148 | |
|
1149 | 0 | cff2_cs_interp_env_t<blend_arg_t> env (str, acc, fd); |
1150 | 0 | cs_interpreter_t<cff2_cs_interp_env_t<blend_arg_t>, cff2_cs_opset_usage_t, cff2_usage_param_t> interp (env); |
1151 | 0 | cff2_usage_param_t param; |
1152 | 0 | param.instancing = &instancing; |
1153 | 0 | param.drop_hints = drop_hints; |
1154 | 0 | if (unlikely (!interp.interpret (param))) |
1155 | 0 | return false; |
1156 | 0 | } |
1157 | 0 | for (unsigned fd : used_fds) |
1158 | 0 | { |
1159 | 0 | cff2_usage_param_t param; |
1160 | 0 | param.instancing = &instancing; |
1161 | 0 | unsigned count = acc.privateDicts[fd].get_count (); |
1162 | 0 | for (unsigned j = 0; j < count; j++) |
1163 | 0 | { |
1164 | 0 | const op_str_t &opstr = acc.privateDicts[fd][j]; |
1165 | 0 | if (drop_hints && dict_opset_t::is_hint_op (opstr.op)) |
1166 | 0 | continue; |
1167 | 0 | cff2_priv_dict_interp_env_t env {hb_ubytes_t (opstr.ptr, opstr.length)}; |
1168 | 0 | dict_interpreter_t<cff2_private_dict_usage_opset_t, cff2_usage_param_t, cff2_priv_dict_interp_env_t> interp (env); |
1169 | 0 | if (unlikely (!interp.interpret (param))) |
1170 | 0 | return false; |
1171 | 0 | } |
1172 | 0 | } |
1173 | 0 | if (unlikely (!instancing.finalize ())) |
1174 | 0 | return false; |
1175 | | |
1176 | | /* Pass 2: like subr_flattener_t, but blends stay symbolic (no coords |
1177 | | * fed to the env) and get rewritten against the instanced store. */ |
1178 | 0 | if (!subset_charstrings.resize_exact (num_glyphs)) |
1179 | 0 | return false; |
1180 | 0 | for (unsigned int i = 0; i < num_glyphs; i++) |
1181 | 0 | { |
1182 | 0 | hb_codepoint_t glyph; |
1183 | 0 | if (!plan->old_gid_for_new_gid (i, &glyph)) |
1184 | 0 | continue; |
1185 | 0 | const hb_ubytes_t str = (*acc.charStrings)[glyph]; |
1186 | 0 | unsigned int fd = acc.fdSelect->get_fd (glyph); |
1187 | 0 | if (unlikely (fd >= acc.fdCount)) |
1188 | 0 | return false; |
1189 | | |
1190 | 0 | cff2_cs_interp_env_t<blend_arg_t> env (str, acc, fd); |
1191 | 0 | cs_interpreter_t<cff2_cs_interp_env_t<blend_arg_t>, cff2_cs_opset_flatten_t, flatten_param_t> interp (env); |
1192 | 0 | flatten_param_t param = { |
1193 | 0 | subset_charstrings.arrayZ[i], |
1194 | 0 | (bool) (plan->flags & HB_SUBSET_FLAGS_NO_HINTING), |
1195 | 0 | plan |
1196 | 0 | }; |
1197 | 0 | param.instancer = &instancing; |
1198 | 0 | if (unlikely (!interp.interpret (param))) |
1199 | 0 | return false; |
1200 | 0 | } |
1201 | 0 | } |
1202 | 0 | else |
1203 | 0 | { |
1204 | | /* Flatten global & local subrs */ |
1205 | 0 | subr_flattener_t<const OT::cff2::accelerator_subset_t, cff2_cs_interp_env_t<blend_arg_t>, cff2_cs_opset_flatten_t> |
1206 | 0 | flattener(acc, plan); |
1207 | | |
1208 | | /* Enable command capture if requested (for specialization) */ |
1209 | 0 | if (capture_commands) |
1210 | 0 | { |
1211 | 0 | if (!charstring_commands.resize_exact (num_glyphs)) |
1212 | 0 | return false; |
1213 | | |
1214 | 0 | if (!flattener.flatten (subset_charstrings, &charstring_commands)) |
1215 | 0 | return false; |
1216 | 0 | } |
1217 | 0 | else |
1218 | 0 | { |
1219 | 0 | if (!flattener.flatten (subset_charstrings)) |
1220 | 0 | return false; |
1221 | 0 | } |
1222 | 0 | } |
1223 | 0 | } |
1224 | 0 | else |
1225 | 0 | { |
1226 | 0 | cff2_subr_subsetter_t subr_subsetter (acc, plan); |
1227 | | |
1228 | | /* Subset subrs: collect used subroutines, leaving all unused ones behind */ |
1229 | 0 | if (!subr_subsetter.subset ()) |
1230 | 0 | return false; |
1231 | | |
1232 | | /* encode charstrings, global subrs, local subrs with new subroutine numbers */ |
1233 | 0 | if (!subr_subsetter.encode_charstrings (subset_charstrings, !pinned)) |
1234 | 0 | return false; |
1235 | | |
1236 | 0 | if (!subr_subsetter.encode_globalsubrs (subset_globalsubrs)) |
1237 | 0 | return false; |
1238 | | |
1239 | | /* local subrs */ |
1240 | 0 | if (!subset_localsubrs.resize (orig_fdcount)) |
1241 | 0 | return false; |
1242 | 0 | for (unsigned int fd = 0; fd < orig_fdcount; fd++) |
1243 | 0 | { |
1244 | 0 | subset_localsubrs[fd].init (); |
1245 | 0 | if (!subr_subsetter.encode_localsubrs (fd, subset_localsubrs[fd])) |
1246 | 0 | return false; |
1247 | 0 | } |
1248 | 0 | } |
1249 | | |
1250 | | /* FDSelect */ |
1251 | 0 | if (acc.fdSelect != &Null (CFF2FDSelect)) |
1252 | 0 | { |
1253 | 0 | if (unlikely (!hb_plan_subset_cff_fdselect (plan, |
1254 | 0 | orig_fdcount, |
1255 | 0 | *(const FDSelect *)acc.fdSelect, |
1256 | 0 | subset_fdcount, |
1257 | 0 | subset_fdselect_size, |
1258 | 0 | subset_fdselect_format, |
1259 | 0 | subset_fdselect_ranges, |
1260 | 0 | fdmap))) |
1261 | 0 | return false; |
1262 | 0 | } |
1263 | 0 | else |
1264 | 0 | fdmap.identity (1); |
1265 | | |
1266 | 0 | return true; |
1267 | 0 | } |
1268 | | |
1269 | | cff2_sub_table_info_t info; |
1270 | | |
1271 | | unsigned int num_glyphs; |
1272 | | unsigned int orig_fdcount = 0; |
1273 | | unsigned int subset_fdcount = 1; |
1274 | | unsigned int subset_fdselect_size = 0; |
1275 | | unsigned int subset_fdselect_format = 0; |
1276 | | bool pinned = false; |
1277 | | hb_vector_t<code_pair_t> subset_fdselect_ranges; |
1278 | | |
1279 | | hb_inc_bimap_t fdmap; |
1280 | | |
1281 | | str_buff_vec_t subset_charstrings; |
1282 | | str_buff_vec_t subset_globalsubrs; |
1283 | | hb_vector_t<str_buff_vec_t> subset_localsubrs; |
1284 | | |
1285 | | bool drop_hints = false; |
1286 | | bool desubroutinize = false; |
1287 | | |
1288 | | bool partial_instancing = false; |
1289 | | cff2_instancing_plan_t instancing; |
1290 | | const hb_vector_t<hb_tag_t> *axis_tags = nullptr; |
1291 | | |
1292 | | unsigned min_charstrings_off_size = 0; |
1293 | | |
1294 | | hb_array_t<int> normalized_coords; // For instantiation |
1295 | | head_maxp_info_t head_maxp_info; // For FontBBox |
1296 | | const hb_hashmap_t<hb_codepoint_t, hb_pair_t<unsigned, int>> *hmtx_map; // For widths |
1297 | | |
1298 | | // Width optimization results (for CFF1 conversion) |
1299 | | unsigned default_width = 0; |
1300 | | unsigned nominal_width = 0; |
1301 | | |
1302 | | // Command capture for specialization (CFF2→CFF1 conversion) |
1303 | | bool capture_commands = false; |
1304 | | hb_vector_t<hb_vector_t<cs_command_t>> charstring_commands; |
1305 | | }; |
1306 | | } // namespace OT |
1307 | | |
1308 | | /* |
1309 | | * CFF2 to CFF1 Converter Implementation |
1310 | | */ |
1311 | | |
1312 | | #include "hb-cff-width-optimizer.hh" |
1313 | | #include "hb-cff-specializer.hh" |
1314 | | |
1315 | | /* Serialize charstrings using CFF1 format with widths */ |
1316 | | static bool |
1317 | | _serialize_cff1_charstrings (hb_serialize_context_t *c, |
1318 | | OT::cff2_subset_plan &plan, |
1319 | | unsigned default_width, |
1320 | | unsigned nominal_width) |
1321 | 0 | { |
1322 | 0 | c->push (); |
1323 | | |
1324 | | // CFF1 requires: |
1325 | | // 1. Width at the beginning (if != defaultWidthX) |
1326 | | // 2. endchar at the end |
1327 | 0 | str_buff_vec_t cff1_charstrings; |
1328 | 0 | if (unlikely (!cff1_charstrings.resize (plan.subset_charstrings.length))) |
1329 | 0 | { |
1330 | 0 | c->pop_discard (); |
1331 | 0 | return false; |
1332 | 0 | } |
1333 | | |
1334 | 0 | for (unsigned i = 0; i < plan.subset_charstrings.length; i++) |
1335 | 0 | { |
1336 | | // Get width for this glyph from hmtx_map |
1337 | 0 | unsigned width = 0; |
1338 | 0 | if (plan.hmtx_map->has (i)) |
1339 | 0 | width = plan.hmtx_map->get (i).first; |
1340 | | |
1341 | | // Encode width if different from default |
1342 | 0 | str_encoder_t encoder (cff1_charstrings[i]); |
1343 | 0 | if (width != default_width) |
1344 | 0 | { |
1345 | 0 | int delta = (int) width - (int) nominal_width; |
1346 | 0 | encoder.encode_int (delta); |
1347 | 0 | } |
1348 | | |
1349 | | // Use specialized commands if available, otherwise use binary |
1350 | 0 | if (plan.capture_commands && i < plan.charstring_commands.length && |
1351 | 0 | plan.charstring_commands[i].length > 0) |
1352 | 0 | { |
1353 | | // Specialize and encode commands |
1354 | 0 | auto &commands = plan.charstring_commands[i]; |
1355 | 0 | CFF::specialize_commands (commands, 48); /* maxstack=48 for CFF1 */ |
1356 | 0 | if (unlikely (!CFF::encode_commands (commands, cff1_charstrings[i]))) |
1357 | 0 | { |
1358 | 0 | c->pop_discard (); |
1359 | 0 | return false; |
1360 | 0 | } |
1361 | 0 | } |
1362 | 0 | else |
1363 | 0 | { |
1364 | | // Use binary CharString |
1365 | 0 | const str_buff_t &cs = plan.subset_charstrings[i]; |
1366 | 0 | for (unsigned j = 0; j < cs.length; j++) |
1367 | 0 | cff1_charstrings[i].push (cs[j]); |
1368 | 0 | } |
1369 | | |
1370 | | // Check if it already ends with endchar (0x0e) or return (0x0b) |
1371 | 0 | if (cff1_charstrings[i].length == 0 || |
1372 | 0 | (cff1_charstrings[i].tail () != 0x0e && cff1_charstrings[i].tail () != 0x0b)) |
1373 | 0 | { |
1374 | | // Append endchar operator |
1375 | 0 | if (unlikely (!cff1_charstrings[i].push_or_fail (0x0e))) |
1376 | 0 | { |
1377 | 0 | c->pop_discard (); |
1378 | 0 | return false; |
1379 | 0 | } |
1380 | 0 | } |
1381 | 0 | } |
1382 | | |
1383 | 0 | unsigned data_size = 0; |
1384 | 0 | unsigned total_size = CFF1CharStrings::total_size (cff1_charstrings, &data_size); |
1385 | 0 | if (unlikely (!c->start_zerocopy (total_size))) |
1386 | 0 | { |
1387 | 0 | c->pop_discard (); |
1388 | 0 | return false; |
1389 | 0 | } |
1390 | | |
1391 | 0 | auto *cs = c->start_embed<CFF1CharStrings> (); |
1392 | 0 | if (unlikely (!cs->serialize (c, cff1_charstrings))) |
1393 | 0 | { |
1394 | 0 | c->pop_discard (); |
1395 | 0 | return false; |
1396 | 0 | } |
1397 | | |
1398 | 0 | plan.info.char_strings_link = c->pop_pack (false); |
1399 | 0 | return true; |
1400 | 0 | } |
1401 | | |
1402 | | /* Serialize CID Charset (format 2 range: gid 0-N -> cid 0-N) */ |
1403 | | static bool |
1404 | | _serialize_cff1_charset (hb_serialize_context_t *c, |
1405 | | unsigned int num_glyphs, |
1406 | | objidx_t &charset_link) |
1407 | 0 | { |
1408 | | // For CID fonts, create a simple identity charset |
1409 | | // Format 2: one range covering all glyphs (except .notdef) |
1410 | 0 | c->push (); |
1411 | |
|
1412 | 0 | auto *charset = c->start_embed<Charset> (); |
1413 | 0 | if (unlikely (!charset)) |
1414 | 0 | { |
1415 | 0 | c->pop_discard (); |
1416 | 0 | return false; |
1417 | 0 | } |
1418 | | |
1419 | | // Create a single range for CID 1 to num_glyphs-1 |
1420 | 0 | hb_vector_t<code_pair_t> ranges; |
1421 | 0 | if (num_glyphs > 1) |
1422 | 0 | { |
1423 | 0 | code_pair_t range; |
1424 | 0 | range.code = 1; // first CID |
1425 | 0 | range.glyph = num_glyphs - 2; // nLeft (covers glyphs 1 to num_glyphs-1) |
1426 | 0 | ranges.push (range); |
1427 | 0 | } |
1428 | |
|
1429 | 0 | if (unlikely (!charset->serialize (c, 2, num_glyphs, ranges))) |
1430 | 0 | { |
1431 | 0 | c->pop_discard (); |
1432 | 0 | return false; |
1433 | 0 | } |
1434 | | |
1435 | 0 | charset_link = c->pop_pack (); |
1436 | 0 | return true; |
1437 | 0 | } |
1438 | | |
1439 | | /* CFF2 to CFF1 serialization */ |
1440 | | namespace CFF { |
1441 | | |
1442 | | bool |
1443 | | serialize_cff2_to_cff1 (hb_serialize_context_t *c, |
1444 | | OT::cff2_subset_plan &plan, |
1445 | | const cff2_top_dict_values_t &cff2_topDict, |
1446 | | const OT::cff2::accelerator_subset_t &acc) |
1447 | 0 | { |
1448 | 0 | TRACE_SERIALIZE (this); |
1449 | | |
1450 | | /* |
1451 | | * CFF1 Serialization Order (reverse, as HarfBuzz packs from end): |
1452 | | * 1. CharStrings |
1453 | | * 2. Private DICs & Local Subrs |
1454 | | * 3. FDArray |
1455 | | * 4. FDSelect |
1456 | | * 5. Charset |
1457 | | * 6. Global Subrs |
1458 | | * 7. String INDEX |
1459 | | * 8. Top DICT INDEX |
1460 | | * 9. Name INDEX |
1461 | | * 10. Header |
1462 | | */ |
1463 | | |
1464 | | // 0. Optimize width encoding (for all FDs) |
1465 | 0 | { |
1466 | | // Collect widths from hmtx_map |
1467 | 0 | hb_vector_t<unsigned> widths; |
1468 | 0 | widths.alloc (plan.num_glyphs); |
1469 | |
|
1470 | 0 | for (unsigned gid = 0; gid < plan.num_glyphs; gid++) |
1471 | 0 | { |
1472 | 0 | unsigned width = 0; |
1473 | 0 | if (plan.hmtx_map->has (gid)) |
1474 | 0 | width = plan.hmtx_map->get (gid).first; |
1475 | 0 | widths.push (width); |
1476 | 0 | } |
1477 | | |
1478 | | // Optimize defaultWidthX and nominalWidthX |
1479 | 0 | CFF::optimize_widths (widths, plan.default_width, plan.nominal_width); |
1480 | 0 | } |
1481 | | |
1482 | | // 1. CharStrings (with widths prepended) |
1483 | 0 | if (!_serialize_cff1_charstrings (c, plan, plan.default_width, plan.nominal_width)) |
1484 | 0 | return_trace (false); |
1485 | | |
1486 | | // 2. Private DICs & Local Subrs (same as CFF2) |
1487 | 0 | hb_vector_t<table_info_t> private_dict_infos; |
1488 | 0 | if (unlikely (!private_dict_infos.resize (plan.subset_fdcount))) |
1489 | 0 | return_trace (false); |
1490 | | |
1491 | 0 | for (int i = (int)acc.privateDicts.length; --i >= 0;) |
1492 | 0 | { |
1493 | 0 | if (plan.fdmap.has (i)) |
1494 | 0 | { |
1495 | 0 | objidx_t subrs_link = 0; |
1496 | |
|
1497 | 0 | if (plan.subset_localsubrs[i].length > 0) |
1498 | 0 | { |
1499 | 0 | auto *dest = c->push<CFF1Subrs> (); |
1500 | 0 | if (likely (dest->serialize (c, plan.subset_localsubrs[i]))) |
1501 | 0 | subrs_link = c->pop_pack (false); |
1502 | 0 | else |
1503 | 0 | { |
1504 | 0 | c->pop_discard (); |
1505 | 0 | return_trace (false); |
1506 | 0 | } |
1507 | 0 | } |
1508 | | |
1509 | 0 | auto *pd = c->push<PrivateDict> (); |
1510 | | // Use the CFF2 Private DICT serializer which instantiates blends when pinned=true |
1511 | 0 | cff2_private_dict_op_serializer_t privSzr (plan.desubroutinize, plan.drop_hints, plan.pinned, |
1512 | 0 | acc.varStore, plan.normalized_coords); |
1513 | 0 | if (likely (pd->serialize (c, acc.privateDicts[i], privSzr, subrs_link))) |
1514 | 0 | { |
1515 | | // Add defaultWidthX and nominalWidthX for CFF1 |
1516 | 0 | str_buff_t width_ops; |
1517 | 0 | str_encoder_t encoder (width_ops); |
1518 | 0 | encoder.encode_int (plan.default_width); |
1519 | 0 | encoder.encode_op (OpCode_defaultWidthX); |
1520 | 0 | encoder.encode_int (plan.nominal_width); |
1521 | 0 | encoder.encode_op (OpCode_nominalWidthX); |
1522 | |
|
1523 | 0 | if (!encoder.in_error () && c->embed (width_ops.as_bytes ().arrayZ, width_ops.length)) |
1524 | 0 | { |
1525 | 0 | unsigned fd = plan.fdmap[i]; |
1526 | 0 | private_dict_infos[fd].size = c->length (); |
1527 | 0 | private_dict_infos[fd].link = c->pop_pack (); |
1528 | 0 | } |
1529 | 0 | else |
1530 | 0 | { |
1531 | 0 | c->pop_discard (); |
1532 | 0 | return_trace (false); |
1533 | 0 | } |
1534 | 0 | } |
1535 | 0 | else |
1536 | 0 | { |
1537 | 0 | c->pop_discard (); |
1538 | 0 | return_trace (false); |
1539 | 0 | } |
1540 | 0 | } |
1541 | 0 | } |
1542 | | |
1543 | | // 3. FDArray - serialize CFF2 font dicts as CFF1 |
1544 | 0 | { |
1545 | 0 | auto *fda = c->push<FDArray<HBUINT16>> (); |
1546 | 0 | cff_font_dict_op_serializer_t fontSzr; |
1547 | 0 | auto it = |
1548 | 0 | + hb_zip (+ hb_iter (acc.fontDicts) |
1549 | 0 | | hb_filter ([&] (const cff2_font_dict_values_t &_) |
1550 | 0 | { return plan.fdmap.has (&_ - &acc.fontDicts[0]); }), |
1551 | 0 | hb_iter (private_dict_infos)) |
1552 | 0 | ; |
1553 | | // Explicitly specify template parameters: DICTVAL, INFO |
1554 | 0 | bool success = fda->serialize<cff2_font_dict_values_t, table_info_t> (c, it, fontSzr); |
1555 | 0 | if (success) |
1556 | 0 | plan.info.fd_array_link = c->pop_pack (false); |
1557 | 0 | else |
1558 | 0 | { |
1559 | 0 | c->pop_discard (); |
1560 | 0 | return_trace (false); |
1561 | 0 | } |
1562 | 0 | } |
1563 | | |
1564 | | // 4. FDSelect (required in CFF1 CID-keyed fonts) |
1565 | | // CFF1 requires FDSelect for all CID-keyed fonts, even with just one FD |
1566 | | // CFF2 makes it optional when there's only one FD |
1567 | 0 | if (acc.fdSelect != &Null (CFF2FDSelect)) |
1568 | 0 | { |
1569 | 0 | c->push (); |
1570 | 0 | if (likely (hb_serialize_cff_fdselect (c, plan.num_glyphs, |
1571 | 0 | *(const FDSelect *)acc.fdSelect, |
1572 | 0 | plan.orig_fdcount, |
1573 | 0 | plan.subset_fdselect_format, |
1574 | 0 | plan.subset_fdselect_size, |
1575 | 0 | plan.subset_fdselect_ranges))) |
1576 | 0 | plan.info.fd_select.link = c->pop_pack (); |
1577 | 0 | else |
1578 | 0 | { |
1579 | 0 | c->pop_discard (); |
1580 | 0 | return_trace (false); |
1581 | 0 | } |
1582 | 0 | } |
1583 | 0 | else |
1584 | 0 | { |
1585 | | // Create a range-based FDSelect3 mapping all glyphs to FD 0 |
1586 | | // Format: format(1) + nRanges(2) + range(3) + sentinel(2) = 8 bytes |
1587 | 0 | c->push (); |
1588 | | |
1589 | | // Format byte |
1590 | 0 | HBUINT8 format; |
1591 | 0 | format = 3; |
1592 | 0 | if (unlikely (!c->embed (format))) |
1593 | 0 | { |
1594 | 0 | c->pop_discard (); |
1595 | 0 | return_trace (false); |
1596 | 0 | } |
1597 | | |
1598 | | // nRanges |
1599 | 0 | HBUINT16 nRanges; |
1600 | 0 | nRanges = 1; |
1601 | 0 | if (unlikely (!c->embed (nRanges))) |
1602 | 0 | { |
1603 | 0 | c->pop_discard (); |
1604 | 0 | return_trace (false); |
1605 | 0 | } |
1606 | | |
1607 | | // Single range: {first: 0, fd: 0} |
1608 | 0 | FDSelect3_Range range; |
1609 | 0 | range.first = 0; |
1610 | 0 | range.fd = 0; |
1611 | 0 | if (unlikely (!c->embed (range))) |
1612 | 0 | { |
1613 | 0 | c->pop_discard (); |
1614 | 0 | return_trace (false); |
1615 | 0 | } |
1616 | | |
1617 | | // Sentinel (number of glyphs) |
1618 | 0 | HBUINT16 sentinel; |
1619 | 0 | sentinel = plan.num_glyphs; |
1620 | 0 | if (unlikely (!c->embed (sentinel))) |
1621 | 0 | { |
1622 | 0 | c->pop_discard (); |
1623 | 0 | return_trace (false); |
1624 | 0 | } |
1625 | | |
1626 | 0 | plan.info.fd_select.link = c->pop_pack (); |
1627 | 0 | } |
1628 | | |
1629 | | // 5. Charset (CID charset for identity mapping) |
1630 | 0 | objidx_t charset_link; |
1631 | 0 | if (!_serialize_cff1_charset (c, plan.num_glyphs, charset_link)) |
1632 | 0 | return_trace (false); |
1633 | | |
1634 | | // 6. Global Subrs |
1635 | 0 | { |
1636 | 0 | auto *dest = c->push<CFF1Subrs> (); |
1637 | 0 | if (likely (dest->serialize (c, plan.subset_globalsubrs))) |
1638 | 0 | c->pop_pack (false); |
1639 | 0 | else |
1640 | 0 | { |
1641 | 0 | c->pop_discard (); |
1642 | 0 | return_trace (false); |
1643 | 0 | } |
1644 | 0 | } |
1645 | | |
1646 | | // 7. String INDEX - Add "Adobe" and "Identity" for ROS operator |
1647 | 0 | { |
1648 | 0 | const char *adobe_str = "Adobe"; |
1649 | 0 | const char *identity_str = "Identity"; |
1650 | 0 | unsigned adobe_len = 5; // strlen("Adobe") |
1651 | 0 | unsigned identity_len = 8; // strlen("Identity") |
1652 | | |
1653 | | // Build strings array |
1654 | 0 | hb_vector_t<hb_ubytes_t> strings; |
1655 | 0 | strings.alloc (2); |
1656 | 0 | strings.push (hb_ubytes_t ((const unsigned char *) adobe_str, adobe_len)); |
1657 | 0 | strings.push (hb_ubytes_t ((const unsigned char *) identity_str, identity_len)); |
1658 | | |
1659 | | // Serialize as CFF INDEX |
1660 | 0 | auto *dest = c->push<CFF1Index> (); |
1661 | 0 | if (likely (dest->serialize (c, strings))) |
1662 | 0 | c->pop_pack (false); |
1663 | 0 | else |
1664 | 0 | { |
1665 | 0 | c->pop_discard (); |
1666 | 0 | return_trace (false); |
1667 | 0 | } |
1668 | 0 | } |
1669 | | |
1670 | | // 8. CFF Header |
1671 | 0 | OT::cff1 *cff = c->allocate_min<OT::cff1> (); |
1672 | 0 | if (unlikely (!cff)) return_trace (false); |
1673 | | |
1674 | | /* header */ |
1675 | 0 | cff->version.major = 0x01; |
1676 | 0 | cff->version.minor = 0x00; |
1677 | 0 | cff->nameIndex = cff->min_size; |
1678 | 0 | cff->offSize = 4; /* unused? */ |
1679 | | |
1680 | | // 9. Name INDEX (single entry) |
1681 | 0 | { |
1682 | 0 | unsigned name_len = strlen (CFF1_DEFAULT_FONT_NAME); |
1683 | |
|
1684 | 0 | CFF1Index *idx = c->start_embed<CFF1Index> (); |
1685 | 0 | if (unlikely (!idx)) return_trace (false); |
1686 | | |
1687 | 0 | if (unlikely (!idx->serialize_header (c, hb_iter (&name_len, 1), name_len))) |
1688 | 0 | return_trace (false); |
1689 | | |
1690 | 0 | if (unlikely (!c->embed (CFF1_DEFAULT_FONT_NAME, name_len))) |
1691 | 0 | return_trace (false); |
1692 | 0 | } |
1693 | | |
1694 | | // 10. Top DICT INDEX |
1695 | 0 | { |
1696 | | // Serialize the Top DICT data first |
1697 | 0 | c->push<TopDict> (); |
1698 | 0 | cff1_from_cff2_top_dict_op_serializer_t topSzr; |
1699 | | |
1700 | | // Serialize ROS first |
1701 | 0 | if (unlikely (!topSzr.serialize_ros (c))) |
1702 | 0 | { |
1703 | 0 | c->pop_discard (); |
1704 | 0 | return_trace (false); |
1705 | 0 | } |
1706 | | |
1707 | | // Serialize FontBBox from head table |
1708 | 0 | { |
1709 | 0 | str_buff_t bbox_buff; |
1710 | 0 | str_encoder_t encoder (bbox_buff); |
1711 | |
|
1712 | 0 | encoder.encode_int (plan.head_maxp_info.xMin); |
1713 | 0 | encoder.encode_int (plan.head_maxp_info.yMin); |
1714 | 0 | encoder.encode_int (plan.head_maxp_info.xMax); |
1715 | 0 | encoder.encode_int (plan.head_maxp_info.yMax); |
1716 | 0 | encoder.encode_op (OpCode_FontBBox); |
1717 | |
|
1718 | 0 | if (encoder.in_error () || !c->embed (bbox_buff.as_bytes ().arrayZ, bbox_buff.length)) |
1719 | 0 | { |
1720 | 0 | c->pop_discard (); |
1721 | 0 | return_trace (false); |
1722 | 0 | } |
1723 | 0 | } |
1724 | | |
1725 | | // Serialize charset operator |
1726 | 0 | if (charset_link && unlikely (!FontDict::serialize_link4_op (c, OpCode_charset, charset_link, whence_t::Absolute))) |
1727 | 0 | { |
1728 | 0 | c->pop_discard (); |
1729 | 0 | return_trace (false); |
1730 | 0 | } |
1731 | | |
1732 | | // Serialize FDSelect operator (required for CID-keyed CFF1 fonts) |
1733 | 0 | if (plan.info.fd_select.link && unlikely (!FontDict::serialize_link4_op (c, OpCode_FDSelect, plan.info.fd_select.link, whence_t::Absolute))) |
1734 | 0 | { |
1735 | 0 | c->pop_discard (); |
1736 | 0 | return_trace (false); |
1737 | 0 | } |
1738 | | |
1739 | | // Serialize FDArray operator (required for CID-keyed CFF1 fonts) |
1740 | 0 | if (plan.info.fd_array_link && unlikely (!FontDict::serialize_link4_op (c, OpCode_FDArray, plan.info.fd_array_link, whence_t::Absolute))) |
1741 | 0 | { |
1742 | 0 | c->pop_discard (); |
1743 | 0 | return_trace (false); |
1744 | 0 | } |
1745 | | |
1746 | | // Serialize other operators from CFF2 TopDict |
1747 | 0 | for (const auto &opstr : cff2_topDict.values) |
1748 | 0 | { |
1749 | 0 | if (unlikely (!topSzr.serialize (c, opstr, plan.info))) |
1750 | 0 | { |
1751 | 0 | c->pop_discard (); |
1752 | 0 | return_trace (false); |
1753 | 0 | } |
1754 | 0 | } |
1755 | | |
1756 | 0 | unsigned top_size = c->length (); |
1757 | 0 | c->pop_pack (false); |
1758 | | |
1759 | | // Serialize INDEX header |
1760 | 0 | auto *dest = c->start_embed<CFF1Index> (); |
1761 | 0 | if (unlikely (!dest->serialize_header (c, hb_iter (&top_size, 1), top_size))) |
1762 | 0 | return_trace (false); |
1763 | 0 | } |
1764 | | |
1765 | 0 | return_trace (true); |
1766 | 0 | } |
1767 | | |
1768 | | } /* namespace CFF */ |
1769 | | |
1770 | | static bool _serialize_cff2_charstrings (hb_serialize_context_t *c, |
1771 | | cff2_subset_plan &plan, |
1772 | | const OT::cff2::accelerator_subset_t &acc) |
1773 | 0 | { |
1774 | 0 | c->push (); |
1775 | |
|
1776 | 0 | unsigned data_size = 0; |
1777 | 0 | unsigned total_size = CFF2CharStrings::total_size (plan.subset_charstrings, &data_size, plan.min_charstrings_off_size); |
1778 | 0 | if (unlikely (!c->start_zerocopy (total_size))) |
1779 | 0 | return false; |
1780 | | |
1781 | 0 | auto *cs = c->start_embed<CFF2CharStrings> (); |
1782 | 0 | if (unlikely (!cs->serialize (c, plan.subset_charstrings, &data_size, plan.min_charstrings_off_size))) |
1783 | 0 | { |
1784 | 0 | c->pop_discard (); |
1785 | 0 | return false; |
1786 | 0 | } |
1787 | | |
1788 | 0 | plan.info.char_strings_link = c->pop_pack (false); |
1789 | 0 | return true; |
1790 | 0 | } |
1791 | | |
1792 | | bool |
1793 | | OT::cff2::accelerator_subset_t::serialize (hb_serialize_context_t *c, |
1794 | | struct cff2_subset_plan &plan, |
1795 | | hb_array_t<int> normalized_coords) const |
1796 | 0 | { |
1797 | | /* push charstrings onto the object stack first which will ensure it packs as the last |
1798 | | object in the table. Keeping the chastrings last satisfies the requirements for patching |
1799 | | via IFTB. If this ordering needs to be changed in the future, charstrings should be left |
1800 | | at the end whenever HB_SUBSET_FLAGS_ITFB_REQUIREMENTS is enabled. */ |
1801 | 0 | if (!_serialize_cff2_charstrings(c, plan, *this)) |
1802 | 0 | return false; |
1803 | | |
1804 | | /* private dicts & local subrs */ |
1805 | 0 | hb_vector_t<table_info_t> private_dict_infos; |
1806 | 0 | if (unlikely (!private_dict_infos.resize (plan.subset_fdcount))) return false; |
1807 | | |
1808 | 0 | for (int i = (int)privateDicts.length; --i >= 0 ;) |
1809 | 0 | { |
1810 | 0 | if (plan.fdmap.has (i)) |
1811 | 0 | { |
1812 | 0 | objidx_t subrs_link = 0; |
1813 | |
|
1814 | 0 | if (plan.subset_localsubrs[i].length > 0) |
1815 | 0 | { |
1816 | 0 | auto *dest = c->push <CFF2Subrs> (); |
1817 | 0 | if (likely (dest->serialize (c, plan.subset_localsubrs[i]))) |
1818 | 0 | subrs_link = c->pop_pack (false); |
1819 | 0 | else |
1820 | 0 | { |
1821 | 0 | c->pop_discard (); |
1822 | 0 | return false; |
1823 | 0 | } |
1824 | 0 | } |
1825 | 0 | auto *pd = c->push<PrivateDict> (); |
1826 | 0 | cff2_private_dict_op_serializer_t privSzr (plan.desubroutinize, plan.drop_hints, plan.pinned, |
1827 | 0 | varStore, normalized_coords, |
1828 | 0 | plan.partial_instancing ? &plan.instancing : nullptr); |
1829 | 0 | if (likely (pd->serialize (c, privateDicts[i], privSzr, subrs_link))) |
1830 | 0 | { |
1831 | 0 | unsigned fd = plan.fdmap[i]; |
1832 | 0 | private_dict_infos[fd].size = c->length (); |
1833 | 0 | private_dict_infos[fd].link = c->pop_pack (); |
1834 | 0 | } |
1835 | 0 | else |
1836 | 0 | { |
1837 | 0 | c->pop_discard (); |
1838 | 0 | return false; |
1839 | 0 | } |
1840 | 0 | } |
1841 | 0 | } |
1842 | | |
1843 | | /* FDSelect */ |
1844 | 0 | if (fdSelect != &Null (CFF2FDSelect)) |
1845 | 0 | { |
1846 | 0 | c->push (); |
1847 | 0 | if (likely (hb_serialize_cff_fdselect (c, plan.num_glyphs, *(const FDSelect *)fdSelect, |
1848 | 0 | plan.orig_fdcount, |
1849 | 0 | plan.subset_fdselect_format, plan.subset_fdselect_size, |
1850 | 0 | plan.subset_fdselect_ranges))) |
1851 | 0 | plan.info.fd_select.link = c->pop_pack (); |
1852 | 0 | else |
1853 | 0 | { |
1854 | 0 | c->pop_discard (); |
1855 | 0 | return false; |
1856 | 0 | } |
1857 | 0 | } |
1858 | | |
1859 | | /* FDArray (FD Index) */ |
1860 | 0 | { |
1861 | 0 | auto *fda = c->push<CFF2FDArray> (); |
1862 | 0 | cff_font_dict_op_serializer_t fontSzr; |
1863 | 0 | auto it = |
1864 | 0 | + hb_zip (+ hb_iter (fontDicts) |
1865 | 0 | | hb_filter ([&] (const cff2_font_dict_values_t &_) |
1866 | 0 | { return plan.fdmap.has (&_ - &fontDicts[0]); }), |
1867 | 0 | hb_iter (private_dict_infos)) |
1868 | 0 | ; |
1869 | 0 | if (unlikely (!fda->serialize (c, it, fontSzr))) |
1870 | 0 | { |
1871 | 0 | c->pop_discard (); |
1872 | 0 | return false; |
1873 | 0 | } |
1874 | 0 | plan.info.fd_array_link = c->pop_pack (false); |
1875 | 0 | } |
1876 | | |
1877 | | /* variation store */ |
1878 | 0 | if (varStore != &Null (CFF2ItemVariationStore) && |
1879 | 0 | !plan.pinned) |
1880 | 0 | { |
1881 | 0 | if (plan.partial_instancing) |
1882 | 0 | { |
1883 | 0 | if (plan.instancing.has_variations ()) |
1884 | 0 | { |
1885 | 0 | c->push (); |
1886 | 0 | if (unlikely (!plan.instancing.serialize_var_store (c, *plan.axis_tags))) |
1887 | 0 | { |
1888 | 0 | c->pop_discard (); |
1889 | 0 | return false; |
1890 | 0 | } |
1891 | 0 | plan.info.var_store_link = c->pop_pack (false); |
1892 | 0 | } |
1893 | | /* else: the store emptied; the vstore op gets dropped along with it. */ |
1894 | 0 | } |
1895 | 0 | else |
1896 | 0 | { |
1897 | 0 | auto *dest = c->push<CFF2ItemVariationStore> (); |
1898 | 0 | if (unlikely (!dest->serialize (c, varStore))) |
1899 | 0 | { |
1900 | 0 | c->pop_discard (); |
1901 | 0 | return false; |
1902 | 0 | } |
1903 | 0 | plan.info.var_store_link = c->pop_pack (false); |
1904 | 0 | } |
1905 | 0 | } |
1906 | | |
1907 | 0 | OT::cff2 *cff2 = c->allocate_min<OT::cff2> (); |
1908 | 0 | if (unlikely (!cff2)) return false; |
1909 | | |
1910 | | /* header */ |
1911 | 0 | cff2->version.major = 0x02; |
1912 | 0 | cff2->version.minor = 0x00; |
1913 | 0 | cff2->topDict = OT::cff2::static_size; |
1914 | | |
1915 | | /* top dict */ |
1916 | 0 | { |
1917 | 0 | TopDict &dict = cff2 + cff2->topDict; |
1918 | 0 | cff2_top_dict_op_serializer_t topSzr; |
1919 | 0 | if (unlikely (!dict.serialize (c, topDict, topSzr, plan.info))) return false; |
1920 | 0 | cff2->topDictSize = c->head - (const char *)&dict; |
1921 | 0 | } |
1922 | | |
1923 | | /* global subrs */ |
1924 | 0 | { |
1925 | 0 | auto *dest = c->start_embed <CFF2Subrs> (); |
1926 | 0 | return dest->serialize (c, plan.subset_globalsubrs); |
1927 | 0 | } |
1928 | 0 | } |
1929 | | |
1930 | | bool |
1931 | | OT::cff2::accelerator_subset_t::subset (hb_subset_context_t *c) const |
1932 | 0 | { |
1933 | 0 | cff2_subset_plan cff2_plan; |
1934 | |
|
1935 | 0 | if (unlikely (!cff2_plan.create (*this, c->plan))) return false; |
1936 | | |
1937 | | // If instantiating (pinned) and downgrade flag is set, convert to CFF1 |
1938 | 0 | if (cff2_plan.pinned && (c->plan->flags & HB_SUBSET_FLAGS_DOWNGRADE_CFF2)) |
1939 | 0 | { |
1940 | | // Serialize CFF1 to the subsetter's serializer |
1941 | | // If we run out of room, returning true will cause subsetter to retry with larger buffer |
1942 | 0 | bool result = CFF::serialize_cff2_to_cff1 (c->serializer, cff2_plan, topDict, *this); |
1943 | |
|
1944 | 0 | if (c->serializer->ran_out_of_room ()) |
1945 | 0 | return true; // Subsetter will retry with larger buffer |
1946 | | |
1947 | 0 | if (result && !c->serializer->in_error ()) |
1948 | 0 | { |
1949 | | // Success - end serialization to resolve links |
1950 | 0 | c->serializer->end_serialize (); |
1951 | | |
1952 | | // Copy the serialized CFF1 data and add as CFF table |
1953 | 0 | hb_blob_t *cff_blob = c->serializer->copy_blob (); |
1954 | 0 | if (cff_blob) |
1955 | 0 | { |
1956 | 0 | c->plan->add_table (HB_TAG('C','F','F',' '), cff_blob); |
1957 | 0 | hb_blob_destroy (cff_blob); |
1958 | | |
1959 | | // Return false to signal CFF2 table is not needed |
1960 | 0 | return false; |
1961 | 0 | } |
1962 | 0 | } |
1963 | | |
1964 | | // Conversion failed - don't fall back, fail hard for debugging |
1965 | 0 | return false; |
1966 | 0 | } |
1967 | | |
1968 | 0 | return serialize (c->serializer, cff2_plan, |
1969 | 0 | c->plan->normalized_coords.as_array ()); |
1970 | 0 | } |
1971 | | |
1972 | | #endif |