Lower CFF2 to CFF when subsetting to static instance (#5748)
* Add CFF2 to CFF1 converter for font instantiation
When instantiating a CFF2 variable font to a static instance, automatically
convert the output to CFF1 format instead of keeping it as CFF2. This is
appropriate since CFF2's variation features are no longer needed in static
fonts, and CFF1 is more widely supported.
The converter integrates with the CFF2 subsetter and triggers automatically
when pinned=true (i.e., when instantiating with normalized coordinates).
Converts table structure:
- CFF2 inline TopDict → CFF1 TopDict INDEX
- Adds Name INDEX and String INDEX
- Creates CID-keyed font with ROS operator (Adobe-Identity-0)
- Generates identity CID charset
- Removes variation operators (vstore, vsindex, blend)
- Reuses desubroutinized charstrings and subroutines
Focus is on structural conversion; charstring encoding details like
endchar/return operators and width encoding can be added incrementally.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix CFF2 to CFF1 converter table tag and serialization
Updates the CFF2 to CFF1 converter to properly output CFF format:
- Use plan->add_table() to output 'CFF ' tag instead of 'CFF2'
- Return false from subset() to signal CFF2 table not needed
- Use passed serializer instead of creating new one
- Change String INDEX to use push/copy/pop_pack pattern
Known issue: Output has 5 extra bytes before CFF header that need
investigation of serializer head/tail allocation patterns.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix CFF2 to CFF1 serialization order and header version
Fixes the INDEX serialization order and version field initialization:
- Reorder serialization to match CFF1 pattern: CFF header, then Name INDEX,
then Top DICT INDEX. This eliminates extra bytes before the header.
- Fix version field by using cff->min_size instead of OT::cff1::min_size
for nameIndex offset. This resolves the issue where major version was
written as 0x00 instead of 0x01.
- Add String INDEX with "Adobe" and "Identity" strings (SIDs 391, 392)
required by the ROS operator in CID-keyed fonts.
The CFF header now correctly starts at offset 0 with version 1.0.
Remaining issue: FDArray offset parsing error needs investigation.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Improve FDArray serialization in CFF2 to CFF1 converter
Change FDArray serialization to use inferred template types and avoid
preprocessor issues with the likely() macro when using template syntax.
Progress:
- CFF header now at offset 0 with correct version 1.0 ✓
- String INDEX with ROS strings (Adobe, Identity) ✓
- ROS operator with correct SIDs ✓
- Serialization order matches CFF1 pattern ✓
Remaining issue:
- FDArray INDEX structure needs investigation - fontTools reports
"unpack requires a buffer of 4 bytes" when reading FDArray offsets.
May be related to INDEX offSize or data alignment.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* WIP: Add CFF2 to CFF1 converter for font instantiation
When a CFF2 variable font is fully instantiated (all axes pinned),
convert it to CFF1 format. This is work in progress.
Key changes:
- Add serialize_cff2_to_cff1() function to convert instantiated CFF2 to CFF1
- Use cff2_private_dict_op_serializer_t to instantiate Private DICT blends
- Add CFF1 table structures (Name INDEX, String INDEX for ROS, etc.)
- Create CID-keyed fonts with ROS operator (Adobe-Identity-0)
- Use subsetter's serializer with end_serialize() to resolve links
Current status:
- Private DICT instantiation works (blends evaluated correctly)
- CharStrings are properly flattened (verified: no blend operators remain)
- CFF1 table structure created but CharStrings INDEX validation fails in OTS
- Need to investigate CharStrings serialization format issue
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Add FDSelect and FDArray to Top DICT for CID-keyed fonts
CFF1 CID-keyed fonts require FDSelect and FDArray operators in the
Top DICT, even when there's only one Font DICT. CFF2 makes FDSelect
optional when there's a single FD, but CFF1 always requires it.
This commit:
- Always creates an FDSelect for CID-keyed CFF1 fonts (even with 1 FD)
- Explicitly serializes FDSelect and FDArray operators in Top DICT
- Avoids duplicating these operators when processing CFF2 Top DICT
Fixes font rendering - glyphs now have proper paths.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Use FDSelect3 (range-based) instead of FDSelect0
When creating a default FDSelect for single-FD CID fonts, use the
compact range-based FDSelect3 format instead of FDSelect0.
For a font with 254 glyphs mapping to FD 0:
- FDSelect0: 254 bytes (1 byte per glyph)
- FDSelect3: 8 bytes (format + nRanges + range + sentinel)
Saves 246 bytes (~1KB in this test font).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Update documentation with implementation status
Documents:
- What's implemented and working
- Known limitations (FontBBox, width, stack depth)
- Key conversion steps
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Copy FontBBox from head table
Instead of calculating from scratch, copy the font bounding box
directly from the head table (xMin, yMin, xMax, yMax).
Before: FontBBox value="0 0 0 0"
After: FontBBox value="-175 -250 1178 899"
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [CFF2->CFF1] Add width optimization and encoding
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [CFF2->CFF1] Port O(n) width optimization algorithm
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [cff2-to-cff1] Add command capture infrastructure for specialization
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [cff2-to-cff1] Implement CharString specialization with stack depth control
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [cff2-to-cff1] Add generalization phase for stack safety
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [cff2-to-cff1] Update documentation to reflect completed features
Removed outdated limitations that have been implemented:
✓ FontBBox (now copied from head table)
✓ Width handling (O(n) optimization + CharString encoding)
✓ Stack depth control (generalize→specialize with maxstack=48)
Added new features to implementation status:
- CharString specialization
- Width optimization and encoding
- Stack depth control
Remaining limitations are now accurate (only CID-keyed, no curve
specialization/peephole optimizations).
* [cff2-to-cff1] Remove CID-keyed 'limitation' from docs
CID-keyed output is by design, not a limitation:
- Variable fonts (CFF2) are instantiated to CID-keyed CFF1
- We explicitly add ROS operator to make output CID-keyed
- This is the correct behavior for instantiated fonts
Changed section from 'KNOWN LIMITATIONS' to 'FUTURE ENHANCEMENTS'
to better reflect that curve/peephole optimizations are optional
improvements, not missing functionality.
* [CFF2->CFF1] Add HB_SUBSET_FLAGS_DOWNGRADE_CFF2 flag
Make the CFF2→CFF1 converter opt-in via the HB_SUBSET_FLAGS_DOWNGRADE_CFF2
flag (disabled by default). This gives users control over whether variable
font instantiation produces CFF2 (default) or CFF1 (for compatibility).
Changes:
- Add HB_SUBSET_FLAGS_DOWNGRADE_CFF2 flag to hb-subset.h
- Gate CFF2→CFF1 conversion on flag check in hb-subset-cff2.cc
- Add --downgrade-cff2 command-line option to hb-subset tool
The flag enables converting instantiated variable fonts from CFF2 to CFF1,
providing compatibility with older renderers that don't support CFF2.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [CFF2->CFF1] Deduplicate CFF1Font string constant
Define CFF1_DEFAULT_FONT_NAME constant once in hb-subset-cff2-to-cff1.hh
and use it in both the plan creation and serialization code.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* [CFF2->CFF1] Fix swapped horizontal/vertical logic in specializer
The specialization logic for rmoveto/rlineto to h/v variants was backwards:
- When dx=0 and dy!=0, movement is VERTICAL (vmoveto/vlineto), not horizontal
- When dy=0 and dx!=0, movement is HORIZONTAL (hmoveto/hlineto), not vertical
This was causing completely garbled outlines in converted fonts. The fix
corrects the operator selection to match the actual movement direction.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
diff --git a/src/harfbuzz-subset.cc b/src/harfbuzz-subset.cc
index f6a5a31..616c244 100644
--- a/src/harfbuzz-subset.cc
+++ b/src/harfbuzz-subset.cc
@@ -54,6 +54,7 @@
#include "hb-style.cc"
#include "hb-subset-cff-common.cc"
#include "hb-subset-cff1.cc"
+#include "hb-subset-cff2-to-cff1.cc"
#include "hb-subset-cff2.cc"
#include "hb-subset-input.cc"
#include "hb-subset-instancer-iup.cc"
diff --git a/src/hb-buffer-deserialize-json.hh b/src/hb-buffer-deserialize-json.hh
index e57b616..75a1a11 100644
--- a/src/hb-buffer-deserialize-json.hh
+++ b/src/hb-buffer-deserialize-json.hh
@@ -32,7 +32,7 @@
#include "hb.hh"
-#line 33 "hb-buffer-deserialize-json.hh"
+#line 36 "hb-buffer-deserialize-json.hh"
static const unsigned char _deserialize_json_trans_keys[] = {
0u, 0u, 9u, 34u, 97u, 121u, 120u, 121u, 34u, 34u, 9u, 58u, 9u, 57u, 48u, 57u,
9u, 125u, 9u, 125u, 9u, 93u, 9u, 125u, 34u, 34u, 9u, 58u, 9u, 57u, 48u, 57u,
@@ -597,12 +597,12 @@
hb_glyph_info_t info = {0};
hb_glyph_position_t pos = {0};
-#line 594 "hb-buffer-deserialize-json.hh"
+#line 601 "hb-buffer-deserialize-json.hh"
{
cs = deserialize_json_start;
}
-#line 597 "hb-buffer-deserialize-json.hh"
+#line 606 "hb-buffer-deserialize-json.hh"
{
int _slen;
int _trans;
@@ -712,7 +712,7 @@
#line 56 "hb-buffer-deserialize-json.rl"
{ if (unlikely (!buffer->ensure_unicode ())) return false; }
break;
-#line 689 "hb-buffer-deserialize-json.hh"
+#line 716 "hb-buffer-deserialize-json.hh"
}
_again:
diff --git a/src/hb-buffer-deserialize-text-glyphs.hh b/src/hb-buffer-deserialize-text-glyphs.hh
index 8fc54b3..8c0353b 100644
--- a/src/hb-buffer-deserialize-text-glyphs.hh
+++ b/src/hb-buffer-deserialize-text-glyphs.hh
@@ -32,7 +32,7 @@
#include "hb.hh"
-#line 33 "hb-buffer-deserialize-text-glyphs.hh"
+#line 36 "hb-buffer-deserialize-text-glyphs.hh"
static const unsigned char _deserialize_text_glyphs_trans_keys[] = {
0u, 0u, 35u, 124u, 48u, 57u, 60u, 124u, 45u, 57u, 48u, 57u, 44u, 44u, 45u, 57u,
48u, 57u, 44u, 44u, 45u, 57u, 48u, 57u, 44u, 44u, 45u, 57u, 48u, 57u, 62u, 62u,
@@ -389,12 +389,12 @@
hb_glyph_info_t info = {0};
hb_glyph_position_t pos = {0};
-#line 386 "hb-buffer-deserialize-text-glyphs.hh"
+#line 393 "hb-buffer-deserialize-text-glyphs.hh"
{
cs = deserialize_text_glyphs_start;
}
-#line 389 "hb-buffer-deserialize-text-glyphs.hh"
+#line 398 "hb-buffer-deserialize-text-glyphs.hh"
{
int _slen;
int _trans;
@@ -552,7 +552,7 @@
return false;
}
break;
-#line 523 "hb-buffer-deserialize-text-glyphs.hh"
+#line 556 "hb-buffer-deserialize-text-glyphs.hh"
}
_again:
@@ -573,7 +573,7 @@
*end_ptr = p;
}
break;
-#line 542 "hb-buffer-deserialize-text-glyphs.hh"
+#line 577 "hb-buffer-deserialize-text-glyphs.hh"
}
}
diff --git a/src/hb-buffer-deserialize-text-unicode.hh b/src/hb-buffer-deserialize-text-unicode.hh
index 0bb00c4..26ec0b4 100644
--- a/src/hb-buffer-deserialize-text-unicode.hh
+++ b/src/hb-buffer-deserialize-text-unicode.hh
@@ -32,7 +32,7 @@
#include "hb.hh"
-#line 33 "hb-buffer-deserialize-text-unicode.hh"
+#line 36 "hb-buffer-deserialize-text-unicode.hh"
static const unsigned char _deserialize_text_unicode_trans_keys[] = {
0u, 0u, 43u, 102u, 48u, 102u, 48u, 124u, 48u, 57u, 62u, 124u, 48u, 124u, 60u, 117u,
85u, 117u, 85u, 117u, 0
@@ -150,12 +150,12 @@
hb_glyph_info_t info = {0};
const hb_glyph_position_t pos = {0};
-#line 147 "hb-buffer-deserialize-text-unicode.hh"
+#line 154 "hb-buffer-deserialize-text-unicode.hh"
{
cs = deserialize_text_unicode_start;
}
-#line 150 "hb-buffer-deserialize-text-unicode.hh"
+#line 159 "hb-buffer-deserialize-text-unicode.hh"
{
int _slen;
int _trans;
@@ -215,7 +215,7 @@
hb_memset (&info, 0, sizeof (info));
}
break;
-#line 203 "hb-buffer-deserialize-text-unicode.hh"
+#line 219 "hb-buffer-deserialize-text-unicode.hh"
}
_again:
@@ -238,7 +238,7 @@
*end_ptr = p;
}
break;
-#line 224 "hb-buffer-deserialize-text-unicode.hh"
+#line 242 "hb-buffer-deserialize-text-unicode.hh"
}
}
diff --git a/src/hb-cff-specializer.hh b/src/hb-cff-specializer.hh
new file mode 100644
index 0000000..005384e
--- /dev/null
+++ b/src/hb-cff-specializer.hh
@@ -0,0 +1,266 @@
+/*
+ * CFF CharString Specializer
+ *
+ * Optimizes CharString bytecode by using specialized operators
+ * (hlineto, vlineto, hhcurveto, etc.) to save bytes and respects
+ * CFF1 stack limit (48 values).
+ *
+ * Based on fontTools.cffLib.specializer
+ */
+
+#ifndef HB_CFF_SPECIALIZER_HH
+#define HB_CFF_SPECIALIZER_HH
+
+#include "hb.hh"
+#include "hb-cff-interp-cs-common.hh"
+
+namespace CFF {
+
+/* CharString command representation - forward declared in hb-subset-cff-common.hh */
+
+/* Check if a value is effectively zero */
+static inline bool
+is_zero (const number_t &n)
+{
+ return n.to_int () == 0;
+}
+
+/* Generalize CharString commands to canonical form
+ *
+ * Converts all operators to their general forms and breaks down
+ * multi-segment operators into single segments. This ensures we
+ * start from a clean baseline before specialization.
+ *
+ * Based on fontTools.cffLib.specializer.generalizeCommands
+ */
+static void
+generalize_commands (hb_vector_t<cs_command_t> &commands)
+{
+ hb_vector_t<cs_command_t> result;
+ result.alloc (commands.length * 2); /* Estimate: might expand */
+
+ for (unsigned i = 0; i < commands.length; i++)
+ {
+ auto &cmd = commands[i];
+
+ switch (cmd.op)
+ {
+ case OpCode_hmoveto:
+ case OpCode_vmoveto:
+ {
+ /* Convert to rmoveto with explicit dx,dy */
+ cs_command_t gen (OpCode_rmoveto);
+ gen.args.alloc (2);
+
+ if (cmd.op == OpCode_hmoveto && cmd.args.length >= 1)
+ {
+ gen.args.push (cmd.args[0]); /* dx */
+ number_t zero; zero.set_int (0);
+ gen.args.push (zero); /* dy = 0 */
+ }
+ else if (cmd.op == OpCode_vmoveto && cmd.args.length >= 1)
+ {
+ number_t zero; zero.set_int (0);
+ gen.args.push (zero); /* dx = 0 */
+ gen.args.push (cmd.args[0]); /* dy */
+ }
+ result.push (gen);
+ break;
+ }
+
+ case OpCode_hlineto:
+ case OpCode_vlineto:
+ {
+ /* Convert h/v lineto to rlineto, breaking into single segments
+ * hlineto alternates: dx1 (→ dx1,0) dy1 (→ 0,dy1) dx2 (→ dx2,0) ...
+ * vlineto alternates: dy1 (→ 0,dy1) dx1 (→ dx1,0) dy2 (→ 0,dy2) ... */
+ bool is_h = (cmd.op == OpCode_hlineto);
+ number_t zero; zero.set_int (0);
+
+ for (unsigned j = 0; j < cmd.args.length; j++)
+ {
+ cs_command_t seg (OpCode_rlineto);
+ seg.args.alloc (2);
+
+ bool is_horizontal = is_h ? (j % 2 == 0) : (j % 2 == 1);
+ if (is_horizontal)
+ {
+ seg.args.push (cmd.args[j]); /* dx */
+ seg.args.push (zero); /* dy = 0 */
+ }
+ else
+ {
+ seg.args.push (zero); /* dx = 0 */
+ seg.args.push (cmd.args[j]); /* dy */
+ }
+ result.push (seg);
+ }
+ break;
+ }
+
+ case OpCode_rlineto:
+ {
+ /* Break into single segments (dx,dy pairs) */
+ for (unsigned j = 0; j + 1 < cmd.args.length; j += 2)
+ {
+ cs_command_t seg (OpCode_rlineto);
+ seg.args.alloc (2);
+ seg.args.push (cmd.args[j]);
+ seg.args.push (cmd.args[j + 1]);
+ result.push (seg);
+ }
+ break;
+ }
+
+ case OpCode_rrcurveto:
+ {
+ /* Break into single segments (6 args each) */
+ for (unsigned j = 0; j + 5 < cmd.args.length; j += 6)
+ {
+ cs_command_t seg (OpCode_rrcurveto);
+ seg.args.alloc (6);
+ for (unsigned k = 0; k < 6; k++)
+ seg.args.push (cmd.args[j + k]);
+ result.push (seg);
+ }
+ break;
+ }
+
+ default:
+ /* Keep other operators as-is */
+ result.push (cmd);
+ break;
+ }
+ }
+
+ /* Replace commands with generalized result */
+ commands.resize (0);
+ for (unsigned i = 0; i < result.length; i++)
+ commands.push (result[i]);
+}
+
+/* Specialize CharString commands to optimize bytecode size
+ *
+ * Follows fontTools approach:
+ * 0. Generalize: Break down to canonical single-segment form
+ * 1. Specialize: Convert rmoveto/rlineto to h/v variants when dx or dy is zero
+ * 2. Combine: Merge adjacent compatible operators
+ * 3. Enforce: Respect maxstack limit (default 48 for CFF1)
+ *
+ * This ensures we never exceed stack depth while optimizing bytecode.
+ */
+static void
+specialize_commands (hb_vector_t<cs_command_t> &commands,
+ unsigned maxstack = 48)
+{
+ if (commands.length == 0) return;
+
+ /* Pass 0: Generalize to canonical form (fontTools does this first) */
+ generalize_commands (commands);
+
+ /* Pass 1: Specialize rmoveto/rlineto into h/v variants */
+ for (unsigned i = 0; i < commands.length; i++)
+ {
+ auto &cmd = commands[i];
+
+ if ((cmd.op == OpCode_rmoveto || cmd.op == OpCode_rlineto) &&
+ cmd.args.length == 2)
+ {
+ bool dx_zero = is_zero (cmd.args[0]);
+ bool dy_zero = is_zero (cmd.args[1]);
+
+ if (dx_zero && !dy_zero)
+ {
+ /* Vertical movement (dx=0): keep only dy */
+ cmd.op = (cmd.op == OpCode_rmoveto) ? OpCode_vmoveto : OpCode_vlineto;
+ /* Shift dy to position 0 */
+ cmd.args[0] = cmd.args[1];
+ cmd.args.resize (1);
+ }
+ else if (!dx_zero && dy_zero)
+ {
+ /* Horizontal movement (dy=0): keep only dx */
+ cmd.op = (cmd.op == OpCode_rmoveto) ? OpCode_hmoveto : OpCode_hlineto;
+ cmd.args.resize (1); /* Keep only dx */
+ }
+ /* else: both zero or both non-zero, keep as rmoveto/rlineto */
+ }
+ }
+
+ /* Pass 2: Combine adjacent hlineto/vlineto operators
+ * hlineto can take multiple args alternating with vlineto
+ * This saves operator bytes */
+ for (int i = (int)commands.length - 1; i > 0; i--)
+ {
+ auto &cmd = commands[i];
+ auto &prev = commands[i-1];
+
+ /* Combine adjacent hlineto + vlineto or vlineto + hlineto */
+ if ((prev.op == OpCode_hlineto && cmd.op == OpCode_vlineto) ||
+ (prev.op == OpCode_vlineto && cmd.op == OpCode_hlineto))
+ {
+ /* Check stack depth */
+ unsigned combined_args = prev.args.length + cmd.args.length;
+ if (combined_args < maxstack)
+ {
+ /* Merge into first command, keep its operator */
+ for (unsigned j = 0; j < cmd.args.length; j++)
+ prev.args.push (cmd.args[j]);
+ commands.remove_ordered (i);
+ i++; /* Adjust for removed element */
+ }
+ }
+ }
+
+ /* Pass 3: Combine adjacent identical operators */
+ for (int i = (int)commands.length - 1; i > 0; i--)
+ {
+ auto &cmd = commands[i];
+ auto &prev = commands[i-1];
+
+ /* Combine same operators (e.g., rlineto + rlineto) */
+ if (prev.op == cmd.op &&
+ (cmd.op == OpCode_rlineto || cmd.op == OpCode_hlineto ||
+ cmd.op == OpCode_vlineto || cmd.op == OpCode_rrcurveto))
+ {
+ /* Check stack depth */
+ unsigned combined_args = prev.args.length + cmd.args.length;
+ if (combined_args < maxstack)
+ {
+ /* Merge args */
+ for (unsigned j = 0; j < cmd.args.length; j++)
+ prev.args.push (cmd.args[j]);
+ commands.remove_ordered (i);
+ i++; /* Adjust for removed element */
+ }
+ }
+ }
+}
+
+/* Encode commands back to binary CharString */
+static bool
+encode_commands (const hb_vector_t<cs_command_t> &commands,
+ str_buff_t &output)
+{
+ for (const auto &cmd : commands)
+ {
+ str_encoder_t encoder (output);
+
+ /* Encode arguments */
+ for (const auto &arg : cmd.args)
+ encoder.encode_num_cs (arg);
+
+ /* Encode operator */
+ if (cmd.op != OpCode_Invalid)
+ encoder.encode_op (cmd.op);
+
+ if (encoder.in_error ())
+ return false;
+ }
+
+ return true;
+}
+
+} /* namespace CFF */
+
+#endif /* HB_CFF_SPECIALIZER_HH */
diff --git a/src/hb-cff-width-optimizer.hh b/src/hb-cff-width-optimizer.hh
new file mode 100644
index 0000000..e105dae
--- /dev/null
+++ b/src/hb-cff-width-optimizer.hh
@@ -0,0 +1,207 @@
+/*
+ * CFF Width Optimizer
+ *
+ * Determines optimal defaultWidthX and nominalWidthX values
+ * to minimize CharString byte cost.
+ *
+ * Based on fontTools.cffLib.width
+ */
+
+#ifndef HB_CFF_WIDTH_OPTIMIZER_HH
+#define HB_CFF_WIDTH_OPTIMIZER_HH
+
+#include "hb.hh"
+
+namespace CFF {
+
+/* Calculate byte cost for encoding a width delta */
+static inline unsigned
+width_delta_cost (int delta)
+{
+ delta = abs (delta);
+ if (delta <= 107) return 1;
+ if (delta <= 1131) return 2;
+ return 5;
+}
+
+/* Cumulative sum forward */
+static void
+cumsum_forward (const hb_hashmap_t<unsigned, unsigned> &freq,
+ unsigned min_w, unsigned max_w,
+ hb_vector_t<unsigned> &cumsum)
+{
+ cumsum.resize (max_w - min_w + 1);
+ unsigned v = 0;
+ for (unsigned x = min_w; x <= max_w; x++)
+ {
+ v += freq.get (x);
+ cumsum[x - min_w] = v;
+ }
+}
+
+/* Cumulative max forward */
+static void
+cummax_forward (const hb_hashmap_t<unsigned, unsigned> &freq,
+ unsigned min_w, unsigned max_w,
+ hb_vector_t<unsigned> &cummax)
+{
+ cummax.resize (max_w - min_w + 1);
+ unsigned v = 0;
+ for (unsigned x = min_w; x <= max_w; x++)
+ {
+ v = hb_max (v, freq.get (x));
+ cummax[x - min_w] = v;
+ }
+}
+
+/* Cumulative sum backward */
+static void
+cumsum_backward (const hb_hashmap_t<unsigned, unsigned> &freq,
+ unsigned min_w, unsigned max_w,
+ hb_vector_t<unsigned> &cumsum)
+{
+ cumsum.resize (max_w - min_w + 1);
+ unsigned v = 0;
+ for (int x = (int) max_w; x >= (int) min_w; x--)
+ {
+ v += freq.get ((unsigned) x);
+ cumsum[x - min_w] = v;
+ }
+}
+
+/* Cumulative max backward */
+static void
+cummax_backward (const hb_hashmap_t<unsigned, unsigned> &freq,
+ unsigned min_w, unsigned max_w,
+ hb_vector_t<unsigned> &cummax)
+{
+ cummax.resize (max_w - min_w + 1);
+ unsigned v = 0;
+ for (int x = (int) max_w; x >= (int) min_w; x--)
+ {
+ v = hb_max (v, freq.get ((unsigned) x));
+ cummax[x - min_w] = v;
+ }
+}
+
+/* Helper to safely get cumulative value with bounds checking */
+static inline unsigned
+safe_get (const hb_vector_t<unsigned> &vec, int x, unsigned min_w, unsigned max_w)
+{
+ if (x < (int) min_w || x > (int) max_w) return 0;
+ return vec[x - min_w];
+}
+
+/* Optimize defaultWidthX and nominalWidthX from a list of widths
+ * O(UPEM+numGlyphs) algorithm from fontTools.cffLib.width */
+static void
+optimize_widths (const hb_vector_t<unsigned> &width_list,
+ unsigned &default_width,
+ unsigned &nominal_width)
+{
+ if (width_list.length == 0)
+ {
+ default_width = nominal_width = 0;
+ return;
+ }
+
+ /* Build frequency map */
+ hb_hashmap_t<unsigned, unsigned> widths;
+ unsigned min_w = width_list[0];
+ unsigned max_w = width_list[0];
+
+ for (unsigned w : width_list)
+ {
+ widths.set (w, widths.get (w) + 1);
+ min_w = hb_min (min_w, w);
+ max_w = hb_max (max_w, w);
+ }
+
+ /* Cumulative sum/max forward/backward */
+ hb_vector_t<unsigned> cumFrqU, cumMaxU, cumFrqD, cumMaxD;
+ cumsum_forward (widths, min_w, max_w, cumFrqU);
+ cummax_forward (widths, min_w, max_w, cumMaxU);
+ cumsum_backward (widths, min_w, max_w, cumFrqD);
+ cummax_backward (widths, min_w, max_w, cumMaxD);
+
+ /* Cost per nominal choice, without default consideration */
+ auto nomnCost = [&] (unsigned x) -> unsigned {
+ return safe_get (cumFrqU, x, min_w, max_w) +
+ safe_get (cumFrqU, x - 108, min_w, max_w) +
+ safe_get (cumFrqU, x - 1132, min_w, max_w) * 3 +
+ safe_get (cumFrqD, x, min_w, max_w) +
+ safe_get (cumFrqD, x + 108, min_w, max_w) +
+ safe_get (cumFrqD, x + 1132, min_w, max_w) * 3 -
+ widths.get (x);
+ };
+
+ /* Cost-saving per nominal choice, by best default choice */
+ auto dfltCost = [&] (unsigned x) -> unsigned {
+ unsigned u = hb_max (hb_max (safe_get (cumMaxU, x, min_w, max_w),
+ safe_get (cumMaxU, x - 108, min_w, max_w) * 2),
+ safe_get (cumMaxU, x - 1132, min_w, max_w) * 5);
+ unsigned d = hb_max (hb_max (safe_get (cumMaxD, x, min_w, max_w),
+ safe_get (cumMaxD, x + 108, min_w, max_w) * 2),
+ safe_get (cumMaxD, x + 1132, min_w, max_w) * 5);
+ return hb_max (u, d);
+ };
+
+ /* Find best nominal */
+ unsigned best_nominal = min_w;
+ unsigned best_cost = nomnCost (min_w) - dfltCost (min_w);
+
+ for (unsigned x = min_w + 1; x <= max_w; x++)
+ {
+ unsigned cost = nomnCost (x) - dfltCost (x);
+ if (cost < best_cost)
+ {
+ best_cost = cost;
+ best_nominal = x;
+ }
+ }
+
+ /* Work back the best default */
+ unsigned best_default = best_nominal;
+ unsigned best_default_cost = (unsigned) -1;
+
+ /* Check candidates around best_nominal */
+ int candidates[] = {
+ (int) best_nominal,
+ (int) best_nominal - 108,
+ (int) best_nominal - 1132,
+ (int) best_nominal + 108,
+ (int) best_nominal + 1132
+ };
+
+ for (int candidate : candidates)
+ {
+ if (candidate < (int) min_w || candidate > (int) max_w)
+ continue;
+
+ /* Compute actual cost with this default */
+ unsigned cost = 0;
+ for (auto kv : widths.iter ())
+ {
+ unsigned w = kv.first;
+ unsigned freq = kv.second;
+
+ if (w == (unsigned) candidate)
+ continue;
+
+ cost += freq * width_delta_cost ((int) w - (int) best_nominal);
+ }
+
+ if (cost < best_default_cost)
+ {
+ best_default_cost = cost;
+ best_default = (unsigned) candidate;
+ }
+ }
+
+ default_width = best_default;
+ nominal_width = best_nominal;
+}
+
+} /* namespace CFF */
+
+#endif /* HB_CFF_WIDTH_OPTIMIZER_HH */
diff --git a/src/hb-number-parser.hh b/src/hb-number-parser.hh
index ec68c3a..1a9dbba 100644
--- a/src/hb-number-parser.hh
+++ b/src/hb-number-parser.hh
@@ -31,7 +31,7 @@
#include "hb.hh"
-#line 32 "hb-number-parser.hh"
+#line 35 "hb-number-parser.hh"
static const unsigned char _double_parser_trans_keys[] = {
0u, 0u, 43u, 57u, 46u, 57u, 48u, 57u, 43u, 57u, 48u, 57u, 48u, 101u, 48u, 57u,
46u, 101u, 0
@@ -135,12 +135,12 @@
int cs;
-#line 132 "hb-number-parser.hh"
+#line 139 "hb-number-parser.hh"
{
cs = double_parser_start;
}
-#line 135 "hb-number-parser.hh"
+#line 144 "hb-number-parser.hh"
{
int _slen;
int _trans;
@@ -198,7 +198,7 @@
exp_overflow = true;
}
break;
-#line 187 "hb-number-parser.hh"
+#line 202 "hb-number-parser.hh"
}
_again:
diff --git a/src/hb-ot-shaper-indic-machine.hh b/src/hb-ot-shaper-indic-machine.hh
index 92fb64a..6ff65c3 100644
--- a/src/hb-ot-shaper-indic-machine.hh
+++ b/src/hb-ot-shaper-indic-machine.hh
@@ -53,7 +53,7 @@
};
-#line 54 "hb-ot-shaper-indic-machine.hh"
+#line 57 "hb-ot-shaper-indic-machine.hh"
#define indic_syllable_machine_ex_A 9u
#define indic_syllable_machine_ex_C 1u
#define indic_syllable_machine_ex_CM 16u
@@ -77,7 +77,7 @@
#define indic_syllable_machine_ex_ZWNJ 5u
-#line 76 "hb-ot-shaper-indic-machine.hh"
+#line 81 "hb-ot-shaper-indic-machine.hh"
static const unsigned char _indic_syllable_machine_trans_keys[] = {
8u, 57u, 4u, 57u, 5u, 57u, 5u, 57u, 13u, 13u, 4u, 57u, 4u, 57u, 4u, 57u,
8u, 57u, 5u, 57u, 5u, 57u, 13u, 13u, 4u, 57u, 4u, 57u, 4u, 57u, 4u, 57u,
@@ -1126,7 +1126,7 @@
int cs;
hb_glyph_info_t *info = buffer->info;
-#line 1119 "hb-ot-shaper-indic-machine.hh"
+#line 1130 "hb-ot-shaper-indic-machine.hh"
{
cs = indic_syllable_machine_start;
ts = 0;
@@ -1142,7 +1142,7 @@
unsigned int syllable_serial = 1;
-#line 1131 "hb-ot-shaper-indic-machine.hh"
+#line 1146 "hb-ot-shaper-indic-machine.hh"
{
int _slen;
int _trans;
@@ -1156,7 +1156,7 @@
#line 1 "NONE"
{ts = p;}
break;
-#line 1143 "hb-ot-shaper-indic-machine.hh"
+#line 1160 "hb-ot-shaper-indic-machine.hh"
}
_keys = _indic_syllable_machine_trans_keys + (cs<<1);
@@ -1268,7 +1268,7 @@
#line 117 "hb-ot-shaper-indic-machine.rl"
{act = 7;}
break;
-#line 1232 "hb-ot-shaper-indic-machine.hh"
+#line 1272 "hb-ot-shaper-indic-machine.hh"
}
_again:
@@ -1277,7 +1277,7 @@
#line 1 "NONE"
{ts = 0;}
break;
-#line 1239 "hb-ot-shaper-indic-machine.hh"
+#line 1281 "hb-ot-shaper-indic-machine.hh"
}
if ( ++p != pe )
diff --git a/src/hb-ot-shaper-khmer-machine.hh b/src/hb-ot-shaper-khmer-machine.hh
index 848ed23..f1e7a91 100644
--- a/src/hb-ot-shaper-khmer-machine.hh
+++ b/src/hb-ot-shaper-khmer-machine.hh
@@ -48,7 +48,7 @@
};
-#line 49 "hb-ot-shaper-khmer-machine.hh"
+#line 52 "hb-ot-shaper-khmer-machine.hh"
#define khmer_syllable_machine_ex_C 1u
#define khmer_syllable_machine_ex_DOTTEDCIRCLE 11u
#define khmer_syllable_machine_ex_H 4u
@@ -66,7 +66,7 @@
#define khmer_syllable_machine_ex_ZWNJ 5u
-#line 65 "hb-ot-shaper-khmer-machine.hh"
+#line 70 "hb-ot-shaper-khmer-machine.hh"
static const unsigned char _khmer_syllable_machine_trans_keys[] = {
5u, 26u, 5u, 26u, 1u, 15u, 5u, 26u, 5u, 26u, 5u, 26u, 5u, 26u, 5u, 26u,
5u, 26u, 5u, 26u, 5u, 26u, 5u, 26u, 5u, 26u, 1u, 15u, 5u, 26u, 5u, 26u,
@@ -294,7 +294,7 @@
int cs;
hb_glyph_info_t *info = buffer->info;
-#line 287 "hb-ot-shaper-khmer-machine.hh"
+#line 298 "hb-ot-shaper-khmer-machine.hh"
{
cs = khmer_syllable_machine_start;
ts = 0;
@@ -310,7 +310,7 @@
unsigned int syllable_serial = 1;
-#line 299 "hb-ot-shaper-khmer-machine.hh"
+#line 314 "hb-ot-shaper-khmer-machine.hh"
{
int _slen;
int _trans;
@@ -324,7 +324,7 @@
#line 1 "NONE"
{ts = p;}
break;
-#line 311 "hb-ot-shaper-khmer-machine.hh"
+#line 328 "hb-ot-shaper-khmer-machine.hh"
}
_keys = _khmer_syllable_machine_trans_keys + (cs<<1);
@@ -394,7 +394,7 @@
#line 98 "hb-ot-shaper-khmer-machine.rl"
{act = 3;}
break;
-#line 368 "hb-ot-shaper-khmer-machine.hh"
+#line 398 "hb-ot-shaper-khmer-machine.hh"
}
_again:
@@ -403,7 +403,7 @@
#line 1 "NONE"
{ts = 0;}
break;
-#line 375 "hb-ot-shaper-khmer-machine.hh"
+#line 407 "hb-ot-shaper-khmer-machine.hh"
}
if ( ++p != pe )
diff --git a/src/hb-ot-shaper-myanmar-machine.hh b/src/hb-ot-shaper-myanmar-machine.hh
index 292bc9f..4b8da58 100644
--- a/src/hb-ot-shaper-myanmar-machine.hh
+++ b/src/hb-ot-shaper-myanmar-machine.hh
@@ -50,7 +50,7 @@
};
-#line 51 "hb-ot-shaper-myanmar-machine.hh"
+#line 54 "hb-ot-shaper-myanmar-machine.hh"
#define myanmar_syllable_machine_ex_A 9u
#define myanmar_syllable_machine_ex_As 32u
#define myanmar_syllable_machine_ex_C 1u
@@ -78,7 +78,7 @@
#define myanmar_syllable_machine_ex_ZWNJ 5u
-#line 77 "hb-ot-shaper-myanmar-machine.hh"
+#line 82 "hb-ot-shaper-myanmar-machine.hh"
static const unsigned char _myanmar_syllable_machine_trans_keys[] = {
1u, 57u, 3u, 57u, 5u, 57u, 5u, 57u, 3u, 57u, 5u, 57u, 3u, 57u, 3u, 57u,
3u, 57u, 3u, 57u, 3u, 57u, 5u, 57u, 1u, 15u, 3u, 57u, 3u, 57u, 3u, 57u,
@@ -549,7 +549,7 @@
int cs;
hb_glyph_info_t *info = buffer->info;
-#line 542 "hb-ot-shaper-myanmar-machine.hh"
+#line 553 "hb-ot-shaper-myanmar-machine.hh"
{
cs = myanmar_syllable_machine_start;
ts = 0;
@@ -565,7 +565,7 @@
unsigned int syllable_serial = 1;
-#line 554 "hb-ot-shaper-myanmar-machine.hh"
+#line 569 "hb-ot-shaper-myanmar-machine.hh"
{
int _slen;
int _trans;
@@ -579,7 +579,7 @@
#line 1 "NONE"
{ts = p;}
break;
-#line 566 "hb-ot-shaper-myanmar-machine.hh"
+#line 583 "hb-ot-shaper-myanmar-machine.hh"
}
_keys = _myanmar_syllable_machine_trans_keys + (cs<<1);
@@ -649,7 +649,7 @@
#line 113 "hb-ot-shaper-myanmar-machine.rl"
{act = 3;}
break;
-#line 623 "hb-ot-shaper-myanmar-machine.hh"
+#line 653 "hb-ot-shaper-myanmar-machine.hh"
}
_again:
@@ -658,7 +658,7 @@
#line 1 "NONE"
{ts = 0;}
break;
-#line 630 "hb-ot-shaper-myanmar-machine.hh"
+#line 662 "hb-ot-shaper-myanmar-machine.hh"
}
if ( ++p != pe )
diff --git a/src/hb-ot-shaper-use-machine.hh b/src/hb-ot-shaper-use-machine.hh
index 5072e4d..65b6adc 100644
--- a/src/hb-ot-shaper-use-machine.hh
+++ b/src/hb-ot-shaper-use-machine.hh
@@ -53,7 +53,7 @@
};
-#line 54 "hb-ot-shaper-use-machine.hh"
+#line 57 "hb-ot-shaper-use-machine.hh"
#define use_syllable_machine_ex_B 1u
#define use_syllable_machine_ex_CGJ 6u
#define use_syllable_machine_ex_CMAbv 31u
@@ -100,7 +100,7 @@
#define use_syllable_machine_ex_ZWNJ 14u
-#line 99 "hb-ot-shaper-use-machine.hh"
+#line 104 "hb-ot-shaper-use-machine.hh"
static const unsigned char _use_syllable_machine_trans_keys[] = {
49u, 51u, 0u, 56u, 11u, 56u, 11u, 56u, 1u, 53u, 14u, 48u, 14u, 47u, 14u, 47u,
14u, 47u, 14u, 46u, 14u, 46u, 14u, 14u, 14u, 48u, 14u, 48u, 14u, 48u, 1u, 14u,
@@ -929,7 +929,7 @@
unsigned int act HB_UNUSED;
int cs;
-#line 922 "hb-ot-shaper-use-machine.hh"
+#line 933 "hb-ot-shaper-use-machine.hh"
{
cs = use_syllable_machine_start;
ts = 0;
@@ -942,7 +942,7 @@
unsigned int syllable_serial = 1;
-#line 931 "hb-ot-shaper-use-machine.hh"
+#line 946 "hb-ot-shaper-use-machine.hh"
{
int _slen;
int _trans;
@@ -956,7 +956,7 @@
#line 1 "NONE"
{ts = p;}
break;
-#line 943 "hb-ot-shaper-use-machine.hh"
+#line 960 "hb-ot-shaper-use-machine.hh"
}
_keys = _use_syllable_machine_trans_keys + (cs<<1);
@@ -1078,7 +1078,7 @@
#line 181 "hb-ot-shaper-use-machine.rl"
{act = 9;}
break;
-#line 1039 "hb-ot-shaper-use-machine.hh"
+#line 1082 "hb-ot-shaper-use-machine.hh"
}
_again:
@@ -1087,7 +1087,7 @@
#line 1 "NONE"
{ts = 0;}
break;
-#line 1046 "hb-ot-shaper-use-machine.hh"
+#line 1091 "hb-ot-shaper-use-machine.hh"
}
if ( ++p != pe )
diff --git a/src/hb-subset-cff-common.hh b/src/hb-subset-cff-common.hh
index 75e5e66..5128459 100644
--- a/src/hb-subset-cff-common.hh
+++ b/src/hb-subset-cff-common.hh
@@ -321,11 +321,30 @@
}
};
+/* CharString command for specialization */
+struct cs_command_t
+{
+ hb_vector_t<number_t> args;
+ op_code_t op;
+
+ cs_command_t () : op (OpCode_Invalid) {}
+ cs_command_t (op_code_t op_) : op (op_) {}
+};
+
+typedef hb_vector_t<cs_command_t> *cs_command_vec_t;
+
struct flatten_param_t
{
+ flatten_param_t (str_buff_t &flatStr_,
+ bool drop_hints_,
+ const hb_subset_plan_t *plan_,
+ cs_command_vec_t commands_ = nullptr)
+ : flatStr (flatStr_), drop_hints (drop_hints_), plan (plan_), commands (commands_) {}
+
str_buff_t &flatStr;
bool drop_hints;
const hb_subset_plan_t *plan;
+ cs_command_vec_t commands; /* Optional: capture parsed commands for specialization */
};
template <typename ACC, typename ENV, typename OPSET, op_code_t endchar_op=OpCode_Invalid>
@@ -335,7 +354,8 @@
const hb_subset_plan_t *plan_)
: acc (acc_), plan (plan_) {}
- bool flatten (str_buff_vec_t &flat_charstrings)
+ bool flatten (str_buff_vec_t &flat_charstrings,
+ hb_vector_t<hb_vector_t<cs_command_t>> *command_capture = nullptr)
{
unsigned count = plan->num_output_glyphs ();
if (!flat_charstrings.resize_exact (count))
@@ -361,7 +381,8 @@
flatten_param_t param = {
flat_charstrings.arrayZ[i],
(bool) (plan->flags & HB_SUBSET_FLAGS_NO_HINTING),
- plan
+ plan,
+ command_capture ? &(*command_capture)[i] : nullptr
};
if (unlikely (!interp.interpret (param)))
return false;
diff --git a/src/hb-subset-cff2-to-cff1.cc b/src/hb-subset-cff2-to-cff1.cc
new file mode 100644
index 0000000..6a65bf5
--- /dev/null
+++ b/src/hb-subset-cff2-to-cff1.cc
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2026 Behdad Esfahbod
+ *
+ * This is part of HarfBuzz, a text shaping library.
+ *
+ * Permission is hereby granted, without written agreement and without
+ * license or royalty fees, to use, copy, modify, and distribute this
+ * software and its documentation for any purpose, provided that the
+ * above copyright notice and the following two paragraphs appear in
+ * all copies of this software.
+ *
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+ * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
+ * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+ * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+
+/*
+ * CFF2 to CFF1 Converter
+ *
+ * The actual implementation is in hb-subset-cff2.cc where it has access
+ * to the full cff2_subset_plan definition.
+ *
+ * This file just exists to keep the build system happy.
+ */
+
+#include "hb.hh"
+
+#ifndef HB_NO_SUBSET_CFF
+
+#include "hb-subset-cff2-to-cff1.hh"
+
+#endif /* HB_NO_SUBSET_CFF */
diff --git a/src/hb-subset-cff2-to-cff1.hh b/src/hb-subset-cff2-to-cff1.hh
new file mode 100644
index 0000000..6022c00
--- /dev/null
+++ b/src/hb-subset-cff2-to-cff1.hh
@@ -0,0 +1,177 @@
+/*
+ * Copyright © 2026 Behdad Esfahbod
+ *
+ * This is part of HarfBuzz, a text shaping library.
+ *
+ * Permission is hereby granted, without written agreement and without
+ * license or royalty fees, to use, copy, modify, and distribute this
+ * software and its documentation for any purpose, provided that the
+ * above copyright notice and the following two paragraphs appear in
+ * all copies of this software.
+ *
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+ * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
+ * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+ * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+
+#ifndef HB_SUBSET_CFF2_TO_CFF1_HH
+#define HB_SUBSET_CFF2_TO_CFF1_HH
+
+#include "hb.hh"
+
+#ifndef HB_NO_SUBSET_CFF
+
+#include "hb-ot-cff1-table.hh"
+#include "hb-ot-cff2-table.hh"
+#include "hb-subset-cff-common.hh"
+
+namespace OT {
+ // Forward declarations - these are defined in hb-subset-cff2.cc
+ struct cff2_subset_plan;
+}
+
+namespace CFF {
+
+// Forward declaration
+struct cff2_top_dict_values_t;
+
+// Default font name for converted CFF1 fonts
+static constexpr const char CFF1_DEFAULT_FONT_NAME[] = "CFF1Font";
+
+/*
+ * CFF2 to CFF1 Converter
+ *
+ * Converts an instantiated (pinned) CFF2 variable font to CFF1 format.
+ * This is used when instantiating a variable font to a static instance.
+ *
+ * IMPLEMENTATION STATUS:
+ * ✓ CFF1 structure (Header, Name INDEX, String INDEX, Top DICT INDEX)
+ * ✓ ROS operator (makes font CID-keyed: "Adobe-Identity-0")
+ * ✓ FDArray and FDSelect in Top DICT (required for CID fonts)
+ * ✓ FDSelect3 format (compact range-based, 8 bytes for single-FD fonts)
+ * ✓ CID Charset with identity mapping (format 2)
+ * ✓ FontBBox from head table (xMin, yMin, xMax, yMax)
+ * ✓ Width optimization (defaultWidthX/nominalWidthX with O(n) algorithm)
+ * ✓ Width encoding in CharStrings (prepended if != defaultWidthX)
+ * ✓ CharString specialization (h/v operators, combined when possible)
+ * ✓ Stack depth control (generalize→specialize with maxstack=48)
+ * ✓ CharStrings with endchar operators (CFF1 requires, CFF2 doesn't)
+ * ✓ Private DICT instantiation (blend operators evaluated)
+ * ✓ Desubroutinized path (CharStrings are flattened, no subroutines)
+ * ✓ OTS validation passes
+ * ✓ HarfBuzz rendering works
+ *
+ * FUTURE ENHANCEMENTS:
+ * - Curve operator specialization (hhcurveto, vvcurveto, etc.)
+ * - Peephole optimization (minor additional size savings)
+ *
+ * Key conversions:
+ * - Version: 2 -> 1
+ * - Add Name INDEX (required in CFF1)
+ * - Wrap Top DICT in an INDEX (inline in CFF2, indexed in CFF1)
+ * - Add String INDEX ("Adobe", "Identity" for ROS operator)
+ * - Add ROS operator to Top DICT (makes it CID-keyed)
+ * - Add FDSelect to Top DICT (required in CFF1 even with single FD)
+ * - Add endchar to CharStrings (required in CFF1, optional in CFF2)
+ */
+
+struct cff1_subset_plan_from_cff2_t
+{
+ // Inherits most data from cff2_subset_plan
+ const OT::cff2_subset_plan *cff2_plan;
+
+ // CFF1-specific additions
+ hb_vector_t<unsigned char> fontName; // Single font name for Name INDEX
+
+ bool create (const OT::cff2_subset_plan &cff2_plan_)
+ {
+ cff2_plan = &cff2_plan_;
+
+ // Create a simple font name (CFF1 requires a Name INDEX)
+ fontName.resize (strlen (CFF1_DEFAULT_FONT_NAME));
+ if (fontName.in_error ()) return false;
+ memcpy (fontName.arrayZ, CFF1_DEFAULT_FONT_NAME, strlen (CFF1_DEFAULT_FONT_NAME));
+
+ return true;
+ }
+};
+
+/* CFF1 Top DICT operator serializer that adds ROS and removes CFF2-specific ops */
+struct cff1_from_cff2_top_dict_op_serializer_t : cff_top_dict_op_serializer_t<>
+{
+ bool serialize (hb_serialize_context_t *c,
+ const op_str_t &opstr,
+ const cff_sub_table_info_t &info) const
+ {
+ TRACE_SERIALIZE (this);
+
+ switch (opstr.op)
+ {
+ case OpCode_vstore:
+ // CFF2-only operator, skip it
+ return_trace (true);
+
+ case OpCode_CharStrings:
+ return_trace (FontDict::serialize_link4_op(c, opstr.op, info.char_strings_link, whence_t::Absolute));
+
+ case OpCode_FDArray:
+ case OpCode_FDSelect:
+ // These are explicitly serialized in the main function to ensure they're present
+ // even if CFF2 doesn't have them. Skip them here to avoid duplication.
+ return_trace (true);
+
+ default:
+ return_trace (copy_opstr (c, opstr));
+ }
+ }
+
+ // Serialize ROS operator to make this a CID-keyed font
+ bool serialize_ros (hb_serialize_context_t *c) const
+ {
+ TRACE_SERIALIZE (this);
+
+ // ROS = Registry-Ordering-Supplement
+ // We use "Adobe", "Identity", 0 for maximum compatibility
+
+ // Allocate space and encode directly
+ // Registry: SID for "Adobe" (custom string at index 0 = SID 391)
+ // Ordering: SID for "Identity" (custom string at index 1 = SID 392)
+ // Supplement: 0
+ // Note: CFF standard strings end at SID 390, custom strings start at 391
+
+ str_buff_t buff;
+ str_encoder_t encoder (buff);
+
+ encoder.encode_int (391); // Registry SID ("Adobe" in our String INDEX)
+ encoder.encode_int (392); // Ordering SID ("Identity" in our String INDEX)
+ encoder.encode_int (0); // Supplement
+ encoder.encode_op (OpCode_ROS);
+
+ if (encoder.in_error ())
+ return_trace (false);
+
+ auto bytes = buff.as_bytes ();
+ return_trace (c->embed (bytes.arrayZ, bytes.length));
+ }
+};
+
+/* Main serialization function */
+HB_INTERNAL bool
+serialize_cff2_to_cff1 (hb_serialize_context_t *c,
+ OT::cff2_subset_plan &plan,
+ const cff2_top_dict_values_t &cff2_topDict,
+ const OT::cff2::accelerator_subset_t &acc);
+
+} /* namespace CFF */
+
+#endif /* HB_NO_SUBSET_CFF */
+
+#endif /* HB_SUBSET_CFF2_TO_CFF1_HH */
diff --git a/src/hb-subset-cff2.cc b/src/hb-subset-cff2.cc
index eb5cb0c..a92d752 100644
--- a/src/hb-subset-cff2.cc
+++ b/src/hb-subset-cff2.cc
@@ -34,6 +34,7 @@
#include "hb-subset-plan.hh"
#include "hb-subset-cff-common.hh"
#include "hb-cff2-interp-cs.hh"
+#include "hb-subset-cff2-to-cff1.hh"
using namespace CFF;
@@ -73,6 +74,56 @@
{
static void flush_args_and_op (op_code_t op, cff2_cs_interp_env_t<blend_arg_t> &env, flatten_param_t& param)
{
+ /* Optionally capture command for specialization (before flushing, to preserve args) */
+ if (param.commands)
+ {
+ bool skip_command = false;
+
+ switch (op)
+ {
+ case OpCode_return:
+ case OpCode_endchar:
+ skip_command = true;
+ break;
+
+ case OpCode_hstem:
+ case OpCode_hstemhm:
+ case OpCode_vstem:
+ case OpCode_vstemhm:
+ case OpCode_hintmask:
+ case OpCode_cntrmask:
+ if (param.drop_hints)
+ skip_command = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (!skip_command)
+ {
+ cs_command_t cmd (op);
+ /* Capture resolved blend values */
+ for (unsigned int i = 0; i < env.argStack.get_count ();)
+ {
+ const blend_arg_t &arg = env.argStack[i];
+ if (arg.blending ())
+ {
+ /* For blend args, capture only the resolved default value */
+ cmd.args.push (arg);
+ /* Skip over the multiple blend values */
+ i += arg.numValues;
+ }
+ else
+ {
+ cmd.args.push (arg);
+ i++;
+ }
+ }
+ param.commands->push (cmd);
+ }
+ }
+
switch (op)
{
case OpCode_return:
@@ -436,9 +487,15 @@
drop_hints = plan->flags & HB_SUBSET_FLAGS_NO_HINTING;
pinned = (bool) plan->normalized_coords;
+ normalized_coords = plan->normalized_coords;
+ head_maxp_info = plan->head_maxp_info;
+ hmtx_map = &plan->hmtx_map;
desubroutinize = plan->flags & HB_SUBSET_FLAGS_DESUBROUTINIZE ||
pinned; // For instancing we need this path
+ /* Enable command capture for CFF2→CFF1 conversion (for specialization) */
+ capture_commands = pinned;
+
#ifdef HB_EXPERIMENTAL_API
min_charstrings_off_size = (plan->flags & HB_SUBSET_FLAGS_IFTB_REQUIREMENTS) ? 4 : 0;
#else
@@ -450,8 +507,21 @@
/* Flatten global & local subrs */
subr_flattener_t<const OT::cff2::accelerator_subset_t, cff2_cs_interp_env_t<blend_arg_t>, cff2_cs_opset_flatten_t>
flattener(acc, plan);
- if (!flattener.flatten (subset_charstrings))
- return false;
+
+ /* Enable command capture if requested (for specialization) */
+ if (capture_commands)
+ {
+ if (!charstring_commands.resize_exact (num_glyphs))
+ return false;
+
+ if (!flattener.flatten (subset_charstrings, &charstring_commands))
+ return false;
+ }
+ else
+ {
+ if (!flattener.flatten (subset_charstrings))
+ return false;
+ }
}
else
{
@@ -518,9 +588,483 @@
bool desubroutinize = false;
unsigned min_charstrings_off_size = 0;
+
+ hb_array_t<int> normalized_coords; // For instantiation
+ head_maxp_info_t head_maxp_info; // For FontBBox
+ const hb_hashmap_t<hb_codepoint_t, hb_pair_t<unsigned, int>> *hmtx_map; // For widths
+
+ // Width optimization results (for CFF1 conversion)
+ unsigned default_width = 0;
+ unsigned nominal_width = 0;
+
+ // Command capture for specialization (CFF2→CFF1 conversion)
+ bool capture_commands = false;
+ hb_vector_t<hb_vector_t<cs_command_t>> charstring_commands;
};
} // namespace OT
+/*
+ * CFF2 to CFF1 Converter Implementation
+ */
+
+#include "hb-cff-width-optimizer.hh"
+#include "hb-cff-specializer.hh"
+
+/* Serialize charstrings using CFF1 format with widths */
+static bool
+_serialize_cff1_charstrings (hb_serialize_context_t *c,
+ OT::cff2_subset_plan &plan,
+ unsigned default_width,
+ unsigned nominal_width)
+{
+ c->push ();
+
+ // CFF1 requires:
+ // 1. Width at the beginning (if != defaultWidthX)
+ // 2. endchar at the end
+ str_buff_vec_t cff1_charstrings;
+ if (unlikely (!cff1_charstrings.resize (plan.subset_charstrings.length)))
+ {
+ c->pop_discard ();
+ return false;
+ }
+
+ for (unsigned i = 0; i < plan.subset_charstrings.length; i++)
+ {
+ // Get width for this glyph from hmtx_map
+ unsigned width = 0;
+ if (plan.hmtx_map->has (i))
+ width = plan.hmtx_map->get (i).first;
+
+ // Encode width if different from default
+ str_encoder_t encoder (cff1_charstrings[i]);
+ if (width != default_width)
+ {
+ int delta = (int) width - (int) nominal_width;
+ encoder.encode_int (delta);
+ }
+
+ // Use specialized commands if available, otherwise use binary
+ if (plan.capture_commands && i < plan.charstring_commands.length &&
+ plan.charstring_commands[i].length > 0)
+ {
+ // Specialize and encode commands
+ auto &commands = plan.charstring_commands[i];
+ CFF::specialize_commands (commands, 48); /* maxstack=48 for CFF1 */
+ if (unlikely (!CFF::encode_commands (commands, cff1_charstrings[i])))
+ {
+ c->pop_discard ();
+ return false;
+ }
+ }
+ else
+ {
+ // Use binary CharString
+ const str_buff_t &cs = plan.subset_charstrings[i];
+ for (unsigned j = 0; j < cs.length; j++)
+ cff1_charstrings[i].push (cs[j]);
+ }
+
+ // Check if it already ends with endchar (0x0e) or return (0x0b)
+ if (cff1_charstrings[i].length == 0 ||
+ (cff1_charstrings[i].tail () != 0x0e && cff1_charstrings[i].tail () != 0x0b))
+ {
+ // Append endchar operator
+ if (unlikely (!cff1_charstrings[i].push (0x0e)))
+ {
+ c->pop_discard ();
+ return false;
+ }
+ }
+ }
+
+ unsigned data_size = 0;
+ unsigned total_size = CFF1CharStrings::total_size (cff1_charstrings, &data_size);
+ if (unlikely (!c->start_zerocopy (total_size)))
+ {
+ c->pop_discard ();
+ return false;
+ }
+
+ auto *cs = c->start_embed<CFF1CharStrings> ();
+ if (unlikely (!cs->serialize (c, cff1_charstrings)))
+ {
+ c->pop_discard ();
+ return false;
+ }
+
+ plan.info.char_strings_link = c->pop_pack (false);
+ return true;
+}
+
+/* Serialize CID Charset (format 2 range: gid 0-N -> cid 0-N) */
+static bool
+_serialize_cff1_charset (hb_serialize_context_t *c,
+ unsigned int num_glyphs,
+ objidx_t &charset_link)
+{
+ // For CID fonts, create a simple identity charset
+ // Format 2: one range covering all glyphs (except .notdef)
+ c->push ();
+
+ auto *charset = c->start_embed<Charset> ();
+ if (unlikely (!charset))
+ {
+ c->pop_discard ();
+ return false;
+ }
+
+ // Create a single range for CID 1 to num_glyphs-1
+ hb_vector_t<code_pair_t> ranges;
+ if (num_glyphs > 1)
+ {
+ code_pair_t range;
+ range.code = 1; // first CID
+ range.glyph = num_glyphs - 2; // nLeft (covers glyphs 1 to num_glyphs-1)
+ ranges.push (range);
+ }
+
+ if (unlikely (!charset->serialize (c, 2, num_glyphs, ranges)))
+ {
+ c->pop_discard ();
+ return false;
+ }
+
+ charset_link = c->pop_pack ();
+ return true;
+}
+
+/* CFF2 to CFF1 serialization */
+namespace CFF {
+
+bool
+serialize_cff2_to_cff1 (hb_serialize_context_t *c,
+ OT::cff2_subset_plan &plan,
+ const cff2_top_dict_values_t &cff2_topDict,
+ const OT::cff2::accelerator_subset_t &acc)
+{
+ TRACE_SERIALIZE (this);
+
+ /*
+ * CFF1 Serialization Order (reverse, as HarfBuzz packs from end):
+ * 1. CharStrings
+ * 2. Private DICs & Local Subrs
+ * 3. FDArray
+ * 4. FDSelect
+ * 5. Charset
+ * 6. Global Subrs
+ * 7. String INDEX
+ * 8. Top DICT INDEX
+ * 9. Name INDEX
+ * 10. Header
+ */
+
+ // 0. Optimize width encoding (for all FDs)
+ {
+ // Collect widths from hmtx_map
+ hb_vector_t<unsigned> widths;
+ widths.alloc (plan.num_glyphs);
+
+ for (unsigned gid = 0; gid < plan.num_glyphs; gid++)
+ {
+ unsigned width = 0;
+ if (plan.hmtx_map->has (gid))
+ width = plan.hmtx_map->get (gid).first;
+ widths.push (width);
+ }
+
+ // Optimize defaultWidthX and nominalWidthX
+ CFF::optimize_widths (widths, plan.default_width, plan.nominal_width);
+ }
+
+ // 1. CharStrings (with widths prepended)
+ if (!_serialize_cff1_charstrings (c, plan, plan.default_width, plan.nominal_width))
+ return_trace (false);
+
+ // 2. Private DICs & Local Subrs (same as CFF2)
+ hb_vector_t<table_info_t> private_dict_infos;
+ if (unlikely (!private_dict_infos.resize (plan.subset_fdcount)))
+ return_trace (false);
+
+ for (int i = (int)acc.privateDicts.length; --i >= 0;)
+ {
+ if (plan.fdmap.has (i))
+ {
+ objidx_t subrs_link = 0;
+
+ if (plan.subset_localsubrs[i].length > 0)
+ {
+ auto *dest = c->push<CFF1Subrs> ();
+ if (likely (dest->serialize (c, plan.subset_localsubrs[i])))
+ subrs_link = c->pop_pack (false);
+ else
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+
+ auto *pd = c->push<PrivateDict> ();
+ // Use the CFF2 Private DICT serializer which instantiates blends when pinned=true
+ cff2_private_dict_op_serializer_t privSzr (plan.desubroutinize, plan.drop_hints, plan.pinned,
+ acc.varStore, plan.normalized_coords);
+ if (likely (pd->serialize (c, acc.privateDicts[i], privSzr, subrs_link)))
+ {
+ // Add defaultWidthX and nominalWidthX for CFF1
+ str_buff_t width_ops;
+ str_encoder_t encoder (width_ops);
+ encoder.encode_int (plan.default_width);
+ encoder.encode_op (OpCode_defaultWidthX);
+ encoder.encode_int (plan.nominal_width);
+ encoder.encode_op (OpCode_nominalWidthX);
+
+ if (!encoder.in_error () && c->embed (width_ops.as_bytes ().arrayZ, width_ops.length))
+ {
+ unsigned fd = plan.fdmap[i];
+ private_dict_infos[fd].size = c->length ();
+ private_dict_infos[fd].link = c->pop_pack ();
+ }
+ else
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+ else
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+ }
+
+ // 3. FDArray - serialize CFF2 font dicts as CFF1
+ {
+ auto *fda = c->push<FDArray<HBUINT16>> ();
+ cff_font_dict_op_serializer_t fontSzr;
+ auto it =
+ + hb_zip (+ hb_iter (acc.fontDicts)
+ | hb_filter ([&] (const cff2_font_dict_values_t &_)
+ { return plan.fdmap.has (&_ - &acc.fontDicts[0]); }),
+ hb_iter (private_dict_infos))
+ ;
+ // Explicitly specify template parameters: DICTVAL, INFO
+ bool success = fda->serialize<cff2_font_dict_values_t, table_info_t> (c, it, fontSzr);
+ if (success)
+ plan.info.fd_array_link = c->pop_pack (false);
+ else
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+
+ // 4. FDSelect (required in CFF1 CID-keyed fonts)
+ // CFF1 requires FDSelect for all CID-keyed fonts, even with just one FD
+ // CFF2 makes it optional when there's only one FD
+ if (acc.fdSelect != &Null (CFF2FDSelect))
+ {
+ c->push ();
+ if (likely (hb_serialize_cff_fdselect (c, plan.num_glyphs,
+ *(const FDSelect *)acc.fdSelect,
+ plan.orig_fdcount,
+ plan.subset_fdselect_format,
+ plan.subset_fdselect_size,
+ plan.subset_fdselect_ranges)))
+ plan.info.fd_select.link = c->pop_pack ();
+ else
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+ else
+ {
+ // Create a range-based FDSelect3 mapping all glyphs to FD 0
+ // Format: format(1) + nRanges(2) + range(3) + sentinel(2) = 8 bytes
+ c->push ();
+
+ // Format byte
+ HBUINT8 format;
+ format = 3;
+ if (unlikely (!c->embed (format)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ // nRanges
+ HBUINT16 nRanges;
+ nRanges = 1;
+ if (unlikely (!c->embed (nRanges)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ // Single range: {first: 0, fd: 0}
+ FDSelect3_Range range;
+ range.first = 0;
+ range.fd = 0;
+ if (unlikely (!c->embed (range)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ // Sentinel (number of glyphs)
+ HBUINT16 sentinel;
+ sentinel = plan.num_glyphs;
+ if (unlikely (!c->embed (sentinel)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ plan.info.fd_select.link = c->pop_pack ();
+ }
+
+ // 5. Charset (CID charset for identity mapping)
+ objidx_t charset_link;
+ if (!_serialize_cff1_charset (c, plan.num_glyphs, charset_link))
+ return_trace (false);
+
+ // 6. Global Subrs
+ {
+ auto *dest = c->push<CFF1Subrs> ();
+ if (likely (dest->serialize (c, plan.subset_globalsubrs)))
+ c->pop_pack (false);
+ else
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+
+ // 7. String INDEX - Add "Adobe" and "Identity" for ROS operator
+ {
+ const char *adobe_str = "Adobe";
+ const char *identity_str = "Identity";
+ unsigned adobe_len = 5; // strlen("Adobe")
+ unsigned identity_len = 8; // strlen("Identity")
+
+ // Build strings array
+ hb_vector_t<hb_ubytes_t> strings;
+ strings.alloc (2);
+ strings.push (hb_ubytes_t ((const unsigned char *) adobe_str, adobe_len));
+ strings.push (hb_ubytes_t ((const unsigned char *) identity_str, identity_len));
+
+ // Serialize as CFF INDEX
+ auto *dest = c->push<CFF1Index> ();
+ if (likely (dest->serialize (c, strings)))
+ c->pop_pack (false);
+ else
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+
+ // 8. CFF Header
+ OT::cff1 *cff = c->allocate_min<OT::cff1> ();
+ if (unlikely (!cff)) return_trace (false);
+
+ /* header */
+ cff->version.major = 0x01;
+ cff->version.minor = 0x00;
+ cff->nameIndex = cff->min_size;
+ cff->offSize = 4; /* unused? */
+
+ // 9. Name INDEX (single entry)
+ {
+ unsigned name_len = strlen (CFF1_DEFAULT_FONT_NAME);
+
+ CFF1Index *idx = c->start_embed<CFF1Index> ();
+ if (unlikely (!idx)) return_trace (false);
+
+ if (unlikely (!idx->serialize_header (c, hb_iter (&name_len, 1), name_len)))
+ return_trace (false);
+
+ if (unlikely (!c->embed (CFF1_DEFAULT_FONT_NAME, name_len)))
+ return_trace (false);
+ }
+
+ // 10. Top DICT INDEX
+ {
+ // Serialize the Top DICT data first
+ c->push<TopDict> ();
+ cff1_from_cff2_top_dict_op_serializer_t topSzr;
+
+ // Serialize ROS first
+ if (unlikely (!topSzr.serialize_ros (c)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ // Serialize FontBBox from head table
+ {
+ str_buff_t bbox_buff;
+ str_encoder_t encoder (bbox_buff);
+
+ encoder.encode_int (plan.head_maxp_info.xMin);
+ encoder.encode_int (plan.head_maxp_info.yMin);
+ encoder.encode_int (plan.head_maxp_info.xMax);
+ encoder.encode_int (plan.head_maxp_info.yMax);
+ encoder.encode_op (OpCode_FontBBox);
+
+ if (encoder.in_error () || !c->embed (bbox_buff.as_bytes ().arrayZ, bbox_buff.length))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+
+ // Serialize charset operator
+ if (charset_link && unlikely (!FontDict::serialize_link4_op (c, OpCode_charset, charset_link, whence_t::Absolute)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ // Serialize FDSelect operator (required for CID-keyed CFF1 fonts)
+ if (plan.info.fd_select.link && unlikely (!FontDict::serialize_link4_op (c, OpCode_FDSelect, plan.info.fd_select.link, whence_t::Absolute)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ // Serialize FDArray operator (required for CID-keyed CFF1 fonts)
+ if (plan.info.fd_array_link && unlikely (!FontDict::serialize_link4_op (c, OpCode_FDArray, plan.info.fd_array_link, whence_t::Absolute)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+
+ // Serialize other operators from CFF2 TopDict
+ for (const auto &opstr : cff2_topDict.values)
+ {
+ if (unlikely (!topSzr.serialize (c, opstr, plan.info)))
+ {
+ c->pop_discard ();
+ return_trace (false);
+ }
+ }
+
+ unsigned top_size = c->length ();
+ c->pop_pack (false);
+
+ // Serialize INDEX header
+ auto *dest = c->start_embed<CFF1Index> ();
+ if (unlikely (!dest->serialize_header (c, hb_iter (&top_size, 1), top_size)))
+ return_trace (false);
+ }
+
+ return_trace (true);
+}
+
+} /* namespace CFF */
+
static bool _serialize_cff2_charstrings (hb_serialize_context_t *c,
cff2_subset_plan &plan,
const OT::cff2::accelerator_subset_t &acc)
@@ -669,6 +1213,38 @@
cff2_subset_plan cff2_plan;
if (unlikely (!cff2_plan.create (*this, c->plan))) return false;
+
+ // If instantiating (pinned) and downgrade flag is set, convert to CFF1
+ if (cff2_plan.pinned && (c->plan->flags & HB_SUBSET_FLAGS_DOWNGRADE_CFF2))
+ {
+ // Serialize CFF1 to the subsetter's serializer
+ // If we run out of room, returning true will cause subsetter to retry with larger buffer
+ bool result = CFF::serialize_cff2_to_cff1 (c->serializer, cff2_plan, topDict, *this);
+
+ if (c->serializer->ran_out_of_room ())
+ return true; // Subsetter will retry with larger buffer
+
+ if (result && !c->serializer->in_error ())
+ {
+ // Success - end serialization to resolve links
+ c->serializer->end_serialize ();
+
+ // Copy the serialized CFF1 data and add as CFF table
+ hb_blob_t *cff_blob = c->serializer->copy_blob ();
+ if (cff_blob)
+ {
+ c->plan->add_table (HB_TAG('C','F','F',' '), cff_blob);
+ hb_blob_destroy (cff_blob);
+
+ // Return false to signal CFF2 table is not needed
+ return false;
+ }
+ }
+
+ // Conversion failed - don't fall back, fail hard for debugging
+ return false;
+ }
+
return serialize (c->serializer, cff2_plan,
c->plan->normalized_coords.as_array ());
}
diff --git a/src/hb-subset.h b/src/hb-subset.h
index bc55b6d..2b9b2a2 100644
--- a/src/hb-subset.h
+++ b/src/hb-subset.h
@@ -84,6 +84,9 @@
* HB_SUBSET_FLAGS_RETAIN_GIDS then the number of glyphs in the font won't
* be reduced as a result of subsetting. If necessary empty glyphs will be
* included at the end of the font to keep the number of glyphs unchanged.
+ * @HB_SUBSET_FLAGS_DOWNGRADE_CFF2: If set and instantiating a variable font,
+ * convert the output CFF2 table to CFF1. This enables compatibility with older
+ * renderers that don't support CFF2. Since: REPLACEME
*
* List of boolean properties that can be configured on the subset input.
*
@@ -107,6 +110,7 @@
HB_SUBSET_FLAGS_IFTB_REQUIREMENTS = 0x00001000u,
HB_SUBSET_FLAGS_RETAIN_NUM_GLYPHS = 0x00002000u,
#endif
+ HB_SUBSET_FLAGS_DOWNGRADE_CFF2 = 0x00004000u,
} hb_subset_flags_t;
/**
diff --git a/src/meson.build b/src/meson.build
index 44c858c..49217aa 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -395,6 +395,8 @@
'hb-subset-cff-common.hh',
'hb-subset-cff1.cc',
'hb-subset-cff2.cc',
+ 'hb-subset-cff2-to-cff1.cc',
+ 'hb-subset-cff2-to-cff1.hh',
'hb-subset-input.cc',
'hb-subset-input.hh',
'hb-subset-instancer-iup.hh',
diff --git a/util/hb-subset.cc b/util/hb-subset.cc
index e790fc7..8c6058d 100644
--- a/util/hb-subset.cc
+++ b/util/hb-subset.cc
@@ -980,6 +980,7 @@
{"no-hinting", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &set_flag<HB_SUBSET_FLAGS_NO_HINTING>, "Whether to drop hints", nullptr},
{"retain-gids", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &set_flag<HB_SUBSET_FLAGS_RETAIN_GIDS>, "If set don't renumber glyph ids in the subset.", nullptr},
{"desubroutinize", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &set_flag<HB_SUBSET_FLAGS_DESUBROUTINIZE>, "Remove CFF/CFF2 use of subroutines", nullptr},
+ {"downgrade-cff2", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &set_flag<HB_SUBSET_FLAGS_DOWNGRADE_CFF2>, "Convert instantiated variable fonts from CFF2 to CFF1", nullptr},
{"name-legacy", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &set_flag<HB_SUBSET_FLAGS_NAME_LEGACY>, "Keep legacy (non-Unicode) 'name' table entries", nullptr},
{"set-overlaps-flag", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &set_flag<HB_SUBSET_FLAGS_SET_OVERLAPS_FLAG>, "Set the overlaps flag on each glyph.", nullptr},
{"notdef-outline", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, (gpointer) &set_flag<HB_SUBSET_FLAGS_NOTDEF_OUTLINE>, "Keep the outline of \'.notdef\' glyph", nullptr},