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,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Cff
8
+ # Type 2 CharString builder/encoder
9
+ #
10
+ # [`CharStringBuilder`](lib/fontisan/tables/cff/charstring_builder.rb)
11
+ # encodes glyph outlines into Type 2 CharString binary format. It takes
12
+ # high-level outline commands and produces the stack-based CharString
13
+ # operators used in CFF fonts.
14
+ #
15
+ # Type 2 CharString encoding:
16
+ # - Numbers are encoded in various compact formats
17
+ # - Operators are single or two-byte commands
18
+ # - All coordinates are relative (dx, dy format)
19
+ # - Current point tracking for relative calculations
20
+ #
21
+ # Operator optimization:
22
+ # - Use specialized operators (hlineto, vlineto) when possible
23
+ # - Merge sequential operators of same type
24
+ # - Minimize operator bytes
25
+ #
26
+ # Reference: Adobe Type 2 CharString Format
27
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf
28
+ #
29
+ # @example Building a CharString from outline
30
+ # builder = Fontisan::Tables::Cff::CharStringBuilder.new
31
+ # charstring_data = builder.build(outline, width: 500)
32
+ class CharStringBuilder
33
+ # Type 2 CharString operators (opposite of parser)
34
+ OPERATORS = {
35
+ hstem: 1,
36
+ vstem: 3,
37
+ vmoveto: 4,
38
+ rlineto: 5,
39
+ hlineto: 6,
40
+ vlineto: 7,
41
+ rrcurveto: 8,
42
+ callsubr: 10,
43
+ return: 11,
44
+ endchar: 14,
45
+ hstemhm: 18,
46
+ hintmask: 19,
47
+ cntrmask: 20,
48
+ rmoveto: 21,
49
+ hmoveto: 22,
50
+ vstemhm: 23,
51
+ rcurveline: 24,
52
+ rlinecurve: 25,
53
+ vvcurveto: 26,
54
+ hhcurveto: 27,
55
+ shortint: 28,
56
+ callgsubr: 29,
57
+ vhcurveto: 30,
58
+ hvcurveto: 31,
59
+ }.freeze
60
+
61
+ # Two-byte operators (12 prefix)
62
+ TWO_BYTE_OPERATORS = {
63
+ and: [12, 3],
64
+ or: [12, 4],
65
+ not: [12, 5],
66
+ abs: [12, 9],
67
+ add: [12, 10],
68
+ sub: [12, 11],
69
+ div: [12, 12],
70
+ neg: [12, 14],
71
+ eq: [12, 15],
72
+ drop: [12, 18],
73
+ put: [12, 20],
74
+ get: [12, 21],
75
+ ifelse: [12, 22],
76
+ random: [12, 23],
77
+ mul: [12, 24],
78
+ sqrt: [12, 26],
79
+ dup: [12, 27],
80
+ exch: [12, 28],
81
+ index: [12, 29],
82
+ roll: [12, 30],
83
+ hflex: [12, 34],
84
+ flex: [12, 35],
85
+ hflex1: [12, 36],
86
+ flex1: [12, 37],
87
+ }.freeze
88
+
89
+ # Build a CharString from an outline
90
+ #
91
+ # @param outline [Models::Outline] Universal outline object
92
+ # @param width [Integer, nil] Glyph width (optional)
93
+ # @return [String] Binary CharString data
94
+ def build(outline, width: nil)
95
+ @output = StringIO.new("".b)
96
+ @current_x = 0.0
97
+ @current_y = 0.0
98
+ @first_move = true
99
+
100
+ # Convert outline to CFF commands
101
+ commands = outline.to_cff_commands
102
+
103
+ # Encode width if provided (before first move)
104
+ if width && !commands.empty?
105
+ # Width is encoded as first operator before first move
106
+ # For now, we'll add it before the first moveto
107
+ encode_width(width)
108
+ end
109
+
110
+ # Encode each command
111
+ commands.each do |cmd|
112
+ encode_command(cmd)
113
+ end
114
+
115
+ # End character
116
+ write_operator(:endchar)
117
+
118
+ @output.string
119
+ end
120
+
121
+ # Build an empty CharString (for .notdef or empty glyphs)
122
+ #
123
+ # @param width [Integer, nil] Glyph width
124
+ # @return [String] Binary CharString data
125
+ def build_empty(width: nil)
126
+ @output = StringIO.new("".b)
127
+
128
+ # Encode width if provided
129
+ encode_width(width) if width
130
+
131
+ # Just endchar for empty glyph
132
+ write_operator(:endchar)
133
+
134
+ @output.string
135
+ end
136
+
137
+ private
138
+
139
+ # Encode a width value
140
+ #
141
+ # Width is encoded as a delta from nominal width
142
+ # For simplicity, we encode as-is (assuming nominal width is 0)
143
+ #
144
+ # @param width [Integer] Width value
145
+ def encode_width(width)
146
+ write_number(width)
147
+ end
148
+
149
+ # Encode a single command
150
+ #
151
+ # @param cmd [Hash] Command hash with :type and coordinates
152
+ def encode_command(cmd)
153
+ case cmd[:type]
154
+ when :move_to
155
+ encode_moveto(cmd)
156
+ when :line_to
157
+ encode_lineto(cmd)
158
+ when :curve_to
159
+ encode_curveto(cmd)
160
+ end
161
+ end
162
+
163
+ # Encode a moveto command
164
+ #
165
+ # Uses rmoveto (relative move) with dx, dy
166
+ # For first move, can optimize to hmoveto/vmoveto if one delta is 0
167
+ #
168
+ # @param cmd [Hash] Command with :x, :y
169
+ def encode_moveto(cmd)
170
+ dx = cmd[:x] - @current_x
171
+ dy = cmd[:y] - @current_y
172
+
173
+ if @first_move
174
+ # First move - can optimize
175
+ if dx.zero?
176
+ write_number(dy.round)
177
+ write_operator(:vmoveto)
178
+ elsif dy.zero?
179
+ write_number(dx.round)
180
+ write_operator(:hmoveto)
181
+ else
182
+ write_number(dx.round)
183
+ write_number(dy.round)
184
+ write_operator(:rmoveto)
185
+ end
186
+ @first_move = false
187
+ else
188
+ # Subsequent moves
189
+ write_number(dx.round)
190
+ write_number(dy.round)
191
+ write_operator(:rmoveto)
192
+ end
193
+
194
+ @current_x = cmd[:x]
195
+ @current_y = cmd[:y]
196
+ end
197
+
198
+ # Encode a lineto command
199
+ #
200
+ # Uses rlineto with dx, dy
201
+ # Could optimize with hlineto/vlineto for horizontal/vertical lines
202
+ #
203
+ # @param cmd [Hash] Command with :x, :y
204
+ def encode_lineto(cmd)
205
+ dx = cmd[:x] - @current_x
206
+ dy = cmd[:y] - @current_y
207
+
208
+ # Simple encoding - could optimize for h/v lines
209
+ write_number(dx.round)
210
+ write_number(dy.round)
211
+ write_operator(:rlineto)
212
+
213
+ @current_x = cmd[:x]
214
+ @current_y = cmd[:y]
215
+ end
216
+
217
+ # Encode a curveto command (cubic Bézier)
218
+ #
219
+ # Uses rrcurveto with 6 relative coordinates:
220
+ # dx1 dy1 dx2 dy2 dx3 dy3
221
+ #
222
+ # @param cmd [Hash] Command with :x1, :y1, :x2, :y2, :x, :y
223
+ def encode_curveto(cmd)
224
+ # Calculate relative coordinates for each control point
225
+ dx1 = cmd[:x1] - @current_x
226
+ dy1 = cmd[:y1] - @current_y
227
+
228
+ dx2 = cmd[:x2] - cmd[:x1]
229
+ dy2 = cmd[:y2] - cmd[:y1]
230
+
231
+ dx3 = cmd[:x] - cmd[:x2]
232
+ dy3 = cmd[:y] - cmd[:y2]
233
+
234
+ # Write operands
235
+ write_number(dx1.round)
236
+ write_number(dy1.round)
237
+ write_number(dx2.round)
238
+ write_number(dy2.round)
239
+ write_number(dx3.round)
240
+ write_number(dy3.round)
241
+
242
+ # Write operator
243
+ write_operator(:rrcurveto)
244
+
245
+ @current_x = cmd[:x]
246
+ @current_y = cmd[:y]
247
+ end
248
+
249
+ # Write a number to the CharString
250
+ #
251
+ # Numbers are encoded in various formats based on their range:
252
+ # - -107 to +107: Single byte (32-246)
253
+ # - -1131 to +1131: Two bytes (247-254 + byte)
254
+ # - -32768 to +32767: Three bytes (28 + 2 bytes)
255
+ # - Otherwise: Five bytes (255 + 4 bytes as 16.16 fixed)
256
+ #
257
+ # @param value [Integer, Float] Number to encode
258
+ def write_number(value)
259
+ # Convert float to integer if it's effectively an integer
260
+ value = value.round if value.is_a?(Float) && value == value.round
261
+
262
+ if value.is_a?(Float)
263
+ # Real number - use 5-byte format (16.16 fixed point)
264
+ write_real(value)
265
+ elsif value >= -107 && value <= 107
266
+ # Single byte format: 32-246 represents -107 to +107
267
+ @output.putc(value + 139)
268
+ elsif value >= 108 && value <= 1131
269
+ # Positive two-byte format: 247-250
270
+ adjusted = value - 108
271
+ b0 = 247 + (adjusted / 256)
272
+ b1 = adjusted % 256
273
+ @output.putc(b0)
274
+ @output.putc(b1)
275
+ elsif value >= -1131 && value <= -108
276
+ # Negative two-byte format: 251-254
277
+ adjusted = -value - 108
278
+ b0 = 251 + (adjusted / 256)
279
+ b1 = adjusted % 256
280
+ @output.putc(b0)
281
+ @output.putc(b1)
282
+ elsif value >= -32768 && value <= 32767
283
+ # Three-byte signed integer
284
+ @output.putc(28)
285
+ @output.write([value].pack("s>")) # Signed 16-bit big-endian
286
+ else
287
+ # Five-byte signed integer (stored as 16.16 fixed point)
288
+ write_real(value.to_f)
289
+ end
290
+ end
291
+
292
+ # Write a real number (5-byte format)
293
+ #
294
+ # @param value [Float] Real number
295
+ def write_real(value)
296
+ # Convert to 16.16 fixed point
297
+ fixed = (value * 65536.0).round
298
+
299
+ @output.putc(255)
300
+ @output.write([fixed].pack("l>")) # Signed 32-bit big-endian
301
+ end
302
+
303
+ # Write an operator to the CharString
304
+ #
305
+ # @param operator [Symbol] Operator name
306
+ def write_operator(operator)
307
+ if OPERATORS.key?(operator)
308
+ # Single-byte operator
309
+ @output.putc(OPERATORS[operator])
310
+ elsif TWO_BYTE_OPERATORS.key?(operator)
311
+ # Two-byte operator
312
+ bytes = TWO_BYTE_OPERATORS[operator]
313
+ @output.putc(bytes[0])
314
+ @output.putc(bytes[1])
315
+ else
316
+ raise ArgumentError, "Unknown operator: #{operator}"
317
+ end
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "index"
4
+ require_relative "charstring"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ class Cff
9
+ # CharStrings INDEX wrapper
10
+ #
11
+ # This class wraps the CharStrings INDEX to provide convenient access
12
+ # to individual CharString objects. The CharStrings INDEX contains the
13
+ # glyph outline programs (Type 2 CharStrings) for each glyph in the font.
14
+ #
15
+ # CharStrings Format:
16
+ # - INDEX structure containing binary CharString data
17
+ # - Each entry is a Type 2 CharString program
18
+ # - Number of entries typically matches the number of glyphs
19
+ # - Index 0 is typically .notdef glyph
20
+ #
21
+ # Usage:
22
+ # 1. Create from raw CharStrings INDEX data
23
+ # 2. Provide Private DICT and subroutine INDEXes for interpretation
24
+ # 3. Access individual CharStrings by glyph index
25
+ #
26
+ # Reference: CFF specification section 16 "Local/Global Subrs INDEXes"
27
+ # https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
28
+ #
29
+ # @example Using CharStringsIndex
30
+ # # Get CharStrings INDEX from CFF table
31
+ # charstrings_offset = top_dict.charstrings
32
+ # io = StringIO.new(cff.raw_data)
33
+ # io.seek(charstrings_offset)
34
+ # charstrings_index = CharstringsIndex.new(io, start_offset:
35
+ # charstrings_offset)
36
+ #
37
+ # # Get a specific CharString
38
+ # charstring = charstrings_index.charstring_at(
39
+ # glyph_index,
40
+ # private_dict,
41
+ # global_subrs,
42
+ # local_subrs
43
+ # )
44
+ #
45
+ # # Access CharString properties
46
+ # puts charstring.width
47
+ # puts charstring.bounding_box
48
+ # charstring.to_commands.each { |cmd| puts cmd.inspect }
49
+ class CharstringsIndex < Index
50
+ # Get a CharString object at the specified glyph index
51
+ #
52
+ # This method retrieves the binary CharString data at the given index
53
+ # and interprets it as a Type 2 CharString program.
54
+ #
55
+ # @param index [Integer] Glyph index (0-based, 0 is typically .notdef)
56
+ # @param private_dict [PrivateDict] Private DICT for width defaults
57
+ # @param global_subrs [Index] Global subroutines INDEX
58
+ # @param local_subrs [Index, nil] Local subroutines INDEX (optional)
59
+ # @return [CharString, nil] Interpreted CharString object, or nil if
60
+ # index is out of bounds
61
+ #
62
+ # @example Getting a CharString
63
+ # charstring = charstrings_index.charstring_at(
64
+ # 42,
65
+ # private_dict,
66
+ # global_subrs,
67
+ # local_subrs
68
+ # )
69
+ # puts "Width: #{charstring.width}"
70
+ # puts "Bounding box: #{charstring.bounding_box.inspect}"
71
+ def charstring_at(index, private_dict, global_subrs, local_subrs = nil)
72
+ data = self[index]
73
+ return nil unless data
74
+
75
+ CharString.new(data, private_dict, global_subrs, local_subrs)
76
+ end
77
+
78
+ # Get all CharStrings as an array of CharString objects
79
+ #
80
+ # This method interprets all CharStrings in the INDEX. Use with
81
+ # caution for fonts with many glyphs as this can be memory-intensive.
82
+ #
83
+ # @param private_dict [PrivateDict] Private DICT for width defaults
84
+ # @param global_subrs [Index] Global subroutines INDEX
85
+ # @param local_subrs [Index, nil] Local subroutines INDEX (optional)
86
+ # @return [Array<CharString>] Array of interpreted CharString objects
87
+ #
88
+ # @example Getting all CharStrings
89
+ # charstrings = charstrings_index.all_charstrings(
90
+ # private_dict,
91
+ # global_subrs,
92
+ # local_subrs
93
+ # )
94
+ # charstrings.each_with_index do |cs, i|
95
+ # puts "Glyph #{i}: width=#{cs.width}, bbox=#{cs.bounding_box}"
96
+ # end
97
+ def all_charstrings(private_dict, global_subrs, local_subrs = nil)
98
+ Array.new(count) do |i|
99
+ charstring_at(i, private_dict, global_subrs, local_subrs)
100
+ end
101
+ end
102
+
103
+ # Iterate over each CharString in the INDEX
104
+ #
105
+ # This method yields each CharString as it is interpreted, which is
106
+ # more memory-efficient than loading all at once.
107
+ #
108
+ # @param private_dict [PrivateDict] Private DICT for width defaults
109
+ # @param global_subrs [Index] Global subroutines INDEX
110
+ # @param local_subrs [Index, nil] Local subroutines INDEX (optional)
111
+ # @yield [CharString, Integer] Interpreted CharString and its index
112
+ # @return [Enumerator] If no block given
113
+ #
114
+ # @example Iterating over CharStrings
115
+ # charstrings_index.each_charstring(private_dict, global_subrs,
116
+ # local_subrs) do |cs, index|
117
+ # puts "Glyph #{index}: #{cs.bounding_box}"
118
+ # end
119
+ def each_charstring(private_dict, global_subrs, local_subrs = nil)
120
+ unless block_given?
121
+ return enum_for(:each_charstring, private_dict, global_subrs,
122
+ local_subrs)
123
+ end
124
+
125
+ count.times do |i|
126
+ charstring = charstring_at(i, private_dict, global_subrs,
127
+ local_subrs)
128
+ yield charstring, i if charstring
129
+ end
130
+ end
131
+
132
+ # Get the number of glyphs (CharStrings) in this INDEX
133
+ #
134
+ # This is typically the same as the number of glyphs in the font.
135
+ #
136
+ # @return [Integer] Number of glyphs
137
+ def glyph_count
138
+ count
139
+ end
140
+
141
+ # Check if a glyph index is valid
142
+ #
143
+ # @param index [Integer] Glyph index to check
144
+ # @return [Boolean] True if index is valid
145
+ def valid_glyph_index?(index)
146
+ index >= 0 && index < count
147
+ end
148
+
149
+ # Get the size of a CharString in bytes
150
+ #
151
+ # This returns the size of the binary CharString data without
152
+ # interpreting it.
153
+ #
154
+ # @param index [Integer] Glyph index
155
+ # @return [Integer, nil] Size in bytes, or nil if index is invalid
156
+ def charstring_size(index)
157
+ item_size(index)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end