| /* |
| * 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 */ |