hb-subset-cff2.cc (19566B)
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-set.h" 34 #include "hb-subset-plan.hh" 35 #include "hb-subset-cff-common.hh" 36 #include "hb-cff2-interp-cs.hh" 37 38 using namespace CFF; 39 40 struct cff2_sub_table_info_t : cff_sub_table_info_t 41 { 42 cff2_sub_table_info_t () 43 : cff_sub_table_info_t (), 44 var_store_link (0) 45 {} 46 47 objidx_t var_store_link; 48 }; 49 50 struct cff2_top_dict_op_serializer_t : cff_top_dict_op_serializer_t<> 51 { 52 bool serialize (hb_serialize_context_t *c, 53 const op_str_t &opstr, 54 const cff2_sub_table_info_t &info) const 55 { 56 TRACE_SERIALIZE (this); 57 58 switch (opstr.op) 59 { 60 case OpCode_vstore: 61 if (info.var_store_link) 62 return_trace (FontDict::serialize_link4_op(c, opstr.op, info.var_store_link)); 63 else 64 return_trace (true); 65 66 default: 67 return_trace (cff_top_dict_op_serializer_t<>::serialize (c, opstr, info)); 68 } 69 } 70 }; 71 72 struct cff2_cs_opset_flatten_t : cff2_cs_opset_t<cff2_cs_opset_flatten_t, flatten_param_t, blend_arg_t> 73 { 74 static void flush_args_and_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) 75 { 76 switch (op) 77 { 78 case OpCode_return: 79 case OpCode_endchar: 80 /* dummy opcodes in CFF2. ignore */ 81 break; 82 83 case OpCode_hstem: 84 case OpCode_hstemhm: 85 case OpCode_vstem: 86 case OpCode_vstemhm: 87 case OpCode_hintmask: 88 case OpCode_cntrmask: 89 if (param.drop_hints) 90 { 91 env.clear_args (); 92 return; 93 } 94 HB_FALLTHROUGH; 95 96 default: 97 SUPER::flush_args_and_op (op, env, param); 98 break; 99 } 100 } 101 102 static void flush_args (cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) 103 { 104 for (unsigned int i = 0; i < env.argStack.get_count ();) 105 { 106 const blend_arg_t &arg = env.argStack[i]; 107 if (arg.blending ()) 108 { 109 if (unlikely (!((arg.numValues > 0) && (env.argStack.get_count () >= arg.numValues)))) 110 { 111 env.set_error (); 112 return; 113 } 114 flatten_blends (arg, i, env, param); 115 i += arg.numValues; 116 } 117 else 118 { 119 str_encoder_t encoder (param.flatStr); 120 encoder.encode_num_cs (arg); 121 i++; 122 } 123 } 124 SUPER::flush_args (env, param); 125 } 126 127 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) 128 { 129 /* flatten the default values */ 130 str_encoder_t encoder (param.flatStr); 131 for (unsigned int j = 0; j < arg.numValues; j++) 132 { 133 const blend_arg_t &arg1 = env.argStack[i + j]; 134 if (unlikely (!((arg1.blending () && (arg.numValues == arg1.numValues) && (arg1.valueIndex == j) && 135 (arg1.deltas.length == env.get_region_count ()))))) 136 { 137 env.set_error (); 138 return; 139 } 140 encoder.encode_num_cs (arg1); 141 } 142 /* flatten deltas for each value */ 143 for (unsigned int j = 0; j < arg.numValues; j++) 144 { 145 const blend_arg_t &arg1 = env.argStack[i + j]; 146 for (unsigned int k = 0; k < arg1.deltas.length; k++) 147 encoder.encode_num_cs (arg1.deltas[k]); 148 } 149 /* flatten the number of values followed by blend operator */ 150 encoder.encode_int (arg.numValues); 151 encoder.encode_op (OpCode_blendcs); 152 } 153 154 static void flush_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) 155 { 156 switch (op) 157 { 158 case OpCode_return: 159 case OpCode_endchar: 160 return; 161 default: 162 str_encoder_t encoder (param.flatStr); 163 encoder.encode_op (op); 164 } 165 } 166 167 static void flush_hintmask (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param) 168 { 169 SUPER::flush_hintmask (op, env, param); 170 if (!param.drop_hints) 171 { 172 str_encoder_t encoder (param.flatStr); 173 for (unsigned int i = 0; i < env.hintmask_size; i++) 174 encoder.encode_byte (env.str_ref[i]); 175 } 176 } 177 178 private: 179 typedef cff2_cs_opset_t<cff2_cs_opset_flatten_t, flatten_param_t, blend_arg_t> SUPER; 180 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; 181 }; 182 183 struct cff2_cs_opset_subr_subset_t : cff2_cs_opset_t<cff2_cs_opset_subr_subset_t, subr_subset_param_t, blend_arg_t> 184 { 185 static void process_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, subr_subset_param_t& param) 186 { 187 switch (op) { 188 189 case OpCode_return: 190 param.current_parsed_str->set_parsed (); 191 env.return_from_subr (); 192 param.set_current_str (env, false); 193 break; 194 195 case OpCode_endchar: 196 param.current_parsed_str->set_parsed (); 197 SUPER::process_op (op, env, param); 198 break; 199 200 case OpCode_callsubr: 201 process_call_subr (op, CSType_LocalSubr, env, param, env.localSubrs, param.local_closure); 202 break; 203 204 case OpCode_callgsubr: 205 process_call_subr (op, CSType_GlobalSubr, env, param, env.globalSubrs, param.global_closure); 206 break; 207 208 default: 209 SUPER::process_op (op, env, param); 210 param.current_parsed_str->add_op (op, env.str_ref); 211 break; 212 } 213 } 214 215 protected: 216 static void process_call_subr (op_code_t op, cs_type_t type, 217 cff2_cs_interp_env_t<blend_arg_t> &env, subr_subset_param_t& param, 218 cff2_biased_subrs_t& subrs, hb_set_t *closure) 219 { 220 byte_str_ref_t str_ref = env.str_ref; 221 env.call_subr (subrs, type); 222 param.current_parsed_str->add_call_op (op, str_ref, env.context.subr_num); 223 closure->add (env.context.subr_num); 224 param.set_current_str (env, true); 225 } 226 227 private: 228 typedef cff2_cs_opset_t<cff2_cs_opset_subr_subset_t, subr_subset_param_t, blend_arg_t> SUPER; 229 }; 230 231 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> 232 { 233 cff2_subr_subsetter_t (const OT::cff2::accelerator_subset_t &acc_, const hb_subset_plan_t *plan_) 234 : subr_subsetter_t (acc_, plan_) {} 235 236 static void complete_parsed_str (cff2_cs_interp_env_t<blend_arg_t> &env, subr_subset_param_t& param, parsed_cs_str_t &charstring) 237 { 238 /* vsindex is inserted at the beginning of the charstring as necessary */ 239 if (env.seen_vsindex ()) 240 { 241 number_t ivs; 242 ivs.set_int ((int)env.get_ivs ()); 243 charstring.set_prefix (ivs, OpCode_vsindexcs); 244 } 245 } 246 }; 247 248 struct cff2_private_blend_encoder_param_t 249 { 250 cff2_private_blend_encoder_param_t (hb_serialize_context_t *c, 251 const CFF2ItemVariationStore *varStore, 252 hb_array_t<int> normalized_coords) : 253 c (c), varStore (varStore), normalized_coords (normalized_coords) {} 254 255 void init () {} 256 257 void process_blend () 258 { 259 if (!seen_blend) 260 { 261 region_count = varStore->varStore.get_region_index_count (ivs); 262 scalars.resize_exact (region_count); 263 varStore->varStore.get_region_scalars (ivs, normalized_coords.arrayZ, normalized_coords.length, 264 &scalars[0], region_count); 265 seen_blend = true; 266 } 267 } 268 269 double blend_deltas (hb_array_t<const number_t> deltas) const 270 { 271 double v = 0; 272 if (likely (scalars.length == deltas.length)) 273 { 274 unsigned count = scalars.length; 275 for (unsigned i = 0; i < count; i++) 276 v += (double) scalars.arrayZ[i] * deltas.arrayZ[i].to_real (); 277 } 278 return v; 279 } 280 281 282 hb_serialize_context_t *c = nullptr; 283 bool seen_blend = false; 284 unsigned ivs = 0; 285 unsigned region_count = 0; 286 hb_vector_t<float> scalars; 287 const CFF2ItemVariationStore *varStore = nullptr; 288 hb_array_t<int> normalized_coords; 289 }; 290 291 struct cff2_private_dict_blend_opset_t : dict_opset_t 292 { 293 static void process_arg_blend (cff2_private_blend_encoder_param_t& param, 294 number_t &arg, 295 const hb_array_t<const number_t> blends, 296 unsigned n, unsigned i) 297 { 298 arg.set_int (round (arg.to_real () + param.blend_deltas (blends))); 299 } 300 301 static void process_blend (cff2_priv_dict_interp_env_t& env, cff2_private_blend_encoder_param_t& param) 302 { 303 unsigned int n, k; 304 305 param.process_blend (); 306 k = param.region_count; 307 n = env.argStack.pop_uint (); 308 /* copy the blend values into blend array of the default values */ 309 unsigned int start = env.argStack.get_count () - ((k+1) * n); 310 /* let an obvious error case fail, but note CFF2 spec doesn't forbid n==0 */ 311 if (unlikely (start > env.argStack.get_count ())) 312 { 313 env.set_error (); 314 return; 315 } 316 for (unsigned int i = 0; i < n; i++) 317 { 318 const hb_array_t<const number_t> blends = env.argStack.sub_array (start + n + (i * k), k); 319 process_arg_blend (param, env.argStack[start + i], blends, n, i); 320 } 321 322 /* pop off blend values leaving default values now adorned with blend values */ 323 env.argStack.pop (k * n); 324 } 325 326 static void process_op (op_code_t op, cff2_priv_dict_interp_env_t& env, cff2_private_blend_encoder_param_t& param) 327 { 328 switch (op) { 329 case OpCode_StdHW: 330 case OpCode_StdVW: 331 case OpCode_BlueScale: 332 case OpCode_BlueShift: 333 case OpCode_BlueFuzz: 334 case OpCode_ExpansionFactor: 335 case OpCode_LanguageGroup: 336 case OpCode_BlueValues: 337 case OpCode_OtherBlues: 338 case OpCode_FamilyBlues: 339 case OpCode_FamilyOtherBlues: 340 case OpCode_StemSnapH: 341 case OpCode_StemSnapV: 342 break; 343 case OpCode_vsindexdict: 344 env.process_vsindex (); 345 param.ivs = env.get_ivs (); 346 env.clear_args (); 347 return; 348 case OpCode_blenddict: 349 process_blend (env, param); 350 return; 351 352 default: 353 dict_opset_t::process_op (op, env); 354 if (!env.argStack.is_empty ()) return; 355 break; 356 } 357 358 if (unlikely (env.in_error ())) return; 359 360 // Write args then op 361 362 str_buff_t str; 363 str_encoder_t encoder (str); 364 365 unsigned count = env.argStack.get_count (); 366 for (unsigned i = 0; i < count; i++) 367 encoder.encode_num_tp (env.argStack[i]); 368 369 encoder.encode_op (op); 370 371 auto bytes = str.as_bytes (); 372 param.c->embed (&bytes, bytes.length); 373 374 env.clear_args (); 375 } 376 }; 377 378 struct cff2_private_dict_op_serializer_t : op_serializer_t 379 { 380 cff2_private_dict_op_serializer_t (bool desubroutinize_, bool drop_hints_, bool pinned_, 381 const CFF::CFF2ItemVariationStore* varStore_, 382 hb_array_t<int> normalized_coords_) 383 : desubroutinize (desubroutinize_), drop_hints (drop_hints_), pinned (pinned_), 384 varStore (varStore_), normalized_coords (normalized_coords_) {} 385 386 bool serialize (hb_serialize_context_t *c, 387 const op_str_t &opstr, 388 objidx_t subrs_link) const 389 { 390 TRACE_SERIALIZE (this); 391 392 if (drop_hints && dict_opset_t::is_hint_op (opstr.op)) 393 return_trace (true); 394 395 if (opstr.op == OpCode_Subrs) 396 { 397 if (desubroutinize || !subrs_link) 398 return_trace (true); 399 else 400 return_trace (FontDict::serialize_link2_op (c, opstr.op, subrs_link)); 401 } 402 403 if (pinned) 404 { 405 // Reinterpret opstr and process blends. 406 cff2_priv_dict_interp_env_t env {hb_ubytes_t (opstr.ptr, opstr.length)}; 407 cff2_private_blend_encoder_param_t param (c, varStore, normalized_coords); 408 dict_interpreter_t<cff2_private_dict_blend_opset_t, cff2_private_blend_encoder_param_t, cff2_priv_dict_interp_env_t> interp (env); 409 return_trace (interp.interpret (param)); 410 } 411 412 return_trace (copy_opstr (c, opstr)); 413 } 414 415 protected: 416 const bool desubroutinize; 417 const bool drop_hints; 418 const bool pinned; 419 const CFF::CFF2ItemVariationStore* varStore; 420 hb_array_t<int> normalized_coords; 421 }; 422 423 424 namespace OT { 425 struct cff2_subset_plan 426 { 427 bool create (const OT::cff2::accelerator_subset_t &acc, 428 hb_subset_plan_t *plan) 429 { 430 /* make sure notdef is first */ 431 hb_codepoint_t old_glyph; 432 if (!plan->old_gid_for_new_gid (0, &old_glyph) || (old_glyph != 0)) return false; 433 434 num_glyphs = plan->num_output_glyphs (); 435 orig_fdcount = acc.fdArray->count; 436 437 drop_hints = plan->flags & HB_SUBSET_FLAGS_NO_HINTING; 438 pinned = (bool) plan->normalized_coords; 439 desubroutinize = plan->flags & HB_SUBSET_FLAGS_DESUBROUTINIZE || 440 pinned; // For instancing we need this path 441 442 #ifdef HB_EXPERIMENTAL_API 443 min_charstrings_off_size = (plan->flags & HB_SUBSET_FLAGS_IFTB_REQUIREMENTS) ? 4 : 0; 444 #else 445 min_charstrings_off_size = 0; 446 #endif 447 448 if (desubroutinize) 449 { 450 /* Flatten global & local subrs */ 451 subr_flattener_t<const OT::cff2::accelerator_subset_t, cff2_cs_interp_env_t<blend_arg_t>, cff2_cs_opset_flatten_t> 452 flattener(acc, plan); 453 if (!flattener.flatten (subset_charstrings)) 454 return false; 455 } 456 else 457 { 458 cff2_subr_subsetter_t subr_subsetter (acc, plan); 459 460 /* Subset subrs: collect used subroutines, leaving all unused ones behind */ 461 if (!subr_subsetter.subset ()) 462 return false; 463 464 /* encode charstrings, global subrs, local subrs with new subroutine numbers */ 465 if (!subr_subsetter.encode_charstrings (subset_charstrings, !pinned)) 466 return false; 467 468 if (!subr_subsetter.encode_globalsubrs (subset_globalsubrs)) 469 return false; 470 471 /* local subrs */ 472 if (!subset_localsubrs.resize (orig_fdcount)) 473 return false; 474 for (unsigned int fd = 0; fd < orig_fdcount; fd++) 475 { 476 subset_localsubrs[fd].init (); 477 if (!subr_subsetter.encode_localsubrs (fd, subset_localsubrs[fd])) 478 return false; 479 } 480 } 481 482 /* FDSelect */ 483 if (acc.fdSelect != &Null (CFF2FDSelect)) 484 { 485 if (unlikely (!hb_plan_subset_cff_fdselect (plan, 486 orig_fdcount, 487 *(const FDSelect *)acc.fdSelect, 488 subset_fdcount, 489 subset_fdselect_size, 490 subset_fdselect_format, 491 subset_fdselect_ranges, 492 fdmap))) 493 return false; 494 } 495 else 496 fdmap.identity (1); 497 498 return true; 499 } 500 501 cff2_sub_table_info_t info; 502 503 unsigned int num_glyphs; 504 unsigned int orig_fdcount = 0; 505 unsigned int subset_fdcount = 1; 506 unsigned int subset_fdselect_size = 0; 507 unsigned int subset_fdselect_format = 0; 508 bool pinned = false; 509 hb_vector_t<code_pair_t> subset_fdselect_ranges; 510 511 hb_inc_bimap_t fdmap; 512 513 str_buff_vec_t subset_charstrings; 514 str_buff_vec_t subset_globalsubrs; 515 hb_vector_t<str_buff_vec_t> subset_localsubrs; 516 517 bool drop_hints = false; 518 bool desubroutinize = false; 519 520 unsigned min_charstrings_off_size = 0; 521 }; 522 } // namespace OT 523 524 static bool _serialize_cff2_charstrings (hb_serialize_context_t *c, 525 cff2_subset_plan &plan, 526 const OT::cff2::accelerator_subset_t &acc) 527 { 528 c->push (); 529 530 unsigned data_size = 0; 531 unsigned total_size = CFF2CharStrings::total_size (plan.subset_charstrings, &data_size, plan.min_charstrings_off_size); 532 if (unlikely (!c->start_zerocopy (total_size))) 533 return false; 534 535 auto *cs = c->start_embed<CFF2CharStrings> (); 536 if (unlikely (!cs->serialize (c, plan.subset_charstrings, &data_size, plan.min_charstrings_off_size))) 537 { 538 c->pop_discard (); 539 return false; 540 } 541 542 plan.info.char_strings_link = c->pop_pack (false); 543 return true; 544 } 545 546 bool 547 OT::cff2::accelerator_subset_t::serialize (hb_serialize_context_t *c, 548 struct cff2_subset_plan &plan, 549 hb_array_t<int> normalized_coords) const 550 { 551 /* push charstrings onto the object stack first which will ensure it packs as the last 552 object in the table. Keeping the chastrings last satisfies the requirements for patching 553 via IFTB. If this ordering needs to be changed in the future, charstrings should be left 554 at the end whenever HB_SUBSET_FLAGS_ITFB_REQUIREMENTS is enabled. */ 555 if (!_serialize_cff2_charstrings(c, plan, *this)) 556 return false; 557 558 /* private dicts & local subrs */ 559 hb_vector_t<table_info_t> private_dict_infos; 560 if (unlikely (!private_dict_infos.resize (plan.subset_fdcount))) return false; 561 562 for (int i = (int)privateDicts.length; --i >= 0 ;) 563 { 564 if (plan.fdmap.has (i)) 565 { 566 objidx_t subrs_link = 0; 567 568 if (plan.subset_localsubrs[i].length > 0) 569 { 570 auto *dest = c->push <CFF2Subrs> (); 571 if (likely (dest->serialize (c, plan.subset_localsubrs[i]))) 572 subrs_link = c->pop_pack (false); 573 else 574 { 575 c->pop_discard (); 576 return false; 577 } 578 } 579 auto *pd = c->push<PrivateDict> (); 580 cff2_private_dict_op_serializer_t privSzr (plan.desubroutinize, plan.drop_hints, plan.pinned, 581 varStore, normalized_coords); 582 if (likely (pd->serialize (c, privateDicts[i], privSzr, subrs_link))) 583 { 584 unsigned fd = plan.fdmap[i]; 585 private_dict_infos[fd].size = c->length (); 586 private_dict_infos[fd].link = c->pop_pack (); 587 } 588 else 589 { 590 c->pop_discard (); 591 return false; 592 } 593 } 594 } 595 596 /* FDSelect */ 597 if (fdSelect != &Null (CFF2FDSelect)) 598 { 599 c->push (); 600 if (likely (hb_serialize_cff_fdselect (c, plan.num_glyphs, *(const FDSelect *)fdSelect, 601 plan.orig_fdcount, 602 plan.subset_fdselect_format, plan.subset_fdselect_size, 603 plan.subset_fdselect_ranges))) 604 plan.info.fd_select.link = c->pop_pack (); 605 else 606 { 607 c->pop_discard (); 608 return false; 609 } 610 } 611 612 /* FDArray (FD Index) */ 613 { 614 auto *fda = c->push<CFF2FDArray> (); 615 cff_font_dict_op_serializer_t fontSzr; 616 auto it = 617 + hb_zip (+ hb_iter (fontDicts) 618 | hb_filter ([&] (const cff2_font_dict_values_t &_) 619 { return plan.fdmap.has (&_ - &fontDicts[0]); }), 620 hb_iter (private_dict_infos)) 621 ; 622 if (unlikely (!fda->serialize (c, it, fontSzr))) 623 { 624 c->pop_discard (); 625 return false; 626 } 627 plan.info.fd_array_link = c->pop_pack (false); 628 } 629 630 /* variation store */ 631 if (varStore != &Null (CFF2ItemVariationStore) && 632 !plan.pinned) 633 { 634 auto *dest = c->push<CFF2ItemVariationStore> (); 635 if (unlikely (!dest->serialize (c, varStore))) 636 { 637 c->pop_discard (); 638 return false; 639 } 640 plan.info.var_store_link = c->pop_pack (false); 641 } 642 643 OT::cff2 *cff2 = c->allocate_min<OT::cff2> (); 644 if (unlikely (!cff2)) return false; 645 646 /* header */ 647 cff2->version.major = 0x02; 648 cff2->version.minor = 0x00; 649 cff2->topDict = OT::cff2::static_size; 650 651 /* top dict */ 652 { 653 TopDict &dict = cff2 + cff2->topDict; 654 cff2_top_dict_op_serializer_t topSzr; 655 if (unlikely (!dict.serialize (c, topDict, topSzr, plan.info))) return false; 656 cff2->topDictSize = c->head - (const char *)&dict; 657 } 658 659 /* global subrs */ 660 { 661 auto *dest = c->start_embed <CFF2Subrs> (); 662 return dest->serialize (c, plan.subset_globalsubrs); 663 } 664 } 665 666 bool 667 OT::cff2::accelerator_subset_t::subset (hb_subset_context_t *c) const 668 { 669 cff2_subset_plan cff2_plan; 670 671 if (unlikely (!cff2_plan.create (*this, c->plan))) return false; 672 return serialize (c->serializer, cff2_plan, 673 c->plan->normalized_coords.as_array ()); 674 } 675 676 #endif