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,450 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../models/outline"
4
+ require_relative "curve_converter"
5
+
6
+ module Fontisan
7
+ module Tables
8
+ # Builds binary TrueType glyph data from universal outline representation
9
+ #
10
+ # [`GlyphBuilder`](lib/fontisan/tables/glyf/glyph_builder.rb) converts the format-agnostic
11
+ # [`Outline`](lib/fontisan/models/outline.rb) model into binary TrueType glyph format.
12
+ # It handles both simple and compound glyphs with proper encoding:
13
+ #
14
+ # **Simple Glyphs**:
15
+ # - Converts universal outline to TrueType contours
16
+ # - Uses [`CurveConverter`](lib/fontisan/tables/glyf/curve_converter.rb) for cubic→quadratic conversion
17
+ # - Delta-encodes coordinates for compact storage
18
+ # - Applies flag compression with run-length encoding
19
+ # - Calculates accurate bounding box
20
+ #
21
+ # **Compound Glyphs**:
22
+ # - Encodes component references
23
+ # - Supports transformation matrices
24
+ # - Handles positioning via points or offsets
25
+ #
26
+ # @example Building a simple glyph from outline
27
+ # outline = Fontisan::Models::Outline.new(...)
28
+ # binary_data = Fontisan::Tables::GlyphBuilder.build_simple_glyph(outline)
29
+ #
30
+ # @example Building a compound glyph
31
+ # components = [
32
+ # { glyph_index: 10, x_offset: 100, y_offset: 0 },
33
+ # { glyph_index: 20, x_offset: 300, y_offset: 0 }
34
+ # ]
35
+ # bbox = { x_min: 0, y_min: 0, x_max: 500, y_max: 700 }
36
+ # binary_data = Fontisan::Tables::GlyphBuilder.build_compound_glyph(components, bbox)
37
+ class GlyphBuilder
38
+ # Flag constants (matching SimpleGlyph)
39
+ ON_CURVE_POINT = 0x01
40
+ X_SHORT_VECTOR = 0x02
41
+ Y_SHORT_VECTOR = 0x04
42
+ REPEAT_FLAG = 0x08
43
+ X_IS_SAME_OR_POSITIVE_X_SHORT = 0x10
44
+ Y_IS_SAME_OR_POSITIVE_Y_SHORT = 0x20
45
+
46
+ # Component flag constants (matching CompoundGlyph)
47
+ ARG_1_AND_2_ARE_WORDS = 0x0001
48
+ ARGS_ARE_XY_VALUES = 0x0002
49
+ ROUND_XY_TO_GRID = 0x0004
50
+ WE_HAVE_A_SCALE = 0x0008
51
+ MORE_COMPONENTS = 0x0020
52
+ WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
53
+ WE_HAVE_A_TWO_BY_TWO = 0x0080
54
+ WE_HAVE_INSTRUCTIONS = 0x0100
55
+ USE_MY_METRICS = 0x0200
56
+ OVERLAP_COMPOUND = 0x0400
57
+
58
+ # Build a simple TrueType glyph from universal outline
59
+ #
60
+ # Converts the universal outline to TrueType format with:
61
+ # - Quadratic curves (cubic curves converted via [`CurveConverter`](lib/fontisan/tables/glyf/curve_converter.rb))
62
+ # - Delta-encoded coordinates
63
+ # - Flag compression
64
+ # - Accurate bounding box
65
+ #
66
+ # @param outline [Fontisan::Models::Outline] Universal outline
67
+ # @param instructions [String] Optional TrueType instructions (default: empty)
68
+ # @return [String] Binary glyph data
69
+ # @raise [ArgumentError] If outline is invalid or empty
70
+ def self.build_simple_glyph(outline, instructions: "".b)
71
+ raise ArgumentError, "outline cannot be nil" if outline.nil?
72
+ raise ArgumentError, "outline must be Outline" unless outline.is_a?(Fontisan::Models::Outline)
73
+ raise ArgumentError, "outline cannot be empty" if outline.empty?
74
+
75
+ # Convert outline to TrueType contours
76
+ contours = outline.to_truetype_contours
77
+ raise ArgumentError, "no contours in outline" if contours.empty?
78
+
79
+ # Calculate bounding box from contours
80
+ bbox = calculate_bounding_box(contours)
81
+
82
+ # Build binary data
83
+ build_simple_glyph_data(contours, bbox, instructions)
84
+ end
85
+
86
+ # Build a compound TrueType glyph
87
+ #
88
+ # Creates a compound glyph by referencing other glyphs with optional
89
+ # transformations. Each component can specify positioning and scaling.
90
+ #
91
+ # @param components [Array<Hash>] Component descriptions
92
+ # Each component hash can contain:
93
+ # - `:glyph_index` (Integer, required): Referenced glyph ID
94
+ # - `:x_offset` (Integer): X offset (default: 0)
95
+ # - `:y_offset` (Integer): Y offset (default: 0)
96
+ # - `:scale` (Float): Uniform scale (optional)
97
+ # - `:scale_x` (Float): X-axis scale (optional)
98
+ # - `:scale_y` (Float): Y-axis scale (optional)
99
+ # - `:scale_01` (Float): Matrix element (0,1) (optional)
100
+ # - `:scale_10` (Float): Matrix element (1,0) (optional)
101
+ # - `:use_my_metrics` (Boolean): Use component's metrics (default: false)
102
+ # - `:overlap` (Boolean): Mark as overlapping (default: false)
103
+ # @param bbox [Hash] Bounding box {:x_min, :y_min, :x_max, :y_max}
104
+ # @param instructions [String] Optional TrueType instructions (default: empty)
105
+ # @return [String] Binary glyph data
106
+ # @raise [ArgumentError] If parameters are invalid
107
+ def self.build_compound_glyph(components, bbox, instructions: "".b)
108
+ raise ArgumentError, "components cannot be nil" if components.nil?
109
+ raise ArgumentError, "components must be Array" unless components.is_a?(Array)
110
+ raise ArgumentError, "components cannot be empty" if components.empty?
111
+
112
+ validate_bbox!(bbox)
113
+
114
+ build_compound_glyph_data(components, bbox, instructions)
115
+ end
116
+
117
+ private_class_method def self.build_simple_glyph_data(contours, bbox, instructions)
118
+ num_contours = contours.length
119
+
120
+ # Build endPtsOfContours array
121
+ end_pts_of_contours = []
122
+ total_points = 0
123
+ contours.each do |contour|
124
+ total_points += contour.length
125
+ end_pts_of_contours << (total_points - 1)
126
+ end
127
+
128
+ # Flatten all points
129
+ all_points = contours.flatten
130
+
131
+ # Encode flags and coordinates
132
+ flags_data, x_coords_data, y_coords_data = encode_coordinates(all_points)
133
+
134
+ # Build binary data
135
+ data = (+"").force_encoding(Encoding::BINARY)
136
+
137
+ # Header (10 bytes)
138
+ data << [num_contours].pack("n") # numberOfContours
139
+ data << [bbox[:x_min], bbox[:y_min], bbox[:x_max], bbox[:y_max]].pack("n4")
140
+
141
+ # endPtsOfContours
142
+ data << end_pts_of_contours.pack("n*")
143
+
144
+ # Instructions
145
+ data << [instructions.bytesize].pack("n")
146
+ data << instructions if instructions.bytesize.positive?
147
+
148
+ # Flags
149
+ data << flags_data
150
+
151
+ # Coordinates
152
+ data << x_coords_data
153
+ data << y_coords_data
154
+
155
+ data
156
+ end
157
+
158
+ private_class_method def self.build_compound_glyph_data(components, bbox, instructions)
159
+ data = (+"").force_encoding(Encoding::BINARY)
160
+
161
+ # Header (10 bytes) - numberOfContours = -1 for compound
162
+ data << [-1].pack("n") # Use signed pack, will convert to 0xFFFF
163
+ data << [bbox[:x_min], bbox[:y_min], bbox[:x_max], bbox[:y_max]].pack("n4")
164
+
165
+ # Encode components
166
+ has_instructions = instructions.bytesize.positive?
167
+ components.each_with_index do |component, index|
168
+ is_last = (index == components.length - 1)
169
+ component_data = encode_component(component, is_last, has_instructions)
170
+ data << component_data
171
+ end
172
+
173
+ # Instructions (if any)
174
+ if has_instructions
175
+ data << [instructions.bytesize].pack("n")
176
+ data << instructions
177
+ end
178
+
179
+ data
180
+ end
181
+
182
+ private_class_method def self.encode_component(component, is_last, has_instructions)
183
+ validate_component!(component)
184
+
185
+ glyph_index = component[:glyph_index]
186
+ x_offset = component[:x_offset] || 0
187
+ y_offset = component[:y_offset] || 0
188
+
189
+ # Build flags
190
+ flags = ARGS_ARE_XY_VALUES # Always use x,y offsets
191
+
192
+ # Determine if we need 16-bit arguments
193
+ if x_offset.abs > 127 || y_offset.abs > 127
194
+ flags |= ARG_1_AND_2_ARE_WORDS
195
+ end
196
+
197
+ # Add transformation flags
198
+ if component[:scale_01] || component[:scale_10]
199
+ # 2x2 matrix
200
+ flags |= WE_HAVE_A_TWO_BY_TWO
201
+ elsif component[:scale_x] && component[:scale_y]
202
+ # Separate x,y scale
203
+ flags |= WE_HAVE_AN_X_AND_Y_SCALE
204
+ elsif component[:scale]
205
+ # Uniform scale
206
+ flags |= WE_HAVE_A_SCALE
207
+ end
208
+
209
+ # Add more components flag if not last
210
+ flags |= MORE_COMPONENTS unless is_last
211
+
212
+ # Add instructions flag if last and has instructions
213
+ flags |= WE_HAVE_INSTRUCTIONS if is_last && has_instructions
214
+
215
+ # Add optional flags
216
+ flags |= USE_MY_METRICS if component[:use_my_metrics]
217
+ flags |= OVERLAP_COMPOUND if component[:overlap]
218
+
219
+ # Build binary data
220
+ data = (+"").force_encoding(Encoding::BINARY)
221
+ data << [flags, glyph_index].pack("n2")
222
+
223
+ # Encode arguments
224
+ data << if (flags & ARG_1_AND_2_ARE_WORDS).zero?
225
+ # 8-bit signed
226
+ [x_offset, y_offset].pack("c2")
227
+ else
228
+ # 16-bit signed
229
+ [x_offset, y_offset].pack("n2")
230
+ end
231
+
232
+ # Encode transformation
233
+ if (flags & WE_HAVE_A_TWO_BY_TWO) != 0
234
+ # 2x2 matrix (4 F2DOT14 values)
235
+ scale_x = component[:scale_x] || 1.0
236
+ scale_y = component[:scale_y] || 1.0
237
+ scale_01 = component[:scale_01] || 0.0
238
+ scale_10 = component[:scale_10] || 0.0
239
+ data << [
240
+ float_to_f2dot14(scale_x),
241
+ float_to_f2dot14(scale_01),
242
+ float_to_f2dot14(scale_10),
243
+ float_to_f2dot14(scale_y),
244
+ ].pack("n4")
245
+ elsif (flags & WE_HAVE_AN_X_AND_Y_SCALE) != 0
246
+ # Separate x,y scale (2 F2DOT14 values)
247
+ scale_x = component[:scale_x] || 1.0
248
+ scale_y = component[:scale_y] || 1.0
249
+ data << [
250
+ float_to_f2dot14(scale_x),
251
+ float_to_f2dot14(scale_y),
252
+ ].pack("n2")
253
+ elsif (flags & WE_HAVE_A_SCALE) != 0
254
+ # Uniform scale (1 F2DOT14 value)
255
+ scale = component[:scale] || 1.0
256
+ data << [float_to_f2dot14(scale)].pack("n")
257
+ end
258
+
259
+ data
260
+ end
261
+
262
+ private_class_method def self.encode_coordinates(points)
263
+ flags = []
264
+ x_deltas = []
265
+ y_deltas = []
266
+
267
+ prev_x = 0
268
+ prev_y = 0
269
+
270
+ # Calculate deltas and determine flags
271
+ points.each do |point|
272
+ x = point[:x]
273
+ y = point[:y]
274
+ on_curve = point[:on_curve]
275
+
276
+ dx = x - prev_x
277
+ dy = y - prev_y
278
+
279
+ flag = 0
280
+ flag |= ON_CURVE_POINT if on_curve
281
+
282
+ # X coordinate encoding
283
+ if dx.zero?
284
+ flag |= X_IS_SAME_OR_POSITIVE_X_SHORT
285
+ elsif dx >= -255 && dx <= 255
286
+ flag |= X_SHORT_VECTOR
287
+ flag |= X_IS_SAME_OR_POSITIVE_X_SHORT if dx.positive?
288
+ x_deltas << dx.abs
289
+ else
290
+ x_deltas << dx
291
+ end
292
+
293
+ # Y coordinate encoding
294
+ if dy.zero?
295
+ flag |= Y_IS_SAME_OR_POSITIVE_Y_SHORT
296
+ elsif dy >= -255 && dy <= 255
297
+ flag |= Y_SHORT_VECTOR
298
+ flag |= Y_IS_SAME_OR_POSITIVE_Y_SHORT if dy.positive?
299
+ y_deltas << dy.abs
300
+ else
301
+ y_deltas << dy
302
+ end
303
+
304
+ flags << flag
305
+ prev_x = x
306
+ prev_y = y
307
+ end
308
+
309
+ # Apply RLE compression to flags
310
+ flags_data = compress_flags(flags)
311
+
312
+ # Encode coordinates
313
+ x_coords_data = encode_coordinate_values(flags, x_deltas, :x)
314
+ y_coords_data = encode_coordinate_values(flags, y_deltas, :y)
315
+
316
+ [flags_data, x_coords_data, y_coords_data]
317
+ end
318
+
319
+ private_class_method def self.compress_flags(flags)
320
+ data = (+"").force_encoding(Encoding::BINARY)
321
+ i = 0
322
+
323
+ while i < flags.length
324
+ flag = flags[i]
325
+ count = 1
326
+
327
+ # Count consecutive identical flags
328
+ while i + count < flags.length && flags[i + count] == flag && count < 256
329
+ count += 1
330
+ end
331
+
332
+ if count > 1
333
+ # Use repeat flag
334
+ data << [flag | REPEAT_FLAG].pack("C")
335
+ data << [count - 1].pack("C") # Repeat count (0 means repeat once more)
336
+ i += count
337
+ else
338
+ # Single flag
339
+ data << [flag].pack("C")
340
+ i += 1
341
+ end
342
+ end
343
+
344
+ data
345
+ end
346
+
347
+ private_class_method def self.encode_coordinate_values(flags, deltas, axis)
348
+ data = (+"").force_encoding(Encoding::BINARY)
349
+ short_flag = axis == :x ? X_SHORT_VECTOR : Y_SHORT_VECTOR
350
+ same_flag = axis == :x ? X_IS_SAME_OR_POSITIVE_X_SHORT : Y_IS_SAME_OR_POSITIVE_Y_SHORT
351
+
352
+ delta_index = 0
353
+
354
+ flags.each do |flag|
355
+ if (flag & short_flag) != 0
356
+ # 1-byte coordinate (already absolute value in deltas)
357
+ data << [deltas[delta_index]].pack("C")
358
+ delta_index += 1
359
+ elsif (flag & same_flag) != 0
360
+ # Same as previous (delta = 0), no data
361
+ else
362
+ # 2-byte signed coordinate
363
+ delta = deltas[delta_index]
364
+ # Pack as signed 16-bit big-endian
365
+ data << [delta].pack("n") # Will need to convert to signed
366
+ delta_index += 1
367
+ end
368
+ end
369
+
370
+ data
371
+ end
372
+
373
+ private_class_method def self.calculate_bounding_box(contours)
374
+ x_min = Float::INFINITY
375
+ y_min = Float::INFINITY
376
+ x_max = -Float::INFINITY
377
+ y_max = -Float::INFINITY
378
+
379
+ contours.each do |contour|
380
+ contour.each do |point|
381
+ x = point[:x]
382
+ y = point[:y]
383
+
384
+ x_min = x if x < x_min
385
+ y_min = y if y < y_min
386
+ x_max = x if x > x_max
387
+ y_max = y if y > y_max
388
+ end
389
+ end
390
+
391
+ {
392
+ x_min: x_min.round,
393
+ y_min: y_min.round,
394
+ x_max: x_max.round,
395
+ y_max: y_max.round,
396
+ }
397
+ end
398
+
399
+ private_class_method def self.float_to_f2dot14(value)
400
+ # Convert float to F2DOT14 fixed-point format
401
+ # F2DOT14: 2 bits integer, 14 bits fractional
402
+ # Range: -2.0 to ~1.99993896484375
403
+ raise ArgumentError, "value out of F2DOT14 range" if value < -2.0 || value > 2.0
404
+
405
+ fixed = (value * 16_384.0).round
406
+ # Convert to unsigned 16-bit
407
+ fixed.negative? ? fixed + 65_536 : fixed
408
+ end
409
+
410
+ private_class_method def self.validate_bbox!(bbox)
411
+ raise ArgumentError, "bbox cannot be nil" if bbox.nil?
412
+ raise ArgumentError, "bbox must be Hash" unless bbox.is_a?(Hash)
413
+
414
+ required = %i[x_min y_min x_max y_max]
415
+ missing = required - bbox.keys
416
+ unless missing.empty?
417
+ raise ArgumentError, "bbox missing keys: #{missing.join(', ')}"
418
+ end
419
+
420
+ required.each do |key|
421
+ value = bbox[key]
422
+ unless value.is_a?(Numeric)
423
+ raise ArgumentError, "bbox[:#{key}] must be Numeric"
424
+ end
425
+ end
426
+
427
+ if bbox[:x_min] > bbox[:x_max]
428
+ raise ArgumentError, "bbox x_min must be <= x_max"
429
+ end
430
+
431
+ if bbox[:y_min] > bbox[:y_max]
432
+ raise ArgumentError, "bbox y_min must be <= y_max"
433
+ end
434
+ end
435
+
436
+ private_class_method def self.validate_component!(component)
437
+ raise ArgumentError, "component must be Hash" unless component.is_a?(Hash)
438
+ unless component[:glyph_index]
439
+ raise ArgumentError, "component must have :glyph_index"
440
+ end
441
+ unless component[:glyph_index].is_a?(Integer)
442
+ raise ArgumentError, "component :glyph_index must be Integer"
443
+ end
444
+ if component[:glyph_index].negative?
445
+ raise ArgumentError, "component :glyph_index must be non-negative"
446
+ end
447
+ end
448
+ end
449
+ end
450
+ end