fontisan 0.1.0 → 0.2.0

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +529 -65
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1301 -275
  6. data/Rakefile +27 -2
  7. data/benchmark/variation_quick_bench.rb +47 -0
  8. data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
  9. data/fontisan.gemspec +4 -1
  10. data/lib/fontisan/binary/base_record.rb +22 -1
  11. data/lib/fontisan/cli.rb +309 -0
  12. data/lib/fontisan/collection/builder.rb +260 -0
  13. data/lib/fontisan/collection/offset_calculator.rb +227 -0
  14. data/lib/fontisan/collection/table_analyzer.rb +204 -0
  15. data/lib/fontisan/collection/table_deduplicator.rb +241 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +8 -1
  18. data/lib/fontisan/commands/convert_command.rb +291 -0
  19. data/lib/fontisan/commands/export_command.rb +161 -0
  20. data/lib/fontisan/commands/info_command.rb +40 -6
  21. data/lib/fontisan/commands/instance_command.rb +295 -0
  22. data/lib/fontisan/commands/ls_command.rb +113 -0
  23. data/lib/fontisan/commands/pack_command.rb +241 -0
  24. data/lib/fontisan/commands/subset_command.rb +245 -0
  25. data/lib/fontisan/commands/unpack_command.rb +338 -0
  26. data/lib/fontisan/commands/validate_command.rb +178 -0
  27. data/lib/fontisan/commands/variable_command.rb +30 -1
  28. data/lib/fontisan/config/collection_settings.yml +56 -0
  29. data/lib/fontisan/config/conversion_matrix.yml +212 -0
  30. data/lib/fontisan/config/export_settings.yml +66 -0
  31. data/lib/fontisan/config/subset_profiles.yml +100 -0
  32. data/lib/fontisan/config/svg_settings.yml +60 -0
  33. data/lib/fontisan/config/validation_rules.yml +149 -0
  34. data/lib/fontisan/config/variable_settings.yml +99 -0
  35. data/lib/fontisan/config/woff2_settings.yml +77 -0
  36. data/lib/fontisan/constants.rb +69 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +259 -0
  39. data/lib/fontisan/converters/outline_converter.rb +936 -0
  40. data/lib/fontisan/converters/svg_generator.rb +244 -0
  41. data/lib/fontisan/converters/table_copier.rb +117 -0
  42. data/lib/fontisan/converters/woff2_encoder.rb +416 -0
  43. data/lib/fontisan/converters/woff_writer.rb +391 -0
  44. data/lib/fontisan/error.rb +203 -0
  45. data/lib/fontisan/export/exporter.rb +262 -0
  46. data/lib/fontisan/export/table_serializer.rb +255 -0
  47. data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
  48. data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
  49. data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
  50. data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
  51. data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
  52. data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
  53. data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
  54. data/lib/fontisan/export/ttx_generator.rb +527 -0
  55. data/lib/fontisan/export/ttx_parser.rb +300 -0
  56. data/lib/fontisan/font_loader.rb +121 -12
  57. data/lib/fontisan/font_writer.rb +301 -0
  58. data/lib/fontisan/formatters/text_formatter.rb +102 -0
  59. data/lib/fontisan/glyph_accessor.rb +503 -0
  60. data/lib/fontisan/hints/hint_converter.rb +177 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
  65. data/lib/fontisan/loading_modes.rb +113 -0
  66. data/lib/fontisan/metrics_calculator.rb +277 -0
  67. data/lib/fontisan/models/collection_font_summary.rb +52 -0
  68. data/lib/fontisan/models/collection_info.rb +76 -0
  69. data/lib/fontisan/models/collection_list_info.rb +37 -0
  70. data/lib/fontisan/models/font_export.rb +158 -0
  71. data/lib/fontisan/models/font_summary.rb +48 -0
  72. data/lib/fontisan/models/glyph_outline.rb +343 -0
  73. data/lib/fontisan/models/hint.rb +233 -0
  74. data/lib/fontisan/models/outline.rb +664 -0
  75. data/lib/fontisan/models/table_sharing_info.rb +40 -0
  76. data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
  77. data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
  78. data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
  79. data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
  80. data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
  81. data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
  82. data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
  83. data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
  84. data/lib/fontisan/models/ttx/ttfont.rb +49 -0
  85. data/lib/fontisan/models/validation_report.rb +203 -0
  86. data/lib/fontisan/open_type_collection.rb +156 -2
  87. data/lib/fontisan/open_type_font.rb +296 -10
  88. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  89. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  90. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  91. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  92. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  93. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  94. data/lib/fontisan/outline_extractor.rb +423 -0
  95. data/lib/fontisan/subset/builder.rb +268 -0
  96. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  97. data/lib/fontisan/subset/options.rb +142 -0
  98. data/lib/fontisan/subset/profile.rb +152 -0
  99. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  100. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  101. data/lib/fontisan/svg/font_generator.rb +264 -0
  102. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  103. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  104. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  105. data/lib/fontisan/tables/cff/charset.rb +282 -0
  106. data/lib/fontisan/tables/cff/charstring.rb +905 -0
  107. data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
  108. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  109. data/lib/fontisan/tables/cff/dict.rb +351 -0
  110. data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
  111. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  112. data/lib/fontisan/tables/cff/header.rb +102 -0
  113. data/lib/fontisan/tables/cff/index.rb +237 -0
  114. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  115. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  116. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  117. data/lib/fontisan/tables/cff.rb +487 -0
  118. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  119. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  120. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  121. data/lib/fontisan/tables/cff2.rb +341 -0
  122. data/lib/fontisan/tables/cvar.rb +242 -0
  123. data/lib/fontisan/tables/fvar.rb +2 -2
  124. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  125. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  126. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  127. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  128. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  129. data/lib/fontisan/tables/glyf.rb +235 -0
  130. data/lib/fontisan/tables/gvar.rb +270 -0
  131. data/lib/fontisan/tables/hhea.rb +124 -0
  132. data/lib/fontisan/tables/hmtx.rb +287 -0
  133. data/lib/fontisan/tables/hvar.rb +191 -0
  134. data/lib/fontisan/tables/loca.rb +322 -0
  135. data/lib/fontisan/tables/maxp.rb +192 -0
  136. data/lib/fontisan/tables/mvar.rb +185 -0
  137. data/lib/fontisan/tables/name.rb +99 -30
  138. data/lib/fontisan/tables/variation_common.rb +346 -0
  139. data/lib/fontisan/tables/vvar.rb +234 -0
  140. data/lib/fontisan/true_type_collection.rb +156 -2
  141. data/lib/fontisan/true_type_font.rb +297 -11
  142. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  143. data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
  144. data/lib/fontisan/utils/thread_pool.rb +134 -0
  145. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  146. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  147. data/lib/fontisan/validation/structure_validator.rb +198 -0
  148. data/lib/fontisan/validation/table_validator.rb +158 -0
  149. data/lib/fontisan/validation/validator.rb +152 -0
  150. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  151. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  152. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  153. data/lib/fontisan/variable/instancer.rb +344 -0
  154. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  155. data/lib/fontisan/variable/region_matcher.rb +208 -0
  156. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  157. data/lib/fontisan/variable/table_updater.rb +219 -0
  158. data/lib/fontisan/variation/blend_applier.rb +199 -0
  159. data/lib/fontisan/variation/cache.rb +298 -0
  160. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  161. data/lib/fontisan/variation/converter.rb +268 -0
  162. data/lib/fontisan/variation/data_extractor.rb +86 -0
  163. data/lib/fontisan/variation/delta_applier.rb +266 -0
  164. data/lib/fontisan/variation/delta_parser.rb +228 -0
  165. data/lib/fontisan/variation/inspector.rb +275 -0
  166. data/lib/fontisan/variation/instance_generator.rb +273 -0
  167. data/lib/fontisan/variation/interpolator.rb +231 -0
  168. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  169. data/lib/fontisan/variation/optimizer.rb +418 -0
  170. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  171. data/lib/fontisan/variation/region_matcher.rb +221 -0
  172. data/lib/fontisan/variation/subsetter.rb +463 -0
  173. data/lib/fontisan/variation/table_accessor.rb +105 -0
  174. data/lib/fontisan/variation/validator.rb +345 -0
  175. data/lib/fontisan/variation/variation_context.rb +211 -0
  176. data/lib/fontisan/version.rb +1 -1
  177. data/lib/fontisan/woff2/directory.rb +257 -0
  178. data/lib/fontisan/woff2/header.rb +101 -0
  179. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  180. data/lib/fontisan/woff2_font.rb +712 -0
  181. data/lib/fontisan/woff_font.rb +483 -0
  182. data/lib/fontisan.rb +120 -0
  183. data/scripts/compare_stack_aware.rb +187 -0
  184. data/scripts/measure_optimization.rb +141 -0
  185. metadata +205 -4
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ class Cff2
6
+ # Operand stack manager for CFF2 CharStrings
7
+ #
8
+ # This class manages the operand stack for CFF2 CharStrings, with special
9
+ # handling for blend operations that mix base values and deltas.
10
+ #
11
+ # In CFF2, the blend operator takes operands in the format:
12
+ # [base1, delta1_axis1, delta1_axis2, ..., base2, delta2_axis1, ..., K, N]
13
+ #
14
+ # Where:
15
+ # - K = number of values to blend
16
+ # - N = number of variation axes
17
+ #
18
+ # The stack manager separates base values from deltas and applies blend
19
+ # operations to produce final values based on variation coordinates.
20
+ #
21
+ # @example Managing a blend operation
22
+ # stack = OperandStack.new(num_axes: 2)
23
+ # stack.push(100, 10, 5) # base=100, deltas=[10, 5]
24
+ # stack.push(200, 20, 10) # base=200, deltas=[20, 10]
25
+ # blended = stack.apply_blend(k: 2, coordinates: { "wght" => 0.5, "wdth" => 0.3 })
26
+ # # => [105.0, 206.0] # base + (delta * scalar)
27
+ class OperandStack
28
+ # @return [Array<Numeric>] The operand stack
29
+ attr_reader :stack
30
+
31
+ # @return [Integer] Number of variation axes
32
+ attr_reader :num_axes
33
+
34
+ # @return [Array<Hash>] Blend values (base + deltas)
35
+ attr_reader :blend_values
36
+
37
+ # Initialize operand stack
38
+ #
39
+ # @param num_axes [Integer] Number of variation axes (default 0)
40
+ def initialize(num_axes: 0)
41
+ @stack = []
42
+ @num_axes = num_axes
43
+ @blend_values = []
44
+ end
45
+
46
+ # Push a value onto the stack
47
+ #
48
+ # @param values [Numeric] Values to push
49
+ def push(*values)
50
+ @stack.concat(values)
51
+ end
52
+
53
+ # Pop a value from the stack
54
+ #
55
+ # @return [Numeric, nil] Popped value or nil if empty
56
+ def pop
57
+ @stack.pop
58
+ end
59
+
60
+ # Pop multiple values from the stack
61
+ #
62
+ # @param count [Integer] Number of values to pop
63
+ # @return [Array<Numeric>] Popped values
64
+ def pop_many(count)
65
+ return [] if count <= 0 || @stack.empty?
66
+
67
+ @stack.pop(count)
68
+ end
69
+
70
+ # Shift a value from the front of the stack
71
+ #
72
+ # @return [Numeric, nil] Shifted value or nil if empty
73
+ def shift
74
+ @stack.shift
75
+ end
76
+
77
+ # Get the top value without popping
78
+ #
79
+ # @return [Numeric, nil] Top value or nil if empty
80
+ def peek
81
+ @stack.last
82
+ end
83
+
84
+ # Get stack size
85
+ #
86
+ # @return [Integer] Number of values on stack
87
+ def size
88
+ @stack.size
89
+ end
90
+
91
+ # Check if stack is empty
92
+ #
93
+ # @return [Boolean] True if empty
94
+ def empty?
95
+ @stack.empty?
96
+ end
97
+
98
+ # Clear the stack
99
+ def clear
100
+ @stack.clear
101
+ @blend_values.clear
102
+ end
103
+
104
+ # Apply blend operation
105
+ #
106
+ # This pops K * (N + 1) + 2 operands from the stack, where:
107
+ # - K = number of values to blend
108
+ # - N = number of axes
109
+ # - Last 2 values are K and N themselves
110
+ #
111
+ # @param scalars [Array<Float>] Variation scalars for each axis
112
+ # @return [Array<Float>] Blended values
113
+ def apply_blend(scalars = [])
114
+ # Pop N and K
115
+ n = pop.to_i
116
+ k = pop.to_i
117
+
118
+ # Validate
119
+ required_operands = k * (n + 1)
120
+ if size < required_operands
121
+ warn "Blend requires #{required_operands} operands, got #{size}"
122
+ clear
123
+ return []
124
+ end
125
+
126
+ # Extract operands (base + deltas for each value)
127
+ blend_operands = pop_many(required_operands).reverse
128
+
129
+ # Process each value to blend
130
+ blended_values = []
131
+ k.times do |i|
132
+ offset = i * (n + 1)
133
+ base = blend_operands[offset]
134
+ deltas = blend_operands[offset + 1, n] || []
135
+
136
+ # Apply blend: result = base + sum(delta[i] * scalar[i])
137
+ blended = base.to_f
138
+ deltas.each_with_index do |delta, axis_index|
139
+ scalar = scalars[axis_index] || 0.0
140
+ blended += delta.to_f * scalar
141
+ end
142
+
143
+ # Store blend info for debugging/inspection
144
+ @blend_values << {
145
+ base: base,
146
+ deltas: deltas,
147
+ blended: blended,
148
+ }
149
+
150
+ blended_values << blended
151
+ end
152
+
153
+ # Push blended values back onto stack
154
+ push(*blended_values)
155
+
156
+ blended_values
157
+ end
158
+
159
+ # Extract blend data without applying
160
+ #
161
+ # This is used when we need to store blend operations for later
162
+ # application with specific coordinates.
163
+ #
164
+ # @return [Hash] Blend operation data
165
+ def extract_blend_data
166
+ # Pop N and K
167
+ n = pop.to_i
168
+ k = pop.to_i
169
+
170
+ # Validate
171
+ required_operands = k * (n + 1)
172
+ if size < required_operands
173
+ warn "Blend requires #{required_operands} operands, got #{size}"
174
+ clear
175
+ return nil
176
+ end
177
+
178
+ # Extract operands
179
+ blend_operands = pop_many(required_operands).reverse
180
+
181
+ # Parse into base + deltas structure
182
+ blends = []
183
+ k.times do |i|
184
+ offset = i * (n + 1)
185
+ base = blend_operands[offset]
186
+ deltas = blend_operands[offset + 1, n] || []
187
+
188
+ blends << {
189
+ base: base,
190
+ deltas: deltas,
191
+ }
192
+
193
+ # Push base value back (will be blended later)
194
+ push(base)
195
+ end
196
+
197
+ {
198
+ num_values: k,
199
+ num_axes: n,
200
+ blends: blends,
201
+ }
202
+ end
203
+
204
+ # Get all values on the stack
205
+ #
206
+ # @return [Array<Numeric>] Stack contents
207
+ def to_a
208
+ @stack.dup
209
+ end
210
+
211
+ # Get string representation for debugging
212
+ #
213
+ # @return [String] Stack contents as string
214
+ def inspect
215
+ "#<OperandStack size=#{size} values=#{@stack.inspect}>"
216
+ end
217
+
218
+ # Get blend value history
219
+ #
220
+ # @return [Array<Hash>] Blend values that have been calculated
221
+ def blend_history
222
+ @blend_values.dup
223
+ end
224
+
225
+ # Reset blend history
226
+ def reset_blend_history
227
+ @blend_values.clear
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../binary/base_record"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ # Parser for the 'CFF2' (Compact Font Format 2) table
9
+ #
10
+ # CFF2 is used primarily in variable fonts with PostScript outlines.
11
+ # Key differences from CFF:
12
+ # - No Name INDEX (font names come from name table)
13
+ # - No Encoding or Charset (use cmap table instead)
14
+ # - Support for blend operators in CharStrings for variations
15
+ # - Different default values in DICTs
16
+ #
17
+ # Reference: Adobe Technical Note #5177
18
+ #
19
+ # @example Reading a CFF2 table
20
+ # data = font.table_data("CFF2")
21
+ # cff2 = Fontisan::Tables::Cff2.read(data)
22
+ # num_glyphs = cff2.glyph_count
23
+ class Cff2 < Binary::BaseRecord
24
+ # CFF2 header structure
25
+ class Header < Binary::BaseRecord
26
+ uint8 :major_version
27
+ uint8 :minor_version
28
+ uint8 :header_size
29
+ uint16 :top_dict_length
30
+
31
+ # Check if version is valid
32
+ #
33
+ # @return [Boolean] True if version is 2.0
34
+ def valid?
35
+ major_version == 2 && minor_version.zero?
36
+ end
37
+ end
38
+
39
+ # Parse the CFF2 table
40
+ #
41
+ # @return [self]
42
+ def parse
43
+ return self if @parsed
44
+
45
+ @header = parse_header
46
+ @global_subr_index = parse_global_subr_index
47
+ @top_dict = parse_top_dict
48
+ @charstrings_index = parse_charstrings_index
49
+
50
+ @parsed = true
51
+ self
52
+ end
53
+
54
+ # Get the CFF2 header
55
+ #
56
+ # @return [Header] Header structure
57
+ def header
58
+ parse unless @parsed
59
+ @header
60
+ end
61
+
62
+ # Get glyph count from font's maxp table
63
+ #
64
+ # CFF2 doesn't store glyph count internally - it relies on the maxp table
65
+ #
66
+ # @return [Integer] Number of glyphs (requires access to font's maxp)
67
+ def glyph_count
68
+ # This needs to be set externally or retrieved from maxp table
69
+ # For now, return a default that indicates it needs to be set
70
+ @glyph_count || 0
71
+ end
72
+
73
+ # Set glyph count (from maxp table)
74
+ #
75
+ # @param count [Integer] Number of glyphs
76
+ def glyph_count=(count)
77
+ @glyph_count = count
78
+ end
79
+
80
+ # Set number of variation axes (from fvar table)
81
+ #
82
+ # @param count [Integer] Number of axes
83
+ def num_axes=(count)
84
+ @num_axes = count
85
+ end
86
+
87
+ # Get number of variation axes
88
+ #
89
+ # @return [Integer] Number of axes
90
+ def num_axes
91
+ @num_axes || 0
92
+ end
93
+
94
+ # Get CharString for a specific glyph
95
+ #
96
+ # @param glyph_id [Integer] Glyph ID
97
+ # @return [CharstringParser, nil] CharString object or nil
98
+ def charstring_for_glyph(glyph_id)
99
+ parse unless @parsed
100
+ return nil if @charstrings_index.nil?
101
+ return nil if glyph_id >= @charstrings_index.count
102
+
103
+ # Get CharString data from INDEX
104
+ charstring_data = @charstrings_index[glyph_id]
105
+ return nil if charstring_data.nil?
106
+
107
+ # Parse with CFF2 CharString parser
108
+ require_relative "cff2/charstring_parser"
109
+ CharstringParser.new(
110
+ charstring_data,
111
+ @num_axes,
112
+ @global_subr_index,
113
+ nil, # local subrs (CFF2 may not have them)
114
+ 0 # vsindex
115
+ ).parse
116
+ end
117
+
118
+ # Get all CharStrings
119
+ #
120
+ # @return [Array<CharstringParser>] Array of parsed CharStrings
121
+ def charstrings
122
+ return [] unless @charstrings_index
123
+
124
+ @charstrings_index.count.times.map do |glyph_id|
125
+ charstring_for_glyph(glyph_id)
126
+ end.compact
127
+ end
128
+
129
+ # Check if table is valid
130
+ #
131
+ # @return [Boolean] True if valid CFF2 table
132
+ def valid?
133
+ header.valid?
134
+ end
135
+
136
+ private
137
+
138
+ # Parse CFF2 header
139
+ #
140
+ # @return [Header] Parsed header
141
+ def parse_header
142
+ data = raw_data
143
+ return nil if data.nil? || data.bytesize < 5
144
+
145
+ Header.read(data.byteslice(0, 5))
146
+ end
147
+
148
+ # Parse Global Subr INDEX
149
+ #
150
+ # @return [Cff::Index] Global subroutines INDEX
151
+ def parse_global_subr_index
152
+ # CFF2 has a Global Subr INDEX after the header
153
+ data = raw_data
154
+ return nil unless @header
155
+
156
+ offset = @header.header_size
157
+
158
+ # Global Subr INDEX follows header
159
+ io = StringIO.new(data)
160
+ io.seek(offset)
161
+
162
+ require_relative "cff/index"
163
+ Cff::Index.new(io, start_offset: offset)
164
+ rescue StandardError => e
165
+ warn "Failed to parse Global Subr INDEX: #{e.message}"
166
+ nil
167
+ end
168
+
169
+ # Parse Top DICT
170
+ #
171
+ # @return [Hash] Top DICT data
172
+ def parse_top_dict
173
+ # CFF2 Top DICT follows the header (length specified in header)
174
+ data = raw_data
175
+ return {} unless @header
176
+
177
+ offset = @header.header_size
178
+ length = @header.top_dict_length
179
+
180
+ return {} if offset + length > data.bytesize
181
+
182
+ top_dict_data = data.byteslice(offset, length)
183
+
184
+ # Parse Top DICT (simplified for now)
185
+ # Full implementation would parse DICT operators
186
+ parse_dict(top_dict_data)
187
+ rescue StandardError => e
188
+ warn "Failed to parse Top DICT: #{e.message}"
189
+ {}
190
+ end
191
+
192
+ # Parse CharStrings INDEX
193
+ #
194
+ # @return [Cff::Index, nil] CharStrings INDEX
195
+ def parse_charstrings_index
196
+ # CharStrings INDEX location is specified in Top DICT
197
+ # For now, we'll try to find it after Global Subr INDEX
198
+ data = raw_data
199
+ return nil unless @header
200
+
201
+ # Calculate offset after header + global subr
202
+ offset = @header.header_size
203
+
204
+ # Skip Global Subr INDEX
205
+ if @global_subr_index
206
+ offset += calculate_index_size(@global_subr_index)
207
+ end
208
+
209
+ # Skip Top DICT
210
+ offset += @header.top_dict_length
211
+
212
+ io = StringIO.new(data)
213
+ io.seek(offset)
214
+
215
+ require_relative "cff/index"
216
+ Cff::Index.new(io, start_offset: offset)
217
+ rescue StandardError => e
218
+ warn "Failed to parse CharStrings INDEX: #{e.message}"
219
+ nil
220
+ end
221
+
222
+ # Parse a DICT structure
223
+ #
224
+ # @param data [String] DICT data
225
+ # @return [Hash] Parsed operators and values
226
+ def parse_dict(data)
227
+ dict = {}
228
+ io = StringIO.new(data)
229
+ io.set_encoding(Encoding::BINARY)
230
+
231
+ operands = []
232
+
233
+ until io.eof?
234
+ byte = io.getbyte
235
+
236
+ if byte <= 21 && ![12, 28, 29, 30, 31].include?(byte)
237
+ # Operator
238
+ operator = byte
239
+ if operator == 12
240
+ operator = [12, io.getbyte]
241
+ end
242
+
243
+ dict[operator] = operands.dup
244
+ operands.clear
245
+ else
246
+ # Operand (number)
247
+ io.pos -= 1
248
+ operands << read_dict_number(io)
249
+ end
250
+ end
251
+
252
+ dict
253
+ rescue StandardError
254
+ {}
255
+ end
256
+
257
+ # Read a number from DICT data
258
+ #
259
+ # @param io [StringIO] Input stream
260
+ # @return [Integer, Float] Number value
261
+ def read_dict_number(io)
262
+ byte = io.getbyte
263
+
264
+ case byte
265
+ when 28
266
+ # 3-byte signed integer
267
+ b1 = io.getbyte
268
+ b2 = io.getbyte
269
+ value = (b1 << 8) | b2
270
+ value > 0x7FFF ? value - 0x10000 : value
271
+ when 29
272
+ # 5-byte signed integer
273
+ bytes = io.read(4)
274
+ bytes.unpack1("l>")
275
+ when 30
276
+ # Real number (nibble-based)
277
+ read_real_number(io)
278
+ when 32..246
279
+ byte - 139
280
+ when 247..250
281
+ b2 = io.getbyte
282
+ (byte - 247) * 256 + b2 + 108
283
+ when 251..254
284
+ b2 = io.getbyte
285
+ -(byte - 251) * 256 - b2 - 108
286
+ else
287
+ 0
288
+ end
289
+ end
290
+
291
+ # Read a real number from DICT
292
+ #
293
+ # @param io [StringIO] Input stream
294
+ # @return [Float] Real number
295
+ def read_real_number(io)
296
+ nibbles = []
297
+ loop do
298
+ byte = io.getbyte
299
+ nibbles << ((byte >> 4) & 0x0F)
300
+ nibbles << (byte & 0x0F)
301
+ break if (byte & 0x0F) == 0x0F
302
+ end
303
+
304
+ # Convert nibbles to string
305
+ str = ""
306
+ nibbles.each do |nibble|
307
+ case nibble
308
+ when 0..9 then str << nibble.to_s
309
+ when 0x0A then str << "."
310
+ when 0x0B then str << "E"
311
+ when 0x0C then str << "E-"
312
+ when 0x0E then str << "-"
313
+ when 0x0F then break
314
+ end
315
+ end
316
+
317
+ str.to_f
318
+ end
319
+
320
+ # Calculate size of an INDEX structure
321
+ #
322
+ # @param index [Cff::Index] INDEX structure
323
+ # @return [Integer] Size in bytes
324
+ def calculate_index_size(index)
325
+ return 2 if index.count.zero? # Just count field
326
+
327
+ # count (2) + offSize (1) + offsets + data
328
+ count = index.count
329
+ data_size = index.instance_variable_get(:@data_size) || 0
330
+ off_size = index.instance_variable_get(:@off_size) || 4
331
+
332
+ 2 + 1 + ((count + 1) * off_size) + data_size
333
+ end
334
+ end
335
+ end
336
+ end
337
+
338
+ # Load CFF2 subcomponents
339
+ require_relative "cff2/charstring_parser"
340
+ require_relative "cff2/blend_operator"
341
+ require_relative "cff2/operand_stack"