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,666 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Woff2
7
+ # Reconstructs glyf and loca tables from WOFF2 transformed format
8
+ #
9
+ # WOFF2 glyf table transformation splits glyph data into separate streams
10
+ # for better compression. This transformer reconstructs the standard
11
+ # `glyf` and `loca` table formats from the transformed data.
12
+ #
13
+ # Transformation format (Section 5 of WOFF2 spec):
14
+ # - Separate streams for nContour, nPoints, flags, x-coords, y-coords
15
+ # - Variable-length integer encoding (255UInt16)
16
+ # - Composite glyph components stored separately
17
+ #
18
+ # See: https://www.w3.org/TR/WOFF2/#glyf_table_format
19
+ #
20
+ # @example Reconstructing tables
21
+ # result = GlyfTransformer.reconstruct(transformed_data, num_glyphs)
22
+ # glyf_data = result[:glyf]
23
+ # loca_data = result[:loca]
24
+ class GlyfTransformer
25
+ # Glyph flags
26
+ ON_CURVE_POINT = 0x01
27
+ X_SHORT_VECTOR = 0x02
28
+ Y_SHORT_VECTOR = 0x04
29
+ REPEAT_FLAG = 0x08
30
+ X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR = 0x10
31
+ Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR = 0x20
32
+
33
+ # Composite glyph flags
34
+ ARG_1_AND_2_ARE_WORDS = 0x0001
35
+ ARGS_ARE_XY_VALUES = 0x0002
36
+ ROUND_XY_TO_GRID = 0x0004
37
+ WE_HAVE_A_SCALE = 0x0008
38
+ MORE_COMPONENTS = 0x0020
39
+ WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
40
+ WE_HAVE_A_TWO_BY_TWO = 0x0080
41
+ WE_HAVE_INSTRUCTIONS = 0x0100
42
+ USE_MY_METRICS = 0x0200
43
+ OVERLAP_COMPOUND = 0x0400
44
+ HAVE_VARIATIONS = 0x1000 # Variable font variation data follows
45
+
46
+ # Reconstruct glyf and loca tables from transformed data
47
+ #
48
+ # @param transformed_data [String] The transformed glyf table data
49
+ # @param num_glyphs [Integer] Number of glyphs from maxp table
50
+ # @param variable_font [Boolean] Whether this is a variable font with variation data
51
+ # @return [Hash] { glyf: String, loca: String }
52
+ # @raise [InvalidFontError] If data is corrupted or invalid
53
+ def self.reconstruct(transformed_data, num_glyphs, variable_font: false)
54
+ io = StringIO.new(transformed_data)
55
+
56
+ # Check minimum size for header
57
+ if io.size < 8
58
+ raise InvalidFontError, "Transformed glyf data too small: #{io.size} bytes"
59
+ end
60
+
61
+ # Read header
62
+ read_uint32(io)
63
+ num_glyphs_in_data = read_uint16(io)
64
+ index_format = read_uint16(io)
65
+
66
+ if num_glyphs_in_data != num_glyphs
67
+ raise InvalidFontError,
68
+ "Glyph count mismatch: expected #{num_glyphs}, got #{num_glyphs_in_data}"
69
+ end
70
+
71
+ # Read nContour stream
72
+ n_contour_data = read_stream_safely(io, "nContour", variable_font: variable_font)
73
+
74
+ # Read nPoints stream
75
+ n_points_data = read_stream_safely(io, "nPoints", variable_font: variable_font)
76
+
77
+ # Read flag stream
78
+ flag_data = read_stream_safely(io, "flag", variable_font: variable_font)
79
+
80
+ # Read glyph stream (coordinates, instructions, composite data)
81
+ glyph_data = read_stream_safely(io, "glyph", variable_font: variable_font)
82
+
83
+ # Read composite stream
84
+ composite_data = read_stream_safely(io, "composite", variable_font: variable_font)
85
+
86
+ # Read bbox stream
87
+ bbox_data = read_stream_safely(io, "bbox", variable_font: variable_font)
88
+
89
+ # Read instruction stream
90
+ instruction_data = read_stream_safely(io, "instruction", variable_font: variable_font)
91
+
92
+ # Parse streams
93
+ n_contours = parse_n_contour_stream(StringIO.new(n_contour_data), num_glyphs)
94
+
95
+ # Reconstruct glyphs
96
+ glyphs = reconstruct_glyphs(
97
+ n_contours,
98
+ StringIO.new(n_points_data),
99
+ StringIO.new(flag_data),
100
+ StringIO.new(glyph_data),
101
+ StringIO.new(composite_data),
102
+ StringIO.new(bbox_data),
103
+ StringIO.new(instruction_data),
104
+ variable_font: variable_font
105
+ )
106
+
107
+ # Build glyf and loca tables
108
+ build_tables(glyphs, index_format)
109
+ end
110
+
111
+ # Safely read a stream with bounds checking
112
+ #
113
+ # @param io [StringIO] Input stream
114
+ # @param stream_name [String] Name of stream for error messages
115
+ # @param variable_font [Boolean] Whether this is a variable font (allows incomplete streams)
116
+ # @return [String] Stream data (empty if not available)
117
+ def self.read_stream_safely(io, stream_name, variable_font: false)
118
+ remaining = io.size - io.pos
119
+ if remaining < 4
120
+ # Not enough data for stream size - return empty stream
121
+ return ""
122
+ end
123
+
124
+ # Read stream size safely
125
+ size_bytes = io.read(4)
126
+ return "" unless size_bytes && size_bytes.bytesize == 4
127
+
128
+ stream_size = size_bytes.unpack1("N")
129
+ remaining = io.size - io.pos
130
+
131
+ if remaining < stream_size
132
+ # Stream size extends beyond available data
133
+ # Read what we can
134
+ available = io.read(remaining) || ""
135
+ # For variable fonts, we may have incomplete streams - just return what we have
136
+ available
137
+ else
138
+ io.read(stream_size) || ""
139
+ end
140
+ end
141
+
142
+ # Read variable-length 255UInt16 integer
143
+ #
144
+ # Format from WOFF2 spec:
145
+ # - value < 253: one byte
146
+ # - value == 253: 253 + next uint16
147
+ # - value == 254: 253 * 2 + next uint16
148
+ # - value == 255: 253 * 3 + next uint16
149
+ #
150
+ # @param io [StringIO] Input stream
151
+ # @return [Integer] Decoded value, or 0 if not enough data
152
+ def self.read_255_uint16(io)
153
+ return 0 if io.eof? || (io.size - io.pos) < 1
154
+
155
+ code_byte = io.read(1)
156
+ return 0 unless code_byte && code_byte.bytesize == 1
157
+
158
+ code = code_byte.unpack1("C")
159
+
160
+ case code
161
+ when 255
162
+ return 0 if io.eof? || (io.size - io.pos) < 2
163
+ value_bytes = io.read(2)
164
+ return 0 unless value_bytes && value_bytes.bytesize == 2
165
+ 759 + value_bytes.unpack1("n") # 253 * 3 + value
166
+ when 254
167
+ return 0 if io.eof? || (io.size - io.pos) < 2
168
+ value_bytes = io.read(2)
169
+ return 0 unless value_bytes && value_bytes.bytesize == 2
170
+ 506 + value_bytes.unpack1("n") # 253 * 2 + value
171
+ when 253
172
+ return 0 if io.eof? || (io.size - io.pos) < 2
173
+ value_bytes = io.read(2)
174
+ return 0 unless value_bytes && value_bytes.bytesize == 2
175
+ 253 + value_bytes.unpack1("n")
176
+ else
177
+ code
178
+ end
179
+ end
180
+
181
+ # Parse nContour stream
182
+ #
183
+ # @param io [StringIO] Input stream
184
+ # @param num_glyphs [Integer] Number of glyphs
185
+ # @return [Array<Integer>] Number of contours per glyph (-1 for composite)
186
+ def self.parse_n_contour_stream(io, num_glyphs)
187
+ n_contours = []
188
+ num_glyphs.times do
189
+ # For variable fonts, stream may be incomplete
190
+ break if io.eof? || (io.size - io.pos) < 2
191
+
192
+ value = read_int16(io)
193
+ n_contours << value
194
+ end
195
+
196
+ # Pad with zeros if we have fewer contours than glyphs
197
+ while n_contours.size < num_glyphs
198
+ n_contours << 0
199
+ end
200
+
201
+ n_contours
202
+ end
203
+
204
+ # Reconstruct all glyphs
205
+ #
206
+ # @param n_contours [Array<Integer>] Contour counts
207
+ # @param n_points_io [StringIO] Points stream
208
+ # @param flag_io [StringIO] Flag stream
209
+ # @param glyph_io [StringIO] Glyph data stream
210
+ # @param composite_io [StringIO] Composite glyph stream
211
+ # @param bbox_io [StringIO] Bounding box stream
212
+ # @param instruction_io [StringIO] Instruction stream
213
+ # @param variable_font [Boolean] Whether this is a variable font
214
+ # @return [Array<String>] Reconstructed glyph data
215
+ def self.reconstruct_glyphs(n_contours, n_points_io, flag_io, glyph_io,
216
+ composite_io, bbox_io, instruction_io, variable_font: false)
217
+ glyphs = []
218
+
219
+ n_contours.each do |num_contours|
220
+ if num_contours.zero?
221
+ # Empty glyph
222
+ glyphs << ""
223
+ elsif num_contours.positive?
224
+ # Simple glyph
225
+ glyphs << reconstruct_simple_glyph(
226
+ num_contours, n_points_io, flag_io,
227
+ glyph_io, bbox_io, instruction_io
228
+ )
229
+ elsif num_contours == -1
230
+ # Composite glyph
231
+ glyphs << reconstruct_composite_glyph(
232
+ composite_io, bbox_io, instruction_io, variable_font: variable_font
233
+ )
234
+ else
235
+ raise InvalidFontError, "Invalid nContours value: #{num_contours}"
236
+ end
237
+ end
238
+
239
+ glyphs
240
+ end
241
+
242
+ # Reconstruct a simple glyph
243
+ #
244
+ # @param num_contours [Integer] Number of contours
245
+ # @param n_points_io [StringIO] Points stream
246
+ # @param flag_io [StringIO] Flag stream
247
+ # @param glyph_io [StringIO] Glyph data stream
248
+ # @param bbox_io [StringIO] Bounding box stream
249
+ # @param instruction_io [StringIO] Instruction stream
250
+ # @return [String] Glyph data in standard format
251
+ def self.reconstruct_simple_glyph(num_contours, n_points_io, flag_io,
252
+ glyph_io, bbox_io, instruction_io)
253
+ # Read end points of contours
254
+ end_pts_of_contours = []
255
+ num_contours.times do
256
+ if end_pts_of_contours.empty?
257
+ end_pts_of_contours << read_255_uint16(n_points_io)
258
+ else
259
+ delta = read_255_uint16(n_points_io)
260
+ end_pts_of_contours << end_pts_of_contours.last + delta + 1
261
+ end
262
+ end
263
+
264
+ total_points = end_pts_of_contours.last + 1
265
+
266
+ # Read flags
267
+ flags = read_flags(flag_io, total_points)
268
+
269
+ # Read coordinates
270
+ x_coordinates = read_coordinates(glyph_io, flags, X_SHORT_VECTOR,
271
+ X_IS_SAME_OR_POSITIVE_X_SHORT_VECTOR)
272
+ y_coordinates = read_coordinates(glyph_io, flags, Y_SHORT_VECTOR,
273
+ Y_IS_SAME_OR_POSITIVE_Y_SHORT_VECTOR)
274
+
275
+ # Read bounding box safely
276
+ bbox_remaining = bbox_io.size - bbox_io.pos
277
+ if bbox_remaining < 8
278
+ # Not enough data, use default bounding box
279
+ x_min = y_min = x_max = y_max = 0
280
+ else
281
+ bbox_bytes = bbox_io.read(8)
282
+ unless bbox_bytes && bbox_bytes.bytesize == 8
283
+ x_min = y_min = x_max = y_max = 0
284
+ else
285
+ x_min, y_min, x_max, y_max = bbox_bytes.unpack("n4")
286
+ # Convert to signed
287
+ x_min = x_min > 0x7FFF ? x_min - 0x10000 : x_min
288
+ y_min = y_min > 0x7FFF ? y_min - 0x10000 : y_min
289
+ x_max = x_max > 0x7FFF ? x_max - 0x10000 : x_max
290
+ y_max = y_max > 0x7FFF ? y_max - 0x10000 : y_max
291
+ end
292
+ end
293
+
294
+ # Read instructions safely
295
+ instruction_length = 0
296
+ instructions = ""
297
+
298
+ inst_remaining = instruction_io.size - instruction_io.pos
299
+ if inst_remaining >= 2
300
+ inst_length_data = read_255_uint16(instruction_io)
301
+ if inst_length_data
302
+ instruction_length = inst_length_data
303
+ if instruction_length.positive?
304
+ inst_remaining = instruction_io.size - instruction_io.pos
305
+ if inst_remaining >= instruction_length
306
+ instructions = instruction_io.read(instruction_length) || ""
307
+ else
308
+ # Read what we can
309
+ instructions = instruction_io.read(inst_remaining) || ""
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ # Build glyph data in standard format
316
+ build_simple_glyph_data(num_contours, x_min, y_min, x_max, y_max,
317
+ end_pts_of_contours, instructions, flags,
318
+ x_coordinates, y_coordinates)
319
+ end
320
+
321
+ # Reconstruct a composite glyph
322
+ #
323
+ # @param composite_io [StringIO] Composite stream
324
+ # @param bbox_io [StringIO] Bounding box stream
325
+ # @param instruction_io [StringIO] Instruction stream
326
+ # @param variable_font [Boolean] Whether this is a variable font
327
+ # @return [String] Glyph data in standard format
328
+ def self.reconstruct_composite_glyph(composite_io, bbox_io, instruction_io, variable_font: false)
329
+ # Track available bytes to prevent EOF errors
330
+ composite_size = composite_io.size - composite_io.pos
331
+
332
+ # Validate minimum size (at least flags + glyph_index + args)
333
+ return "" if composite_size < 8
334
+
335
+ # Read bounding box safely
336
+ bbox_remaining = bbox_io.size - bbox_io.pos
337
+ if bbox_remaining < 8
338
+ # Not enough data for bounding box, return empty glyph
339
+ return ""
340
+ end
341
+
342
+ bbox_bytes = bbox_io.read(8)
343
+ unless bbox_bytes && bbox_bytes.bytesize == 8
344
+ return ""
345
+ end
346
+
347
+ x_min, y_min, x_max, y_max = bbox_bytes.unpack("n4")
348
+ # Convert to signed
349
+ x_min = x_min > 0x7FFF ? x_min - 0x10000 : x_min
350
+ y_min = y_min > 0x7FFF ? y_min - 0x10000 : y_min
351
+ x_max = x_max > 0x7FFF ? x_max - 0x10000 : x_max
352
+ y_max = y_max > 0x7FFF ? y_max - 0x10000 : y_max
353
+
354
+ # Read composite data
355
+ composite_data = +""
356
+ has_instructions = false
357
+ has_variations = false
358
+
359
+ loop do
360
+ # Check if we have enough bytes for flags and glyph_index
361
+ remaining = composite_io.size - composite_io.pos
362
+ break if composite_io.eof? || remaining < 4
363
+
364
+ # Read flags and glyph_index safely
365
+ component_header = composite_io.read(4)
366
+ break unless component_header && component_header.bytesize == 4
367
+ flags, glyph_index = component_header.unpack("n2")
368
+
369
+ # Write flags and index
370
+ composite_data << [flags].pack("n")
371
+ composite_data << [glyph_index].pack("n")
372
+
373
+ # Read arguments (depend on flags)
374
+ if (flags & ARG_1_AND_2_ARE_WORDS).zero?
375
+ remaining = composite_io.size - composite_io.pos
376
+ break if composite_io.eof? || remaining < 2
377
+ arg_bytes = composite_io.read(2)
378
+ break unless arg_bytes && arg_bytes.bytesize == 2
379
+ arg1, arg2 = arg_bytes.unpack("c2")
380
+ composite_data << [arg1, arg2].pack("c2")
381
+ else
382
+ remaining = composite_io.size - composite_io.pos
383
+ break if composite_io.eof? || remaining < 4
384
+ arg_bytes = composite_io.read(4)
385
+ break unless arg_bytes && arg_bytes.bytesize == 4
386
+ arg1, arg2 = arg_bytes.unpack("n2")
387
+ # Convert to signed
388
+ arg1 = arg1 > 0x7FFF ? arg1 - 0x10000 : arg1
389
+ arg2 = arg2 > 0x7FFF ? arg2 - 0x10000 : arg2
390
+ composite_data << [arg1, arg2].pack("n2")
391
+ end
392
+
393
+ # Read transformation matrix (depends on flags) with bounds checking
394
+ if (flags & WE_HAVE_A_SCALE) != 0
395
+ remaining = composite_io.size - composite_io.pos
396
+ break if composite_io.eof? || remaining < 2
397
+ scale_bytes = composite_io.read(2)
398
+ break unless scale_bytes && scale_bytes.bytesize == 2
399
+ scale = scale_bytes.unpack1("n")
400
+ composite_data << [scale].pack("n")
401
+ elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0
402
+ remaining = composite_io.size - composite_io.pos
403
+ break if composite_io.eof? || remaining < 4
404
+ scale_bytes = composite_io.read(4)
405
+ break unless scale_bytes && scale_bytes.bytesize == 4
406
+ x_scale, y_scale = scale_bytes.unpack("n2")
407
+ composite_data << [x_scale, y_scale].pack("n2")
408
+ elsif (flags & WE_HAVE_A_TWO_BY_TWO) != 0
409
+ remaining = composite_io.size - composite_io.pos
410
+ break if composite_io.eof? || remaining < 8
411
+ matrix_bytes = composite_io.read(8)
412
+ break unless matrix_bytes && matrix_bytes.bytesize == 8
413
+ x_scale, scale01, scale10, y_scale = matrix_bytes.unpack("n4")
414
+ composite_data << [x_scale, scale01, scale10, y_scale].pack("n4")
415
+ end
416
+
417
+ # Check for variable font variation data
418
+ # Only parse if this is a variable font and the flag is set
419
+ if variable_font && (flags & HAVE_VARIATIONS) != 0
420
+ has_variations = true
421
+ # Read tuple variation count and data
422
+ remaining = composite_io.size - composite_io.pos
423
+ if !composite_io.eof? && remaining >= 2
424
+ # Read tuple count safely
425
+ tuple_bytes = composite_io.read(2)
426
+ if tuple_bytes && tuple_bytes.bytesize == 2
427
+ tuple_count = tuple_bytes.unpack1("n")
428
+ composite_data << [tuple_count].pack("n")
429
+
430
+ # Each tuple has variation data - read and preserve it
431
+ tuple_count.times do
432
+ remaining = composite_io.size - composite_io.pos
433
+ break if composite_io.eof? || remaining < 4
434
+
435
+ # Read variation data (2 int16 values per tuple)
436
+ var_bytes = composite_io.read(4)
437
+ break unless var_bytes && var_bytes.bytesize == 4
438
+
439
+ var1, var2 = var_bytes.unpack("n2")
440
+ # Convert to signed if needed
441
+ var1 = var1 > 0x7FFF ? var1 - 0x10000 : var1
442
+ var2 = var2 > 0x7FFF ? var2 - 0x10000 : var2
443
+ composite_data << [var1, var2].pack("n2")
444
+ end
445
+ end
446
+ end
447
+ end
448
+
449
+ has_instructions = (flags & WE_HAVE_INSTRUCTIONS) != 0
450
+
451
+ break if (flags & MORE_COMPONENTS).zero?
452
+ end
453
+
454
+ # Add instructions if present
455
+ instructions = +""
456
+ if has_instructions
457
+ # Read instruction length safely
458
+ remaining = instruction_io.size - instruction_io.pos
459
+ if !instruction_io.eof? && remaining >= 2
460
+ length_bytes = instruction_io.read(2)
461
+ if length_bytes && length_bytes.bytesize == 2
462
+ instruction_length = length_bytes.unpack1("n")
463
+ if instruction_length.positive?
464
+ remaining = instruction_io.size - instruction_io.pos
465
+ if remaining >= instruction_length
466
+ instructions = instruction_io.read(instruction_length) || ""
467
+ else
468
+ # Read what we can
469
+ instructions = instruction_io.read(remaining) || ""
470
+ end
471
+ end
472
+ end
473
+ end
474
+ end
475
+
476
+ # Build composite glyph data
477
+ data = +""
478
+ data << [-1].pack("n") # numberOfContours = -1
479
+ data << [x_min, y_min, x_max, y_max].pack("n4")
480
+ data << composite_data
481
+ data << [instructions.bytesize].pack("n") if has_instructions
482
+ data << instructions if has_instructions
483
+
484
+ data
485
+ end
486
+
487
+ # Read flags with repeat handling
488
+ #
489
+ # @param io [StringIO] Flag stream
490
+ # @param count [Integer] Number of flags to read
491
+ # @return [Array<Integer>] Flag values
492
+ def self.read_flags(io, count)
493
+ flags = []
494
+
495
+ while flags.size < count
496
+ # EOF protection for variable fonts
497
+ break if io.eof? || (io.size - io.pos) < 1
498
+
499
+ flag = read_uint8(io)
500
+ flags << flag
501
+
502
+ if (flag & REPEAT_FLAG) != 0
503
+ break if io.eof? || (io.size - io.pos) < 1
504
+ repeat_count = read_uint8(io)
505
+ repeat_count.times { flags << flag }
506
+ end
507
+ end
508
+
509
+ # Pad with zero flags if needed
510
+ while flags.size < count
511
+ flags << 0
512
+ end
513
+
514
+ flags
515
+ end
516
+
517
+ # Read coordinates
518
+ #
519
+ # @param io [StringIO] Glyph stream
520
+ # @param flags [Array<Integer>] Flag values
521
+ # @param short_flag [Integer] Flag bit for short vector
522
+ # @param same_or_positive_flag [Integer] Flag bit for same/positive
523
+ # @return [Array<Integer>] Coordinate values
524
+ def self.read_coordinates(io, flags, short_flag, same_or_positive_flag)
525
+ coords = []
526
+ value = 0
527
+
528
+ flags.each do |flag|
529
+ # EOF protection
530
+ if (flag & short_flag) != 0
531
+ break if io.eof? || (io.size - io.pos) < 1
532
+ # Short vector (one byte)
533
+ delta = read_uint8(io)
534
+ delta = -delta if (flag & same_or_positive_flag).zero?
535
+ elsif (flag & same_or_positive_flag) != 0
536
+ # Same as previous (delta = 0)
537
+ delta = 0
538
+ else
539
+ break if io.eof? || (io.size - io.pos) < 2
540
+ # Long vector (two bytes, signed)
541
+ delta = read_int16(io)
542
+ end
543
+
544
+ value += delta
545
+ coords << value
546
+ end
547
+
548
+ # Pad with last value if needed
549
+ last_val = coords.last || 0
550
+ while coords.size < flags.size
551
+ coords << last_val
552
+ end
553
+
554
+ coords
555
+ end
556
+
557
+ # Build simple glyph data in standard format
558
+ #
559
+ # @return [String] Glyph data
560
+ def self.build_simple_glyph_data(num_contours, x_min, y_min, x_max, y_max,
561
+ end_pts, instructions, flags, x_coords, y_coords)
562
+ data = +""
563
+ data << [num_contours].pack("n")
564
+ data << [x_min, y_min, x_max, y_max].pack("n4")
565
+
566
+ end_pts.each { |pt| data << [pt].pack("n") }
567
+
568
+ data << [instructions.bytesize].pack("n")
569
+ data << instructions
570
+
571
+ flags.each { |flag| data << [flag].pack("C") }
572
+
573
+ # Write x-coordinates
574
+ prev_x = 0
575
+ x_coords.each do |x|
576
+ delta = x - prev_x
577
+ prev_x = x
578
+
579
+ data << if delta.abs <= 255
580
+ [delta.abs].pack("C")
581
+ else
582
+ [delta].pack("n")
583
+ end
584
+ end
585
+
586
+ # Write y-coordinates
587
+ prev_y = 0
588
+ y_coords.each do |y|
589
+ delta = y - prev_y
590
+ prev_y = y
591
+
592
+ data << if delta.abs <= 255
593
+ [delta.abs].pack("C")
594
+ else
595
+ [delta].pack("n")
596
+ end
597
+ end
598
+
599
+ data
600
+ end
601
+
602
+ # Build glyf and loca tables
603
+ #
604
+ # @param glyphs [Array<String>] Glyph data
605
+ # @param index_format [Integer] Loca format (0 = short, 1 = long)
606
+ # @return [Hash] { glyf: String, loca: String }
607
+ def self.build_tables(glyphs, index_format)
608
+ glyf_data = +""
609
+ loca_offsets = [0]
610
+
611
+ glyphs.each do |glyph|
612
+ glyf_data << glyph
613
+
614
+ # Add padding to 4-byte boundary
615
+ padding = (4 - (glyph.bytesize % 4)) % 4
616
+ glyf_data << ("\x00" * padding)
617
+
618
+ loca_offsets << glyf_data.bytesize
619
+ end
620
+
621
+ # Build loca table
622
+ loca_data = +""
623
+ if index_format.zero?
624
+ # Short format (divide offsets by 2)
625
+ loca_offsets.each do |offset|
626
+ loca_data << [offset / 2].pack("n")
627
+ end
628
+ else
629
+ # Long format
630
+ loca_offsets.each do |offset|
631
+ loca_data << [offset].pack("N")
632
+ end
633
+ end
634
+
635
+ { glyf: glyf_data, loca: loca_data }
636
+ end
637
+
638
+ # Helper methods for reading binary data
639
+
640
+ def self.read_uint8(io)
641
+ io.read(1)&.unpack1("C") || raise(EOFError, "Unexpected end of stream")
642
+ end
643
+
644
+ def self.read_int8(io)
645
+ io.read(1)&.unpack1("c") || raise(EOFError, "Unexpected end of stream")
646
+ end
647
+
648
+ def self.read_uint16(io)
649
+ io.read(2)&.unpack1("n") || raise(EOFError, "Unexpected end of stream")
650
+ end
651
+
652
+ def self.read_int16(io)
653
+ value = read_uint16(io)
654
+ value > 0x7FFF ? value - 0x10000 : value
655
+ end
656
+
657
+ def self.read_uint32(io)
658
+ io.read(4)&.unpack1("N") || raise(EOFError, "Unexpected end of stream")
659
+ end
660
+
661
+ def self.read_f2dot14(io)
662
+ read_uint16(io)
663
+ end
664
+ end
665
+ end
666
+ end