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,346 @@
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 Cff2Header < 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 [Cff2Header] 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 [Cff2Header] Parsed header
141
+ def parse_header
142
+ data = raw_data
143
+ return nil if data.nil? || data.bytesize < 5
144
+
145
+ Cff2Header.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"
342
+ require_relative "cff2/table_reader"
343
+ require_relative "cff2/variation_data_extractor"
344
+ require_relative "cff2/region_matcher"
345
+ require_relative "cff2/private_dict_blend_handler"
346
+ require_relative "cff2/table_builder"
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../binary/base_record"
5
+ require_relative "../variation/tuple_variation_header"
6
+
7
+ module Fontisan
8
+ module Tables
9
+ # Parser for the 'cvar' (CVT Variations) table
10
+ #
11
+ # The cvar table provides variation data for the Control Value Table (CVT)
12
+ # in TrueType variable fonts with hinting. The CVT contains scalar values
13
+ # that are referenced by TrueType instructions for grid-fitting.
14
+ #
15
+ # Like gvar, this table uses a TupleVariationStore structure with packed
16
+ # delta values rather than ItemVariationStore.
17
+ #
18
+ # Reference: OpenType specification, cvar table
19
+ #
20
+ # @example Reading a cvar table
21
+ # data = font.table_data("cvar")
22
+ # cvar = Fontisan::Tables::Cvar.read(data)
23
+ # cvt_deltas = cvar.cvt_variations
24
+ class Cvar < Binary::BaseRecord
25
+ uint16 :major_version
26
+ uint16 :minor_version
27
+ uint16 :tuple_variation_count
28
+ uint16 :data_offset
29
+
30
+ # Get version as a float
31
+ #
32
+ # @return [Float] Version number (e.g., 1.0)
33
+ def version
34
+ major_version + (minor_version / 10.0)
35
+ end
36
+
37
+ # Get tuple count
38
+ #
39
+ # @return [Integer] Number of tuple variations
40
+ def tuple_count
41
+ tuple_variation_count & 0x0FFF
42
+ end
43
+
44
+ # Check if using shared point numbers
45
+ #
46
+ # @return [Boolean] True if shared points
47
+ def shared_point_numbers?
48
+ (tuple_variation_count & 0x8000) != 0
49
+ end
50
+
51
+ # Get axis count from fvar table (needs to be provided externally)
52
+ # This is a placeholder that should be set by the caller
53
+ attr_accessor :axis_count
54
+
55
+ # Parse tuple variation headers
56
+ #
57
+ # @return [Array<Hash>] Array of tuple information
58
+ def tuple_variations
59
+ return @tuple_variations if @tuple_variations
60
+ return @tuple_variations = [] if tuple_count.zero?
61
+
62
+ data = raw_data
63
+ # Tuple records start after header (8 bytes)
64
+ offset = 8
65
+
66
+ count = tuple_count
67
+ tuples = []
68
+
69
+ count.times do |_i|
70
+ break if offset + 4 > data.bytesize
71
+
72
+ header_data = data.byteslice(offset, 4)
73
+ header = Variation::TupleVariationHeader.read(header_data)
74
+ offset += 4
75
+
76
+ tuple_info = {
77
+ data_size: header.variation_data_size,
78
+ embedded_peak: header.embedded_peak_tuple?,
79
+ intermediate: header.intermediate_region?,
80
+ private_points: header.private_point_numbers?,
81
+ shared_index: header.shared_tuple_index,
82
+ }
83
+
84
+ # Read peak tuple if embedded
85
+ if header.embedded_peak_tuple? && axis_count
86
+ peak = Array.new(axis_count) do
87
+ next nil if offset + 2 > data.bytesize
88
+
89
+ coord_data = data.byteslice(offset, 2)
90
+ offset += 2
91
+
92
+ value = coord_data.unpack1("n")
93
+ signed = value > 0x7FFF ? value - 0x10000 : value
94
+ signed / 16384.0
95
+ end.compact
96
+ tuple_info[:peak] = peak
97
+ end
98
+
99
+ # Read intermediate region if present
100
+ if header.intermediate_region? && axis_count
101
+ start_tuple = Array.new(axis_count) do
102
+ next nil if offset + 2 > data.bytesize
103
+
104
+ coord_data = data.byteslice(offset, 2)
105
+ offset += 2
106
+
107
+ value = coord_data.unpack1("n")
108
+ signed = value > 0x7FFF ? value - 0x10000 : value
109
+ signed / 16384.0
110
+ end.compact
111
+
112
+ end_tuple = Array.new(axis_count) do
113
+ next nil if offset + 2 > data.bytesize
114
+
115
+ coord_data = data.byteslice(offset, 2)
116
+ offset += 2
117
+
118
+ value = coord_data.unpack1("n")
119
+ signed = value > 0x7FFF ? value - 0x10000 : value
120
+ signed / 16384.0
121
+ end.compact
122
+
123
+ tuple_info[:start] = start_tuple
124
+ tuple_info[:end] = end_tuple
125
+ end
126
+
127
+ tuples << tuple_info
128
+ end
129
+
130
+ @tuple_variations = tuples
131
+ rescue StandardError => e
132
+ warn "Failed to parse cvar tuple variations: #{e.message}"
133
+ @tuple_variations = []
134
+ end
135
+
136
+ # Get variation data section
137
+ #
138
+ # @return [String, nil] Raw variation data
139
+ def variation_data
140
+ return @variation_data if defined?(@variation_data)
141
+
142
+ data = raw_data
143
+ offset = data_offset
144
+
145
+ return @variation_data = nil if offset >= data.bytesize
146
+
147
+ @variation_data = data.byteslice(offset..-1)
148
+ end
149
+
150
+ # Parse CVT deltas for a specific tuple
151
+ #
152
+ # This is a simplified parser that returns the raw delta data.
153
+ # Full delta unpacking would require knowing point counts and
154
+ # delta formats.
155
+ #
156
+ # @param tuple_index [Integer] Tuple index
157
+ # @return [Hash, nil] Tuple info with data offset
158
+ def tuple_variation_data(tuple_index)
159
+ return nil if tuple_index >= tuple_count
160
+
161
+ tuples = tuple_variations
162
+ return nil if tuple_index >= tuples.length
163
+
164
+ tuple = tuples[tuple_index]
165
+
166
+ # Calculate data offset for this tuple
167
+ # This is complex and requires walking through all previous tuples
168
+ # For now, return tuple metadata
169
+ {
170
+ tuple: tuple,
171
+ data_size: tuple[:data_size],
172
+ }
173
+ end
174
+
175
+ # Get summary of CVT variations
176
+ #
177
+ # @return [Hash] Summary information
178
+ def summary
179
+ {
180
+ version: version,
181
+ tuple_count: tuple_count,
182
+ shared_points: shared_point_numbers?,
183
+ data_offset: data_offset,
184
+ tuples: tuple_variations.map do |t|
185
+ {
186
+ embedded_peak: t[:embedded_peak],
187
+ intermediate: t[:intermediate],
188
+ private_points: t[:private_points],
189
+ peak: t[:peak],
190
+ }
191
+ end,
192
+ }
193
+ end
194
+
195
+ # Check if table is valid
196
+ #
197
+ # @return [Boolean] True if valid
198
+ def valid?
199
+ major_version == 1 && minor_version.zero?
200
+ end
201
+ end
202
+ end
203
+ end
@@ -89,7 +89,7 @@ module Fontisan
89
89
  return @axes = [] if axis_count.zero?
90
90
 
91
91
  # Get the full data buffer as binary string
92
- data = to_binary_s
92
+ data = raw_data
93
93
 
94
94
  @axes = Array.new(axis_count) do |i|
95
95
  offset = axes_array_offset + (i * axis_size)
@@ -106,7 +106,7 @@ module Fontisan
106
106
  return @instances = [] if instance_count.zero?
107
107
 
108
108
  # Get the full data buffer as binary string
109
- data = to_binary_s
109
+ data = raw_data
110
110
 
111
111
  # Calculate instance data offset (after all axes)
112
112
  instance_offset = axes_array_offset + (axis_count * axis_size)