fontisan 0.1.0 → 0.2.1

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 (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +672 -69
  3. data/Gemfile +1 -0
  4. data/LICENSE +5 -1
  5. data/README.adoc +1477 -297
  6. data/Rakefile +63 -41
  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 +364 -4
  12. data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
  16. data/lib/fontisan/collection/writer.rb +306 -0
  17. data/lib/fontisan/commands/base_command.rb +24 -1
  18. data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
  37. data/lib/fontisan/converters/conversion_strategy.rb +96 -0
  38. data/lib/fontisan/converters/format_converter.rb +408 -0
  39. data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
  57. data/lib/fontisan/font_writer.rb +302 -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 +310 -0
  61. data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
  62. data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
  63. data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
  64. data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
  65. data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
  88. data/lib/fontisan/open_type_font_extensions.rb +54 -0
  89. data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
  90. data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
  91. data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
  92. data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
  93. data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
  94. data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
  95. data/lib/fontisan/outline_extractor.rb +423 -0
  96. data/lib/fontisan/pipeline/format_detector.rb +249 -0
  97. data/lib/fontisan/pipeline/output_writer.rb +154 -0
  98. data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
  99. data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
  100. data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
  101. data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
  102. data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
  103. data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
  104. data/lib/fontisan/subset/builder.rb +268 -0
  105. data/lib/fontisan/subset/glyph_mapping.rb +215 -0
  106. data/lib/fontisan/subset/options.rb +142 -0
  107. data/lib/fontisan/subset/profile.rb +152 -0
  108. data/lib/fontisan/subset/table_subsetter.rb +461 -0
  109. data/lib/fontisan/svg/font_face_generator.rb +278 -0
  110. data/lib/fontisan/svg/font_generator.rb +264 -0
  111. data/lib/fontisan/svg/glyph_generator.rb +168 -0
  112. data/lib/fontisan/svg/view_box_calculator.rb +137 -0
  113. data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
  114. data/lib/fontisan/tables/cff/charset.rb +282 -0
  115. data/lib/fontisan/tables/cff/charstring.rb +934 -0
  116. data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
  117. data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
  118. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
  119. data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
  120. data/lib/fontisan/tables/cff/dict.rb +351 -0
  121. data/lib/fontisan/tables/cff/dict_builder.rb +257 -0
  122. data/lib/fontisan/tables/cff/encoding.rb +274 -0
  123. data/lib/fontisan/tables/cff/header.rb +102 -0
  124. data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
  125. data/lib/fontisan/tables/cff/index.rb +237 -0
  126. data/lib/fontisan/tables/cff/index_builder.rb +170 -0
  127. data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
  128. data/lib/fontisan/tables/cff/private_dict.rb +284 -0
  129. data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
  130. data/lib/fontisan/tables/cff/table_builder.rb +221 -0
  131. data/lib/fontisan/tables/cff/top_dict.rb +236 -0
  132. data/lib/fontisan/tables/cff.rb +489 -0
  133. data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
  134. data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
  135. data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
  136. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +246 -0
  137. data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
  138. data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
  139. data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
  140. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
  141. data/lib/fontisan/tables/cff2.rb +346 -0
  142. data/lib/fontisan/tables/cvar.rb +203 -0
  143. data/lib/fontisan/tables/fvar.rb +2 -2
  144. data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
  145. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
  146. data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
  147. data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
  148. data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
  149. data/lib/fontisan/tables/glyf.rb +235 -0
  150. data/lib/fontisan/tables/gvar.rb +231 -0
  151. data/lib/fontisan/tables/hhea.rb +124 -0
  152. data/lib/fontisan/tables/hmtx.rb +287 -0
  153. data/lib/fontisan/tables/hvar.rb +191 -0
  154. data/lib/fontisan/tables/loca.rb +322 -0
  155. data/lib/fontisan/tables/maxp.rb +192 -0
  156. data/lib/fontisan/tables/mvar.rb +185 -0
  157. data/lib/fontisan/tables/name.rb +99 -30
  158. data/lib/fontisan/tables/variation_common.rb +346 -0
  159. data/lib/fontisan/tables/vvar.rb +234 -0
  160. data/lib/fontisan/true_type_collection.rb +156 -2
  161. data/lib/fontisan/true_type_font.rb +321 -20
  162. data/lib/fontisan/true_type_font_extensions.rb +54 -0
  163. data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
  164. data/lib/fontisan/utilities/checksum_calculator.rb +60 -0
  165. data/lib/fontisan/utils/thread_pool.rb +134 -0
  166. data/lib/fontisan/validation/checksum_validator.rb +170 -0
  167. data/lib/fontisan/validation/consistency_validator.rb +197 -0
  168. data/lib/fontisan/validation/structure_validator.rb +198 -0
  169. data/lib/fontisan/validation/table_validator.rb +158 -0
  170. data/lib/fontisan/validation/validator.rb +152 -0
  171. data/lib/fontisan/validation/variable_font_validator.rb +218 -0
  172. data/lib/fontisan/variable/axis_normalizer.rb +215 -0
  173. data/lib/fontisan/variable/delta_applicator.rb +313 -0
  174. data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
  175. data/lib/fontisan/variable/instancer.rb +344 -0
  176. data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
  177. data/lib/fontisan/variable/region_matcher.rb +208 -0
  178. data/lib/fontisan/variable/static_font_builder.rb +213 -0
  179. data/lib/fontisan/variable/table_updater.rb +219 -0
  180. data/lib/fontisan/variation/blend_applier.rb +199 -0
  181. data/lib/fontisan/variation/cache.rb +298 -0
  182. data/lib/fontisan/variation/cache_key_builder.rb +162 -0
  183. data/lib/fontisan/variation/converter.rb +375 -0
  184. data/lib/fontisan/variation/data_extractor.rb +86 -0
  185. data/lib/fontisan/variation/delta_applier.rb +266 -0
  186. data/lib/fontisan/variation/delta_parser.rb +228 -0
  187. data/lib/fontisan/variation/inspector.rb +275 -0
  188. data/lib/fontisan/variation/instance_generator.rb +273 -0
  189. data/lib/fontisan/variation/instance_writer.rb +341 -0
  190. data/lib/fontisan/variation/interpolator.rb +231 -0
  191. data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
  192. data/lib/fontisan/variation/optimizer.rb +418 -0
  193. data/lib/fontisan/variation/parallel_generator.rb +150 -0
  194. data/lib/fontisan/variation/region_matcher.rb +221 -0
  195. data/lib/fontisan/variation/subsetter.rb +463 -0
  196. data/lib/fontisan/variation/table_accessor.rb +105 -0
  197. data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
  198. data/lib/fontisan/variation/validator.rb +345 -0
  199. data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
  200. data/lib/fontisan/variation/variation_context.rb +211 -0
  201. data/lib/fontisan/variation/variation_preserver.rb +288 -0
  202. data/lib/fontisan/version.rb +1 -1
  203. data/lib/fontisan/version.rb.orig +9 -0
  204. data/lib/fontisan/woff2/directory.rb +257 -0
  205. data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
  206. data/lib/fontisan/woff2/header.rb +101 -0
  207. data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
  208. data/lib/fontisan/woff2/table_transformer.rb +163 -0
  209. data/lib/fontisan/woff2_font.rb +717 -0
  210. data/lib/fontisan/woff_font.rb +488 -0
  211. data/lib/fontisan.rb +132 -0
  212. data/scripts/compare_stack_aware.rb +187 -0
  213. data/scripts/measure_optimization.rb +141 -0
  214. metadata +234 -4
@@ -0,0 +1,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../../binary/base_record"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ class Cff2
9
+ # Table reader for CFF2 with Variable Store support
10
+ #
11
+ # CFF2TableReader parses CFF2 tables and extracts variation data
12
+ # from the Variable Store, which is essential for applying hints
13
+ # to variable fonts with CFF2 outlines.
14
+ #
15
+ # Variable Store Structure:
16
+ # - RegionList: Defines variation regions (min/peak/max per axis)
17
+ # - ItemVariationData: Contains delta arrays per region
18
+ #
19
+ # Reference: Adobe Technical Note #5177 (CFF2)
20
+ # Reference: OpenType spec - Item Variation Store
21
+ #
22
+ # @example Reading CFF2 with Variable Store
23
+ # reader = CFF2TableReader.new(cff2_data)
24
+ # store = reader.read_variable_store
25
+ # regions = store[:regions]
26
+ # deltas = store[:deltas]
27
+ class TableReader
28
+ # @return [String] Binary CFF2 data
29
+ attr_reader :data
30
+
31
+ # @return [Hash] CFF2 header information
32
+ attr_reader :header
33
+
34
+ # @return [Hash] Top DICT data
35
+ attr_reader :top_dict
36
+
37
+ # @return [Hash, nil] Variable Store data
38
+ attr_reader :variable_store
39
+
40
+ # CFF2-specific operators
41
+ VSTORE_OPERATOR = 24
42
+
43
+ # Initialize reader with CFF2 data
44
+ #
45
+ # @param data [String] Binary CFF2 table data
46
+ def initialize(data)
47
+ @data = data
48
+ @io = StringIO.new(data)
49
+ @io.set_encoding(Encoding::BINARY)
50
+ @header = nil
51
+ @top_dict = nil
52
+ @variable_store = nil
53
+ end
54
+
55
+ # Read CFF2 header
56
+ #
57
+ # @return [Hash] Header information
58
+ def read_header
59
+ @io.rewind
60
+ @header = {
61
+ major_version: read_uint8,
62
+ minor_version: read_uint8,
63
+ header_size: read_uint8,
64
+ top_dict_length: read_uint16
65
+ }
66
+
67
+ # Validate CFF2 version
68
+ unless @header[:major_version] == 2 && @header[:minor_version].zero?
69
+ raise CorruptedTableError,
70
+ "Invalid CFF2 version: #{@header[:major_version]}.#{@header[:minor_version]}"
71
+ end
72
+
73
+ @header
74
+ end
75
+
76
+ # Read Top DICT
77
+ #
78
+ # @return [Hash] Top DICT operators and values
79
+ def read_top_dict
80
+ read_header unless @header
81
+
82
+ # Seek to Top DICT (after header)
83
+ @io.seek(@header[:header_size])
84
+
85
+ top_dict_data = @io.read(@header[:top_dict_length])
86
+ @top_dict = parse_dict(top_dict_data)
87
+ end
88
+
89
+ # Read Variable Store from Top DICT
90
+ #
91
+ # The Variable Store is referenced by the vstore operator (24)
92
+ # in the Top DICT. It contains regions and deltas for variation.
93
+ #
94
+ # @return [Hash, nil] Variable Store data with :regions and :deltas
95
+ def read_variable_store
96
+ read_top_dict unless @top_dict
97
+
98
+ # Check if Variable Store is present (operator 24)
99
+ vstore_offset = @top_dict[VSTORE_OPERATOR]
100
+ return nil unless vstore_offset
101
+
102
+ # Seek to Variable Store
103
+ @io.seek(vstore_offset)
104
+
105
+ # Parse Variable Store structure
106
+ @variable_store = {
107
+ regions: read_region_list,
108
+ item_variation_data: read_item_variation_data
109
+ }
110
+
111
+ @variable_store
112
+ end
113
+
114
+ # Read Region List from Variable Store
115
+ #
116
+ # Region List defines variation regions, where each region
117
+ # specifies min/peak/max values per axis.
118
+ #
119
+ # @return [Array<Hash>] Array of region definitions
120
+ def read_region_list
121
+ region_count = read_uint16
122
+ regions = []
123
+
124
+ region_count.times do
125
+ region = read_region
126
+ regions << region
127
+ end
128
+
129
+ regions
130
+ end
131
+
132
+ # Read a single region
133
+ #
134
+ # @return [Hash] Region with axis coordinates
135
+ def read_region
136
+ axis_count = read_uint16
137
+ axes = []
138
+
139
+ axis_count.times do
140
+ axes << {
141
+ start_coord: read_f2dot14,
142
+ peak_coord: read_f2dot14,
143
+ end_coord: read_f2dot14
144
+ }
145
+ end
146
+
147
+ { axis_count: axis_count, axes: axes }
148
+ end
149
+
150
+ # Read Item Variation Data
151
+ #
152
+ # Contains delta arrays per region for varying values
153
+ #
154
+ # @return [Array<Hash>] Array of item variation data
155
+ def read_item_variation_data
156
+ data_count = read_uint16
157
+ return [] if data_count.zero?
158
+
159
+ item_variation_data = []
160
+
161
+ data_count.times do |idx|
162
+ begin
163
+ item_data = read_single_item_variation_data
164
+ item_variation_data << item_data
165
+ rescue EOFError => e
166
+ # break
167
+ end
168
+ end
169
+
170
+ item_variation_data
171
+ end
172
+
173
+ # Read a single Item Variation Data entry
174
+ #
175
+ # @return [Hash] Item variation data with region indices and deltas
176
+ def read_single_item_variation_data
177
+ item_count = read_uint16
178
+ short_delta_count = read_uint16
179
+ region_index_count = read_uint16
180
+
181
+ # Read region indices
182
+ region_indices = []
183
+ region_index_count.times do
184
+ region_indices << read_uint16
185
+ end
186
+
187
+ # Read delta sets
188
+ delta_sets = []
189
+ item_count.times do |item_idx|
190
+ begin
191
+ deltas = []
192
+
193
+ # Short deltas (16-bit)
194
+ short_delta_count.times do
195
+ break if @io.eof?
196
+ deltas << read_int16
197
+ end
198
+
199
+ # Long deltas (8-bit) for remaining regions
200
+ (region_index_count - short_delta_count).times do
201
+ break if @io.eof?
202
+ deltas << read_int8
203
+ end
204
+
205
+ delta_sets << deltas
206
+ rescue EOFError => e
207
+ # break
208
+ end
209
+ end
210
+
211
+ {
212
+ item_count: item_count,
213
+ region_indices: region_indices,
214
+ delta_sets: delta_sets
215
+ }
216
+ end
217
+
218
+ # Read Private DICT with blend support
219
+ #
220
+ # Private DICT in CFF2 can contain blend operators for
221
+ # variable hint parameters.
222
+ #
223
+ # @param size [Integer] Private DICT size
224
+ # @param offset [Integer] Private DICT offset
225
+ # @return [Hash] Private DICT data
226
+ def read_private_dict(size, offset)
227
+ @io.seek(offset)
228
+ private_dict_data = @io.read(size)
229
+ parse_dict(private_dict_data)
230
+ end
231
+
232
+ # Read CharStrings INDEX
233
+ #
234
+ # @param offset [Integer] CharStrings offset from Top DICT
235
+ # @return [Cff::Index] CharStrings INDEX
236
+ def read_charstrings(offset)
237
+ @io.seek(offset)
238
+ require_relative "../cff/index"
239
+ Cff::Index.new(@io, start_offset: offset)
240
+ end
241
+
242
+ private
243
+
244
+ # Read bytes safely with EOF checking
245
+ #
246
+ # @param bytes [Integer] Number of bytes to read
247
+ # @param description [String] Description for error messages
248
+ # @return [String] Binary data
249
+ # @raise [EOFError] If not enough bytes available
250
+ def read_safely(bytes, description)
251
+ data = @io.read(bytes)
252
+ raise EOFError, "Unexpected EOF while reading #{description}" if data.nil? || data.bytesize < bytes
253
+
254
+ data
255
+ end
256
+
257
+ # Parse DICT structure
258
+ #
259
+ # @param data [String] DICT binary data
260
+ # @return [Hash] Parsed operators and values
261
+ def parse_dict(data)
262
+ dict = {}
263
+ io = StringIO.new(data)
264
+ io.set_encoding(Encoding::BINARY)
265
+ operands = []
266
+
267
+ until io.eof?
268
+ byte = io.getbyte
269
+
270
+ if operator_byte?(byte)
271
+ operator = read_dict_operator(io, byte)
272
+ dict[operator] = operands.size == 1 ? operands.first : operands.dup
273
+ operands.clear
274
+ else
275
+ # Operand (number)
276
+ io.pos -= 1
277
+ operands << read_dict_number(io)
278
+ end
279
+ end
280
+
281
+ dict
282
+ end
283
+
284
+ # Check if byte is an operator
285
+ #
286
+ # CFF2 extends the operator range to include operator 24 (vstore)
287
+ #
288
+ # @param byte [Integer] Byte value
289
+ # @return [Boolean] True if operator
290
+ def operator_byte?(byte)
291
+ # Standard DICT operators (0-21, excluding number markers)
292
+ return true if byte <= 21 && ![12, 28, 29, 30, 31].include?(byte)
293
+
294
+ # CFF2-specific operators
295
+ return true if byte == VSTORE_OPERATOR
296
+
297
+ false
298
+ end
299
+
300
+ # Read DICT operator
301
+ #
302
+ # @param io [StringIO] Input stream
303
+ # @param first_byte [Integer] First operator byte
304
+ # @return [Integer, Array<Integer>] Operator code
305
+ def read_dict_operator(io, first_byte)
306
+ if first_byte == 12
307
+ # Two-byte operator
308
+ second_byte = io.getbyte
309
+ [12, second_byte]
310
+ else
311
+ first_byte
312
+ end
313
+ end
314
+
315
+ # Read number from DICT
316
+ #
317
+ # @param io [StringIO] Input stream
318
+ # @return [Integer, Float] Number value
319
+ def read_dict_number(io)
320
+ byte = io.getbyte
321
+
322
+ case byte
323
+ when 28
324
+ # 3-byte signed integer
325
+ b1 = io.getbyte
326
+ b2 = io.getbyte
327
+ value = (b1 << 8) | b2
328
+ value > 0x7FFF ? value - 0x10000 : value
329
+ when 29
330
+ # 5-byte signed integer
331
+ io.read(4).unpack1("l>")
332
+ when 30
333
+ # Real number
334
+ read_real_number(io)
335
+ when 32..246
336
+ byte - 139
337
+ when 247..250
338
+ b2 = io.getbyte
339
+ (byte - 247) * 256 + b2 + 108
340
+ when 251..254
341
+ b2 = io.getbyte
342
+ -(byte - 251) * 256 - b2 - 108
343
+ else
344
+ 0
345
+ end
346
+ end
347
+
348
+ # Read real number from DICT
349
+ #
350
+ # @param io [StringIO] Input stream
351
+ # @return [Float] Real number
352
+ def read_real_number(io)
353
+ nibbles = []
354
+ loop do
355
+ byte = io.getbyte
356
+ nibbles << ((byte >> 4) & 0x0F)
357
+ nibbles << (byte & 0x0F)
358
+ break if (byte & 0x0F) == 0x0F
359
+ end
360
+
361
+ str = ""
362
+ nibbles.each do |nibble|
363
+ case nibble
364
+ when 0..9 then str << nibble.to_s
365
+ when 0x0A then str << "."
366
+ when 0x0B then str << "E"
367
+ when 0x0C then str << "E-"
368
+ when 0x0E then str << "-"
369
+ when 0x0F then break
370
+ end
371
+ end
372
+
373
+ str.to_f
374
+ end
375
+
376
+ # Read unsigned 8-bit integer
377
+ #
378
+ # @return [Integer] Value
379
+ def read_uint8
380
+ read_safely(1, "uint8").unpack1("C")
381
+ end
382
+
383
+ # Read unsigned 16-bit integer (big-endian)
384
+ #
385
+ # @return [Integer] Value
386
+ def read_uint16
387
+ read_safely(2, "uint16").unpack1("n")
388
+ end
389
+
390
+ # Read signed 16-bit integer (big-endian)
391
+ #
392
+ # @return [Integer] Value
393
+ def read_int16
394
+ read_safely(2, "int16").unpack1("s>")
395
+ end
396
+
397
+ # Read signed 8-bit integer
398
+ #
399
+ # @return [Integer] Value
400
+ def read_int8
401
+ value = read_safely(1, "int8").unpack1("C")
402
+ value > 0x7F ? value - 0x100 : value
403
+ end
404
+
405
+ # Read F2DOT14 format (signed 16-bit fixed-point)
406
+ #
407
+ # F2DOT14 represents a number in 2.14 format:
408
+ # - 2 bits for integer part
409
+ # - 14 bits for fractional part
410
+ #
411
+ # @return [Float] Value
412
+ def read_f2dot14
413
+ value = read_int16
414
+ value / 16384.0
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ class Cff2
6
+ # Variation data extractor for CFF2 Variable Store
7
+ #
8
+ # Extracts regions and deltas from the Variable Store and provides
9
+ # utilities for working with variation data.
10
+ #
11
+ # Reference: OpenType spec - Item Variation Store
12
+ # Reference: Adobe Technical Note #5177 (CFF2)
13
+ #
14
+ # @example Extracting variation data
15
+ # extractor = VariationDataExtractor.new(variable_store)
16
+ # regions = extractor.regions
17
+ # deltas = extractor.deltas_for_item(item_index)
18
+ class VariationDataExtractor
19
+ # @return [Hash] Variable Store data
20
+ attr_reader :variable_store
21
+
22
+ # @return [Array<Hash>] Extracted regions
23
+ attr_reader :regions
24
+
25
+ # @return [Array<Hash>] Item variation data
26
+ attr_reader :item_variation_data
27
+
28
+ # Initialize extractor with Variable Store data
29
+ #
30
+ # @param variable_store [Hash] Variable Store from CFF2TableReader
31
+ def initialize(variable_store)
32
+ @variable_store = variable_store
33
+ @regions = variable_store[:regions] || []
34
+ @item_variation_data = variable_store[:item_variation_data] || []
35
+ end
36
+
37
+ # Get deltas for a specific item
38
+ #
39
+ # @param item_index [Integer] Item index
40
+ # @param data_index [Integer] Item variation data index (default 0)
41
+ # @return [Array<Integer>, nil] Deltas for the item, or nil if not found
42
+ def deltas_for_item(item_index, data_index: 0)
43
+ return nil if data_index >= @item_variation_data.size
44
+
45
+ item_data = @item_variation_data[data_index]
46
+ return nil if item_index >= item_data[:delta_sets].size
47
+
48
+ item_data[:delta_sets][item_index]
49
+ end
50
+
51
+ # Get region indices for item variation data
52
+ #
53
+ # @param data_index [Integer] Item variation data index (default 0)
54
+ # @return [Array<Integer>] Region indices
55
+ def region_indices(data_index: 0)
56
+ return [] if data_index >= @item_variation_data.size
57
+
58
+ @item_variation_data[data_index][:region_indices] || []
59
+ end
60
+
61
+ # Get number of items in item variation data
62
+ #
63
+ # @param data_index [Integer] Item variation data index (default 0)
64
+ # @return [Integer] Number of items
65
+ def item_count(data_index: 0)
66
+ return 0 if data_index >= @item_variation_data.size
67
+
68
+ @item_variation_data[data_index][:item_count] || 0
69
+ end
70
+
71
+ # Get all deltas for all items
72
+ #
73
+ # @param data_index [Integer] Item variation data index (default 0)
74
+ # @return [Array<Array<Integer>>] Array of delta sets
75
+ def all_deltas(data_index: 0)
76
+ return [] if data_index >= @item_variation_data.size
77
+
78
+ @item_variation_data[data_index][:delta_sets] || []
79
+ end
80
+
81
+ # Calculate blended value for an item at specific coordinates
82
+ #
83
+ # @param item_index [Integer] Item index
84
+ # @param base_value [Numeric] Base value to blend
85
+ # @param scalars [Array<Float>] Region scalars for each region
86
+ # @param data_index [Integer] Item variation data index (default 0)
87
+ # @return [Float] Blended value
88
+ def blend_value(item_index, base_value, scalars, data_index: 0)
89
+ deltas = deltas_for_item(item_index, data_index: data_index)
90
+ return base_value.to_f unless deltas
91
+
92
+ indices = region_indices(data_index: data_index)
93
+
94
+ # Apply blend: result = base + Σ(delta[i] * scalar[region_index[i]])
95
+ result = base_value.to_f
96
+ deltas.each_with_index do |delta, i|
97
+ region_index = indices[i]
98
+ next unless region_index
99
+
100
+ scalar = scalars[region_index] || 0.0
101
+ result += delta.to_f * scalar
102
+ end
103
+
104
+ result
105
+ end
106
+
107
+ # Get region by index
108
+ #
109
+ # @param region_index [Integer] Region index
110
+ # @return [Hash, nil] Region data or nil if not found
111
+ def region(region_index)
112
+ return nil if region_index >= @regions.size
113
+
114
+ @regions[region_index]
115
+ end
116
+
117
+ # Get number of regions
118
+ #
119
+ # @return [Integer] Total number of regions
120
+ def region_count
121
+ @regions.size
122
+ end
123
+
124
+ # Get number of axes from first region
125
+ #
126
+ # @return [Integer] Number of axes
127
+ def axis_count
128
+ return 0 if @regions.empty?
129
+
130
+ @regions.first[:axis_count] || 0
131
+ end
132
+
133
+ # Check if Variable Store has data
134
+ #
135
+ # @return [Boolean] True if Variable Store contains data
136
+ def has_data?
137
+ !@regions.empty? && !@item_variation_data.empty?
138
+ end
139
+
140
+ # Extract all region coordinates as arrays
141
+ #
142
+ # Useful for debugging and validation
143
+ #
144
+ # @return [Array<Array<Hash>>] Array of regions with axis coordinates
145
+ def region_coordinates
146
+ @regions.map do |region|
147
+ region[:axes].map do |axis|
148
+ {
149
+ start: axis[:start_coord],
150
+ peak: axis[:peak_coord],
151
+ end: axis[:end_coord]
152
+ }
153
+ end
154
+ end
155
+ end
156
+
157
+ # Validate Variable Store structure
158
+ #
159
+ # @return [Array<String>] Array of validation errors (empty if valid)
160
+ def validate
161
+ errors = []
162
+
163
+ # Check regions consistency
164
+ if @regions.any?
165
+ expected_axes = @regions.first[:axis_count]
166
+ @regions.each_with_index do |region, i|
167
+ unless region[:axis_count] == expected_axes
168
+ errors << "Region #{i} has inconsistent axis_count: " \
169
+ "#{region[:axis_count]} vs #{expected_axes}"
170
+ end
171
+
172
+ unless region[:axes].size == expected_axes
173
+ errors << "Region #{i} has #{region[:axes].size} axes, " \
174
+ "expected #{expected_axes}"
175
+ end
176
+ end
177
+ end
178
+
179
+ # Check item variation data
180
+ @item_variation_data.each_with_index do |item_data, i|
181
+ item_count = item_data[:item_count]
182
+ delta_sets = item_data[:delta_sets]
183
+ region_indices = item_data[:region_indices]
184
+
185
+ unless delta_sets.size == item_count
186
+ errors << "Item variation data #{i} has #{delta_sets.size} " \
187
+ "delta sets, expected #{item_count}"
188
+ end
189
+
190
+ # Check each delta set has correct number of deltas
191
+ delta_sets.each_with_index do |deltas, j|
192
+ unless deltas.size == region_indices.size
193
+ errors << "Delta set #{j} in data #{i} has #{deltas.size} " \
194
+ "deltas, expected #{region_indices.size}"
195
+ end
196
+ end
197
+
198
+ # Check region indices are valid
199
+ region_indices.each_with_index do |idx, j|
200
+ if idx >= @regions.size
201
+ errors << "Region index #{idx} at position #{j} in data #{i} " \
202
+ "exceeds region count #{@regions.size}"
203
+ end
204
+ end
205
+ end
206
+
207
+ errors
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end