fontisan 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +119 -308
  3. data/README.adoc +1525 -1323
  4. data/Rakefile +45 -47
  5. data/benchmark/variation_quick_bench.rb +4 -4
  6. data/docs/FONT_HINTING.adoc +562 -0
  7. data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
  8. data/lib/fontisan/cli.rb +92 -34
  9. data/lib/fontisan/collection/builder.rb +82 -0
  10. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  11. data/lib/fontisan/collection/table_deduplicator.rb +76 -0
  12. data/lib/fontisan/commands/base_command.rb +21 -2
  13. data/lib/fontisan/commands/convert_command.rb +96 -165
  14. data/lib/fontisan/commands/info_command.rb +111 -5
  15. data/lib/fontisan/commands/instance_command.rb +77 -85
  16. data/lib/fontisan/commands/validate_command.rb +28 -0
  17. data/lib/fontisan/config/validation_rules.yml +1 -1
  18. data/lib/fontisan/constants.rb +34 -24
  19. data/lib/fontisan/converters/format_converter.rb +154 -1
  20. data/lib/fontisan/converters/outline_converter.rb +101 -34
  21. data/lib/fontisan/converters/woff_writer.rb +9 -4
  22. data/lib/fontisan/font_loader.rb +14 -9
  23. data/lib/fontisan/font_writer.rb +9 -6
  24. data/lib/fontisan/formatters/text_formatter.rb +45 -1
  25. data/lib/fontisan/hints/hint_converter.rb +131 -2
  26. data/lib/fontisan/hints/hint_validator.rb +284 -0
  27. data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
  28. data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
  29. data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
  30. data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
  31. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  32. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  33. data/lib/fontisan/loading_modes.rb +6 -4
  34. data/lib/fontisan/models/collection_brief_info.rb +31 -0
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +183 -12
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/open_type_font.rb +28 -10
  39. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  40. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  41. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  42. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  43. data/lib/fontisan/pipeline/output_writer.rb +159 -0
  44. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  45. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  46. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  47. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  48. data/lib/fontisan/pipeline/transformation_pipeline.rb +416 -0
  49. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  51. data/lib/fontisan/tables/cff/charstring.rb +58 -3
  52. data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
  53. data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
  54. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  55. data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
  56. data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
  57. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  58. data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
  59. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  60. data/lib/fontisan/tables/cff.rb +2 -0
  61. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  62. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
  63. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  64. data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
  65. data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
  66. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  67. data/lib/fontisan/tables/cff2.rb +10 -5
  68. data/lib/fontisan/tables/cvar.rb +2 -41
  69. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  70. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  71. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  72. data/lib/fontisan/tables/gvar.rb +2 -41
  73. data/lib/fontisan/tables/name.rb +4 -4
  74. data/lib/fontisan/true_type_font.rb +27 -10
  75. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  76. data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
  77. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  78. data/lib/fontisan/validation/table_validator.rb +1 -1
  79. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  80. data/lib/fontisan/variation/cache.rb +3 -1
  81. data/lib/fontisan/variation/converter.rb +121 -13
  82. data/lib/fontisan/variation/delta_applier.rb +2 -1
  83. data/lib/fontisan/variation/inspector.rb +2 -1
  84. data/lib/fontisan/variation/instance_generator.rb +2 -1
  85. data/lib/fontisan/variation/instance_writer.rb +341 -0
  86. data/lib/fontisan/variation/optimizer.rb +6 -3
  87. data/lib/fontisan/variation/subsetter.rb +32 -10
  88. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  89. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  90. data/lib/fontisan/variation/variation_preserver.rb +291 -0
  91. data/lib/fontisan/version.rb +1 -1
  92. data/lib/fontisan/version.rb.orig +9 -0
  93. data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
  94. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  95. data/lib/fontisan/woff2_font.rb +489 -468
  96. data/lib/fontisan/woff_font.rb +16 -11
  97. data/lib/fontisan.rb +54 -2
  98. data/scripts/measure_optimization.rb +15 -7
  99. metadata +37 -2
@@ -1,184 +1,263 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../models/hint"
4
+ require "json"
4
5
 
5
6
  module Fontisan
6
7
  module Hints
7
- # Applies rendering hints to PostScript/CFF CharString data
8
+ # Applies rendering hints to PostScript/CFF font tables
8
9
  #
9
- # This applier converts universal Hint objects into PostScript hint
10
- # operators and integrates them into CharString data. It ensures proper
11
- # operator placement and maintains CharString validity.
10
+ # This applier validates and applies PostScript hint data to CFF fonts by
11
+ # rebuilding the entire CFF table structure with updated Private DICT parameters.
12
12
  #
13
- # **PostScript Hint Placement:**
13
+ # **Status**: Fully Operational (Phase 10A Complete)
14
14
  #
15
- # - Stem hints (hstem/vstem) must appear at the beginning
16
- # - Hintmask operators can appear throughout the CharString
17
- # - Hints affect all subsequent path operations
15
+ # **PostScript Hint Parameters (Private DICT)**:
18
16
  #
19
- # @example Apply hints to a CharString
17
+ # - blue_values: Alignment zones for overshoot suppression
18
+ # - other_blues: Additional alignment zones
19
+ # - std_hw: Standard horizontal stem width
20
+ # - std_vw: Standard vertical stem width
21
+ # - stem_snap_h: Horizontal stem snap widths
22
+ # - stem_snap_v: Vertical stem snap widths
23
+ # - blue_scale, blue_shift, blue_fuzz: Overshoot parameters
24
+ # - force_bold: Force bold flag
25
+ # - language_group: Language group (0=Latin, 1=CJK)
26
+ #
27
+ # @example Apply PostScript hints
20
28
  # applier = PostScriptHintApplier.new
21
- # charstring_with_hints = applier.apply(charstring, hints)
29
+ # tables = { "CFF " => cff_table }
30
+ # hint_set = HintSet.new(format: "postscript", private_dict_hints: hints_json)
31
+ # result = applier.apply(hint_set, tables)
22
32
  class PostScriptHintApplier
23
- # CFF CharString operators
24
- HSTEM = 1
25
- VSTEM = 3
26
- HINTMASK = 19
27
- CNTRMASK = 20
28
- HSTEM3 = [12, 2]
29
- VSTEM3 = [12, 1]
30
-
31
- # Apply hints to CharString
33
+ # Apply PostScript hints to font tables
34
+ #
35
+ # Validates hint data and rebuilds CFF table with updated Private DICT.
36
+ # Supports arbitrary Private DICT size changes through full table reconstruction.
37
+ # Also supports per-glyph hints injected directly into CharStrings.
32
38
  #
33
- # @param charstring [String] Original CharString bytes
34
- # @param hints [Array<Hint>] Hints to apply
35
- # @return [String] CharString with applied hints
36
- def apply(charstring, hints)
37
- return charstring if hints.nil? || hints.empty?
38
- return charstring if charstring.nil? || charstring.empty?
39
-
40
- # Build hint operators
41
- hint_ops = build_hint_operators(hints)
42
-
43
- # Insert hints at the beginning of CharString
44
- # (simplified - real implementation would analyze existing structure)
45
- hint_ops + charstring
39
+ # @param hint_set [HintSet] Hint data to apply
40
+ # @param tables [Hash] Font tables (must include "CFF " or "CFF2 ")
41
+ # @return [Hash] Updated font tables with hints applied
42
+ def apply(hint_set, tables)
43
+ return tables if hint_set.nil? || hint_set.empty?
44
+ return tables unless hint_set.format == "postscript"
45
+
46
+ if cff2_table?(tables)
47
+ apply_cff2_hints(hint_set, tables)
48
+ elsif cff_table?(tables)
49
+ apply_cff_hints(hint_set, tables)
50
+ else
51
+ tables
52
+ end
46
53
  end
47
54
 
48
55
  private
49
56
 
50
- # Build hint operators from hints
57
+ # Check if tables contain CFF2 table
51
58
  #
52
- # @param hints [Array<Hint>] Hints to convert
53
- # @return [String] Hint operator bytes
54
- def build_hint_operators(hints)
55
- operators = "".b
56
-
57
- # Group hints by type for proper ordering
58
- stem_hints = hints.select { |h| h.type == :stem }
59
- stem3_hints = hints.select { |h| h.type == :stem3 }
60
- mask_hints = hints.select { |h| %i[hint_replacement counter].include?(h.type) }
61
-
62
- # Add stem hints first
63
- stem_hints.each do |hint|
64
- operators << encode_stem_hint(hint)
65
- end
59
+ # @param tables [Hash] Font tables
60
+ # @return [Boolean] True if CFF2 table present
61
+ def cff2_table?(tables)
62
+ tables.key?("CFF2") || tables.key?("CFF2 ")
63
+ end
66
64
 
67
- # Add stem3 hints
68
- stem3_hints.each do |hint|
69
- operators << encode_stem3_hint(hint)
70
- end
65
+ # Check if tables contain CFF table
66
+ #
67
+ # @param tables [Hash] Font tables
68
+ # @return [Boolean] True if CFF table present
69
+ def cff_table?(tables)
70
+ tables.key?("CFF ")
71
+ end
71
72
 
72
- # Add mask hints
73
- mask_hints.each do |hint|
74
- operators << encode_mask_hint(hint)
75
- end
73
+ # Apply hints to CFF2 variable font
74
+ #
75
+ # @param hint_set [HintSet] Hint set with font-level and per-glyph hints
76
+ # @param tables [Hash] Font tables
77
+ # @return [Hash] Updated tables
78
+ def apply_cff2_hints(hint_set, tables)
79
+ # Load CFF2 table
80
+ cff2_data = tables["CFF2"] || tables["CFF2 "]
76
81
 
77
- operators
82
+ begin
83
+ require_relative "../tables/cff2/table_reader"
84
+ require_relative "../tables/cff2/table_builder"
85
+
86
+ reader = Tables::Cff2::TableReader.new(cff2_data)
87
+
88
+ # Validate CFF2 version
89
+ reader.read_header
90
+ unless reader.header[:major_version] == 2
91
+ warn "Invalid CFF2 table version: #{reader.header[:major_version]}"
92
+ return tables
93
+ end
94
+
95
+ # Build with hints
96
+ builder = Tables::Cff2::TableBuilder.new(reader, hint_set)
97
+ modified_table = builder.build
98
+
99
+ # Update tables
100
+ table_key = tables.key?("CFF2") ? "CFF2" : "CFF2 "
101
+ tables[table_key] = modified_table
102
+
103
+ tables
104
+ rescue StandardError => e
105
+ warn "Error applying CFF2 hints: #{e.message}"
106
+ tables
107
+ end
78
108
  end
79
109
 
80
- # Encode stem hint as CharString bytes
110
+ # Apply hints to CFF font
81
111
  #
82
- # @param hint [Hint] Stem hint
83
- # @return [String] Encoded bytes
84
- def encode_stem_hint(hint)
85
- data = hint.to_postscript
86
- return "".b if data.empty?
87
-
88
- args = data[:args] || []
89
- operator = data[:operator]
90
-
91
- # Encode arguments as CFF integers
92
- bytes = args.map { |arg| encode_cff_integer(arg) }.join
93
-
94
- # Add operator
95
- bytes << if operator == :vstem
96
- [VSTEM].pack("C")
97
- else
98
- [HSTEM].pack("C")
99
- end
100
-
101
- bytes
112
+ # @param hint_set [HintSet] Hint set with font-level and per-glyph hints
113
+ # @param tables [Hash] Font tables
114
+ # @return [Hash] Updated tables
115
+ def apply_cff_hints(hint_set, tables)
116
+ return tables unless tables["CFF "]
117
+
118
+ # Validate hint parameters (Private DICT)
119
+ hint_params = parse_hint_parameters(hint_set)
120
+
121
+ # Check if we have per-glyph hints
122
+ has_per_glyph_hints = hint_set.hinted_glyph_count.positive?
123
+
124
+ # If neither font-level nor per-glyph hints, return unchanged
125
+ return tables unless hint_params || has_per_glyph_hints
126
+
127
+ # Validate font-level parameters if present
128
+ if hint_params && !valid_hint_parameters?(hint_params)
129
+ return tables
130
+ end
131
+
132
+ # Apply hints (both font-level and per-glyph)
133
+ begin
134
+ require_relative "../tables/cff/table_builder"
135
+ require_relative "../tables/cff/charstring_rebuilder"
136
+ require_relative "../tables/cff/hint_operation_injector"
137
+ require_relative "../tables/cff"
138
+
139
+ # Parse CFF binary data into Cff object if needed
140
+ cff_data = tables["CFF "]
141
+ cff_table = if cff_data.is_a?(Tables::Cff)
142
+ cff_data
143
+ else
144
+ Tables::Cff.read(cff_data)
145
+ end
146
+
147
+ # Prepare per-glyph hint data if present
148
+ per_glyph_hints = if has_per_glyph_hints
149
+ extract_per_glyph_hints(hint_set)
150
+ end
151
+
152
+ new_cff_data = Tables::Cff::TableBuilder.rebuild(
153
+ cff_table,
154
+ private_dict_hints: hint_params,
155
+ per_glyph_hints: per_glyph_hints,
156
+ )
157
+
158
+ tables["CFF "] = new_cff_data
159
+ tables
160
+ rescue StandardError => e
161
+ warn "Failed to apply PostScript hints: #{e.message}"
162
+ tables
163
+ end
102
164
  end
103
165
 
104
- # Encode stem3 hint as CharString bytes
166
+ # Parse hint parameters from HintSet
105
167
  #
106
- # @param hint [Hint] Stem3 hint
107
- # @return [String] Encoded bytes
108
- def encode_stem3_hint(hint)
109
- data = hint.to_postscript
110
- return "".b if data.empty?
111
-
112
- args = data[:args] || []
113
- operator = data[:operator]
114
-
115
- # Encode arguments
116
- bytes = args.map { |arg| encode_cff_integer(arg) }.join
117
-
118
- # Add two-byte operator (12 followed by subop)
119
- bytes << if operator == :vstem3
120
- VSTEM3.pack("C*")
121
- else
122
- HSTEM3.pack("C*")
123
- end
124
-
125
- bytes
168
+ # @param hint_set [HintSet] Hint set with Private dict hints
169
+ # @return [Hash, nil] Parsed hint parameters, or nil if invalid
170
+ def parse_hint_parameters(hint_set)
171
+ return nil unless hint_set.private_dict_hints
172
+ return nil if hint_set.private_dict_hints == "{}"
173
+
174
+ JSON.parse(hint_set.private_dict_hints)
175
+ rescue JSON::ParserError => e
176
+ warn "Failed to parse Private dict hints: #{e.message}"
177
+ nil
126
178
  end
127
179
 
128
- # Encode mask hint as CharString bytes
180
+ # Validate hint parameters against CFF specification limits
129
181
  #
130
- # @param hint [Hint] Mask hint
131
- # @return [String] Encoded bytes
132
- def encode_mask_hint(hint)
133
- operator = hint.type == :hint_replacement ? HINTMASK : CNTRMASK
134
- mask = hint.data[:mask] || []
182
+ # @param params [Hash] Hint parameters
183
+ # @return [Boolean] True if all parameters are valid
184
+ def valid_hint_parameters?(params)
185
+ # Validate blue values (must be pairs, max 7 pairs = 14 values)
186
+ if params["blue_values"] || params[:blue_values]
187
+ values = params["blue_values"] || params[:blue_values]
188
+ return false unless values.is_a?(Array)
189
+ return false if values.length > 14 # Max 7 pairs
190
+ return false if values.length.odd? # Must be pairs
191
+ end
135
192
 
136
- # Encode mask bytes
137
- bytes = mask.map { |b| [b].pack("C") }.join
193
+ # Validate other_blues (max 5 pairs = 10 values)
194
+ if params["other_blues"] || params[:other_blues]
195
+ values = params["other_blues"] || params[:other_blues]
196
+ return false unless values.is_a?(Array)
197
+ return false if values.length > 10
198
+ return false if values.length.odd?
199
+ end
138
200
 
139
- # Add operator
140
- bytes + [operator].pack("C")
141
- end
201
+ # Validate stem widths (single values)
202
+ if params["std_hw"] || params[:std_hw]
203
+ value = params["std_hw"] || params[:std_hw]
204
+ return false unless value.is_a?(Numeric)
205
+ return false if value.negative?
206
+ end
142
207
 
143
- # Encode integer as CFF CharString number
144
- #
145
- # @param num [Integer] Number to encode
146
- # @return [String] Encoded bytes
147
- def encode_cff_integer(num)
148
- # Range 1: -107 to 107 (single byte)
149
- if num >= -107 && num <= 107
150
- return [32 + num].pack("c")
208
+ if params["std_vw"] || params[:std_vw]
209
+ value = params["std_vw"] || params[:std_vw]
210
+ return false unless value.is_a?(Numeric)
211
+ return false if value.negative?
212
+ end
213
+
214
+ # Validate stem snaps (arrays, max 12 values each)
215
+ %w[stem_snap_h stem_snap_v].each do |key|
216
+ next unless params[key] || params[key.to_sym]
217
+
218
+ values = params[key] || params[key.to_sym]
219
+ return false unless values.is_a?(Array)
220
+ return false if values.length > 12
151
221
  end
152
222
 
153
- # Range 2: 108 to 1131 (two bytes)
154
- if num >= 108 && num <= 1131
155
- b0 = 247 + ((num - 108) >> 8)
156
- b1 = (num - 108) & 0xff
157
- return [b0, b1].pack("C*")
223
+ # Validate blue_scale (should be positive)
224
+ if params["blue_scale"] || params[:blue_scale]
225
+ value = params["blue_scale"] || params[:blue_scale]
226
+ return false unless value.is_a?(Numeric)
227
+ return false if value <= 0
158
228
  end
159
229
 
160
- # Range 3: -1131 to -108 (two bytes)
161
- if num >= -1131 && num <= -108
162
- b0 = 251 - ((num + 108) >> 8)
163
- b1 = -(num + 108) & 0xff
164
- return [b0, b1].pack("C*")
230
+ # Validate language_group (0 or 1 only)
231
+ if params["language_group"] || params[:language_group]
232
+ value = params["language_group"] || params[:language_group]
233
+ return false unless [0, 1].include?(value)
165
234
  end
166
235
 
167
- # Range 4: -32768 to 32767 (three bytes)
168
- if num >= -32_768 && num <= 32_767
169
- bytes = [28, (num >> 8) & 0xff, num & 0xff]
170
- return bytes.pack("C*")
236
+ true
237
+ end
238
+
239
+ # Extract specific hint parameter with symbol/string key support
240
+ #
241
+ # @param params [Hash] Hint parameters
242
+ # @param key [String] Parameter name
243
+ # @return [Object, nil] Parameter value
244
+ def extract_param(params, key)
245
+ params[key] || params[key.to_sym]
246
+ end
247
+
248
+ # Extract per-glyph hint data from HintSet
249
+ #
250
+ # @param hint_set [HintSet] Hint set with per-glyph hints
251
+ # @return [Hash] Hash mapping glyph_id => Array<Hint>
252
+ def extract_per_glyph_hints(hint_set)
253
+ per_glyph = {}
254
+
255
+ hint_set.hinted_glyph_ids.each do |glyph_id|
256
+ hints = hint_set.get_glyph_hints(glyph_id)
257
+ per_glyph[glyph_id.to_i] = hints unless hints.empty?
171
258
  end
172
259
 
173
- # Range 5: Larger numbers (five bytes)
174
- bytes = [
175
- 255,
176
- (num >> 24) & 0xff,
177
- (num >> 16) & 0xff,
178
- (num >> 8) & 0xff,
179
- num & 0xff
180
- ]
181
- bytes.pack("C*")
260
+ per_glyph
182
261
  end
183
262
  end
184
263
  end
@@ -29,6 +29,28 @@ module Fontisan
29
29
  HSTEM3 = 12 << 8 | 2
30
30
  VSTEM3 = 12 << 8 | 1
31
31
 
32
+ # Extract complete hint data from OpenType/CFF font
33
+ #
34
+ # This extracts both font-level hints (CFF Private dict) and
35
+ # per-glyph hints from CharStrings.
36
+ #
37
+ # @param font [OpenTypeFont] OpenType font with CFF table
38
+ # @return [Models::HintSet] Complete hint set
39
+ def extract_from_font(font)
40
+ hint_set = Models::HintSet.new(format: "postscript")
41
+
42
+ # Extract font-level Private dict hints
43
+ hint_set.private_dict_hints = extract_private_dict_hints(font).to_json
44
+
45
+ # Extract per-glyph CharString hints
46
+ extract_charstring_hints(font, hint_set)
47
+
48
+ # Update metadata
49
+ hint_set.has_hints = !hint_set.empty?
50
+
51
+ hint_set
52
+ end
53
+
32
54
  # Extract hints from CFF CharString
33
55
  #
34
56
  # @param charstring [CharString, String] CFF CharString object or bytes
@@ -69,12 +91,12 @@ module Fontisan
69
91
  if operator?(byte)
70
92
  # Process operator
71
93
  operator = if byte == 12
72
- # Two-byte operator
73
- i += 1
74
- (12 << 8) | bytes[i]
75
- else
76
- byte
77
- end
94
+ # Two-byte operator
95
+ i += 1
96
+ (12 << 8) | bytes[i]
97
+ else
98
+ byte
99
+ end
78
100
 
79
101
  hint = process_operator(operator, stack)
80
102
  hints << hint if hint
@@ -143,7 +165,7 @@ module Fontisan
143
165
  # 5-byte signed integer
144
166
  if index + 4 < bytes.length
145
167
  num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
146
- (bytes[index + 3] << 8) | bytes[index + 4]
168
+ (bytes[index + 3] << 8) | bytes[index + 4]
147
169
  num = num - 4294967296 if num > 2147483647
148
170
  [num, 5]
149
171
  else
@@ -182,7 +204,7 @@ module Fontisan
182
204
  Models::Hint.new(
183
205
  type: :hint_replacement,
184
206
  data: { mask: stack.dup },
185
- source_format: :postscript
207
+ source_format: :postscript,
186
208
  )
187
209
 
188
210
  when CNTRMASK
@@ -190,11 +212,9 @@ module Fontisan
190
212
  Models::Hint.new(
191
213
  type: :counter,
192
214
  data: { zones: stack.dup },
193
- source_format: :postscript
215
+ source_format: :postscript,
194
216
  )
195
217
 
196
- else
197
- nil
198
218
  end
199
219
  end
200
220
 
@@ -216,9 +236,9 @@ module Fontisan
216
236
  data: {
217
237
  position: position,
218
238
  width: width,
219
- orientation: orientation
239
+ orientation: orientation,
220
240
  },
221
- source_format: :postscript
241
+ source_format: :postscript,
222
242
  )
223
243
  end
224
244
 
@@ -236,7 +256,7 @@ module Fontisan
236
256
  pos_idx = i * 2
237
257
  stems << {
238
258
  position: stack[pos_idx],
239
- width: stack[pos_idx + 1]
259
+ width: stack[pos_idx + 1],
240
260
  }
241
261
  end
242
262
 
@@ -244,11 +264,126 @@ module Fontisan
244
264
  type: :stem3,
245
265
  data: {
246
266
  stems: stems,
247
- orientation: orientation
267
+ orientation: orientation,
248
268
  },
249
- source_format: :postscript
269
+ source_format: :postscript,
250
270
  )
251
271
  end
272
+
273
+ # Extract Private dict hints from CFF table
274
+ #
275
+ # Private dict contains font-level hint parameters like BlueValues,
276
+ # StdHW, StdVW, etc.
277
+ #
278
+ # @param font [OpenTypeFont] OpenType font
279
+ # @return [Hash] Private dict hint parameters
280
+ def extract_private_dict_hints(font)
281
+ hints = {}
282
+
283
+ return hints unless font.has_table?("CFF ")
284
+
285
+ cff_table = font.table("CFF ")
286
+ return hints unless cff_table
287
+
288
+ # Get Private DICT for first font (index 0)
289
+ private_dict = cff_table.private_dict(0)
290
+ return hints unless private_dict
291
+
292
+ # Extract hint-related parameters from Private DICT
293
+ # These are the key hinting parameters in CFF
294
+ if private_dict.respond_to?(:blue_values)
295
+ hints[:blue_values] =
296
+ private_dict.blue_values
297
+ end
298
+ if private_dict.respond_to?(:other_blues)
299
+ hints[:other_blues] =
300
+ private_dict.other_blues
301
+ end
302
+ if private_dict.respond_to?(:family_blues)
303
+ hints[:family_blues] =
304
+ private_dict.family_blues
305
+ end
306
+ if private_dict.respond_to?(:family_other_blues)
307
+ hints[:family_other_blues] =
308
+ private_dict.family_other_blues
309
+ end
310
+ if private_dict.respond_to?(:blue_scale)
311
+ hints[:blue_scale] =
312
+ private_dict.blue_scale
313
+ end
314
+ if private_dict.respond_to?(:blue_shift)
315
+ hints[:blue_shift] =
316
+ private_dict.blue_shift
317
+ end
318
+ if private_dict.respond_to?(:blue_fuzz)
319
+ hints[:blue_fuzz] =
320
+ private_dict.blue_fuzz
321
+ end
322
+ if private_dict.respond_to?(:std_hw)
323
+ hints[:std_hw] =
324
+ private_dict.std_hw
325
+ end
326
+ if private_dict.respond_to?(:std_vw)
327
+ hints[:std_vw] =
328
+ private_dict.std_vw
329
+ end
330
+ if private_dict.respond_to?(:stem_snap_h)
331
+ hints[:stem_snap_h] =
332
+ private_dict.stem_snap_h
333
+ end
334
+ if private_dict.respond_to?(:stem_snap_v)
335
+ hints[:stem_snap_v] =
336
+ private_dict.stem_snap_v
337
+ end
338
+ if private_dict.respond_to?(:force_bold)
339
+ hints[:force_bold] =
340
+ private_dict.force_bold
341
+ end
342
+ if private_dict.respond_to?(:language_group)
343
+ hints[:language_group] =
344
+ private_dict.language_group
345
+ end
346
+
347
+ hints.compact
348
+ rescue StandardError => e
349
+ warn "Failed to extract Private dict hints: #{e.message}"
350
+ {}
351
+ end
352
+
353
+ # Extract per-glyph CharString hints from CFF table
354
+ #
355
+ # @param font [OpenTypeFont] OpenType font
356
+ # @param hint_set [Models::HintSet] Hint set to populate
357
+ # @return [void]
358
+ def extract_charstring_hints(font, hint_set)
359
+ return unless font.has_table?("CFF ")
360
+
361
+ cff_table = font.table("CFF ")
362
+ return unless cff_table
363
+
364
+ # Get CharStrings INDEX
365
+ charstrings_index = cff_table.charstrings_index(0)
366
+ return unless charstrings_index
367
+
368
+ # Iterate through all glyphs
369
+ glyph_count = cff_table.glyph_count(0)
370
+ (0...glyph_count).each do |glyph_id|
371
+ # Get CharString for this glyph
372
+ charstring = cff_table.charstring_for_glyph(glyph_id, 0)
373
+ next unless charstring
374
+
375
+ # Extract hints from CharString
376
+ hints = extract(charstring)
377
+ next if hints.empty?
378
+
379
+ # Store glyph hints
380
+ hint_set.add_glyph_hints(glyph_id, hints)
381
+ rescue StandardError => e
382
+ warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
383
+ end
384
+ rescue StandardError => e
385
+ warn "Failed to extract CharString hints: #{e.message}"
386
+ end
252
387
  end
253
388
  end
254
389
  end