fontisan 0.4.43 → 0.4.45

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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.bug-fixes/12-type1-generators-duck-typing.md +80 -0
  3. data/TODO.bug-fixes/13-hint-extractor-duck-typing.md +30 -0
  4. data/TODO.bug-fixes/14-font-interface-type-checks.md +82 -0
  5. data/TODO.bug-fixes/15-glyph-type-checks-and-simple-glyph-points-bug.md +49 -0
  6. data/TODO.bug-fixes/16-variation-table-checks.md +31 -0
  7. data/TODO.bug-fixes/17-ufo-and-stitcher-cleanup.md +32 -0
  8. data/TODO.bug-fixes/18-generic-value-and-io-checks.md +56 -0
  9. data/TODO.bug-fixes/19-serialization-migration.md +52 -0
  10. data/TODO.bug-fixes/20-variation-validator-type-checks.md +29 -0
  11. data/TODO.bug-fixes/21-xml-builder-send.md +24 -0
  12. data/TODO.bug-fixes/22-type1-converter-font-info.md +25 -0
  13. data/TODO.bug-fixes/23-boundary-type-checks.md +31 -0
  14. data/TODO.bug-fixes/24-validator-dynamic-field-key.md +25 -0
  15. data/TODO.bug-fixes/25-serialization-migration.md +49 -0
  16. data/TODO.bug-fixes/26-variation-preserver-spec-migration.md +25 -0
  17. data/TODO.bug-fixes/27-table-class-registry.md +50 -0
  18. data/TODO.bug-fixes/28-spec-doubles-cleanup.md +40 -0
  19. data/TODO.bug-fixes/29-strategy-registries.md +30 -0
  20. data/TODO.bug-fixes/30-eliminate-send-in-specs.md +36 -0
  21. data/TODO.bug-fixes/README.md +27 -0
  22. data/lib/fontisan/binary/base_record.rb +13 -12
  23. data/lib/fontisan/collection/builder.rb +3 -5
  24. data/lib/fontisan/collection/dfont_builder.rb +4 -11
  25. data/lib/fontisan/collection/writer.rb +1 -1
  26. data/lib/fontisan/commands/base_command.rb +1 -1
  27. data/lib/fontisan/commands/info_command.rb +1 -1
  28. data/lib/fontisan/commands/pack_command.rb +4 -4
  29. data/lib/fontisan/commands/unpack_command.rb +1 -1
  30. data/lib/fontisan/commands/validate_command.rb +1 -1
  31. data/lib/fontisan/conversion_options.rb +1 -1
  32. data/lib/fontisan/converters/format_converter.rb +3 -3
  33. data/lib/fontisan/converters/glyf_table_builder.rb +1 -1
  34. data/lib/fontisan/converters/outline_converter.rb +2 -6
  35. data/lib/fontisan/converters/table_copier.rb +4 -12
  36. data/lib/fontisan/converters/type1_converter.rb +13 -38
  37. data/lib/fontisan/converters/woff2_encoder.rb +8 -10
  38. data/lib/fontisan/converters/woff_writer.rb +3 -3
  39. data/lib/fontisan/export/exporter.rb +8 -9
  40. data/lib/fontisan/export/table_serializer.rb +71 -186
  41. data/lib/fontisan/export/transformers/font_to_ttx.rb +18 -18
  42. data/lib/fontisan/export/transformers/head_transformer.rb +1 -1
  43. data/lib/fontisan/export/transformers/hhea_transformer.rb +1 -1
  44. data/lib/fontisan/export/transformers/maxp_transformer.rb +1 -1
  45. data/lib/fontisan/export/transformers/name_transformer.rb +1 -1
  46. data/lib/fontisan/export/transformers/os2_transformer.rb +1 -1
  47. data/lib/fontisan/export/transformers/post_transformer.rb +1 -1
  48. data/lib/fontisan/export/ttx_generator.rb +36 -37
  49. data/lib/fontisan/font_writer.rb +1 -1
  50. data/lib/fontisan/glyph_accessor.rb +7 -9
  51. data/lib/fontisan/hints/hint_converter.rb +1 -1
  52. data/lib/fontisan/hints/postscript_hint_extractor.rb +24 -64
  53. data/lib/fontisan/hints/truetype_hint_extractor.rb +1 -1
  54. data/lib/fontisan/models/ttx/tables/binary_table.rb +1 -1
  55. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  56. data/lib/fontisan/optimizers/pattern_analyzer.rb +1 -1
  57. data/lib/fontisan/parsers/dfont_parser.rb +2 -2
  58. data/lib/fontisan/pipeline/transformation_pipeline.rb +1 -1
  59. data/lib/fontisan/sfnt_font.rb +2 -0
  60. data/lib/fontisan/sfnt_source.rb +28 -0
  61. data/lib/fontisan/stitcher/source.rb +8 -10
  62. data/lib/fontisan/subset/builder.rb +2 -2
  63. data/lib/fontisan/subset/table_strategy/cff2.rb +2 -3
  64. data/lib/fontisan/svg_to_glyf/assembler.rb +59 -20
  65. data/lib/fontisan/svg_to_glyf/document.rb +28 -9
  66. data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +34 -17
  67. data/lib/fontisan/tables/cff/charstring.rb +3 -0
  68. data/lib/fontisan/tables/cff/dict.rb +6 -3
  69. data/lib/fontisan/tables/cff/index.rb +1 -1
  70. data/lib/fontisan/tables/cff/private_dict_writer.rb +2 -2
  71. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  72. data/lib/fontisan/tables/cff.rb +3 -0
  73. data/lib/fontisan/tables/cff2/table_builder.rb +3 -5
  74. data/lib/fontisan/tables/cff2.rb +3 -0
  75. data/lib/fontisan/tables/cmap.rb +3 -0
  76. data/lib/fontisan/tables/cvar.rb +3 -0
  77. data/lib/fontisan/tables/fvar.rb +3 -0
  78. data/lib/fontisan/tables/glyf/simple_glyph.rb +9 -0
  79. data/lib/fontisan/tables/glyf.rb +12 -8
  80. data/lib/fontisan/tables/glyf_table.rb +1 -1
  81. data/lib/fontisan/tables/gvar.rb +3 -0
  82. data/lib/fontisan/tables/head.rb +3 -0
  83. data/lib/fontisan/tables/hhea.rb +3 -0
  84. data/lib/fontisan/tables/hmtx.rb +3 -7
  85. data/lib/fontisan/tables/hvar.rb +4 -0
  86. data/lib/fontisan/tables/loca.rb +3 -0
  87. data/lib/fontisan/tables/maxp.rb +3 -0
  88. data/lib/fontisan/tables/mvar.rb +3 -0
  89. data/lib/fontisan/tables/name.rb +3 -0
  90. data/lib/fontisan/tables/os2.rb +3 -0
  91. data/lib/fontisan/tables/post.rb +3 -0
  92. data/lib/fontisan/tables/registry.rb +71 -0
  93. data/lib/fontisan/tables/vvar.rb +3 -0
  94. data/lib/fontisan/tables.rb +2 -0
  95. data/lib/fontisan/type1/afm_generator.rb +12 -65
  96. data/lib/fontisan/type1/charstring_converter.rb +3 -3
  97. data/lib/fontisan/type1/conversion_options.rb +3 -2
  98. data/lib/fontisan/type1/decryptor.rb +1 -1
  99. data/lib/fontisan/type1/generator.rb +14 -9
  100. data/lib/fontisan/type1/inf_generator.rb +10 -35
  101. data/lib/fontisan/type1/pfa_generator.rb +36 -26
  102. data/lib/fontisan/type1/pfb_generator.rb +24 -12
  103. data/lib/fontisan/type1/pfm_generator.rb +14 -31
  104. data/lib/fontisan/type1/ttf_to_type1_converter.rb +30 -24
  105. data/lib/fontisan/ufo/bounds.rb +91 -0
  106. data/lib/fontisan/ufo/compile/fvar.rb +2 -2
  107. data/lib/fontisan/ufo/contour.rb +5 -0
  108. data/lib/fontisan/ufo/convert/from_bin_data.rb +15 -29
  109. data/lib/fontisan/ufo/info.rb +4 -0
  110. data/lib/fontisan/ufo/point.rb +0 -7
  111. data/lib/fontisan/ufo.rb +1 -0
  112. data/lib/fontisan/utilities/brotli_wrapper.rb +3 -5
  113. data/lib/fontisan/utilities/padding.rb +9 -8
  114. data/lib/fontisan/validation/collection_validator.rb +2 -2
  115. data/lib/fontisan/validators/validator.rb +4 -4
  116. data/lib/fontisan/variable/delta_applicator.rb +4 -11
  117. data/lib/fontisan/variable/metric_delta_processor.rb +3 -3
  118. data/lib/fontisan/variable/static_font_builder.rb +19 -23
  119. data/lib/fontisan/variation/converter.rb +1 -1
  120. data/lib/fontisan/variation/instance_font_wrapper.rb +41 -0
  121. data/lib/fontisan/variation/instance_writer.rb +19 -17
  122. data/lib/fontisan/variation/metrics_adjuster.rb +16 -21
  123. data/lib/fontisan/variation/validator.rb +26 -38
  124. data/lib/fontisan/variation/variable_svg_generator.rb +0 -48
  125. data/lib/fontisan/variation/variation_preserver.rb +2 -3
  126. data/lib/fontisan/variation.rb +1 -0
  127. data/lib/fontisan/version.rb +1 -1
  128. data/lib/fontisan/woff2/sfnt_checksum.rb +1 -1
  129. data/lib/fontisan/woff2/table_transformer.rb +22 -29
  130. data/lib/fontisan/woff2_font.rb +11 -10
  131. data/lib/fontisan/woff_font.rb +2 -0
  132. data/lib/fontisan.rb +1 -0
  133. metadata +25 -2
@@ -181,15 +181,10 @@ module Fontisan
181
181
  # @return [String] Font name
182
182
  def extract_font_name
183
183
  name_table = @font.table(Constants::NAME_TAG)
184
- return "" unless name_table
184
+ return extract_postscript_name unless name_table
185
185
 
186
- # Try full font name first, then postscript name
187
- if name_table.respond_to?(:full_font_name)
188
- name_table.full_font_name(1) || name_table.full_font_name(3) ||
189
- extract_postscript_name
190
- else
186
+ name_table.english_name(Tables::Name::FULL_NAME) ||
191
187
  extract_postscript_name
192
- end
193
188
  end
194
189
 
195
190
  # Extract PostScript name
@@ -199,12 +194,8 @@ module Fontisan
199
194
  name_table = @font.table(Constants::NAME_TAG)
200
195
  return @font.post_script_name || "Unknown" unless name_table
201
196
 
202
- if name_table.respond_to?(:postscript_name)
203
- name_table.postscript_name(1) || name_table.postscript_name(3) ||
204
- @font.post_script_name || "Unknown"
205
- else
197
+ name_table.english_name(Tables::Name::POSTSCRIPT_NAME) ||
206
198
  @font.post_script_name || "Unknown"
207
- end
208
199
  end
209
200
 
210
201
  # Extract family name
@@ -214,9 +205,7 @@ module Fontisan
214
205
  name_table = @font.table(Constants::NAME_TAG)
215
206
  return nil unless name_table
216
207
 
217
- if name_table.respond_to?(:font_family)
218
- name_table.font_family(1) || name_table.font_family(3)
219
- end
208
+ name_table.english_name(Tables::Name::FAMILY)
220
209
  end
221
210
 
222
211
  # Extract weight
@@ -226,11 +215,7 @@ module Fontisan
226
215
  os2 = @font.table(Constants::OS2_TAG)
227
216
  return nil unless os2
228
217
 
229
- weight_class = if os2.respond_to?(:us_weight_class)
230
- os2.us_weight_class
231
- elsif os2.respond_to?(:weight_class)
232
- os2.weight_class
233
- end
218
+ weight_class = os2.us_weight_class
234
219
  return nil unless weight_class
235
220
 
236
221
  case weight_class
@@ -254,9 +239,7 @@ module Fontisan
254
239
  post = @font.table(Constants::POST_TAG)
255
240
  return nil unless post
256
241
 
257
- if post.respond_to?(:italic_angle)
258
- post.italic_angle
259
- end
242
+ post.italic_angle
260
243
  end
261
244
 
262
245
  # Extract version
@@ -266,9 +249,7 @@ module Fontisan
266
249
  name_table = @font.table(Constants::NAME_TAG)
267
250
  return nil unless name_table
268
251
 
269
- if name_table.respond_to?(:version_string)
270
- name_table.version_string(1) || name_table.version_string(3)
271
- end
252
+ name_table.english_name(Tables::Name::VERSION)
272
253
  end
273
254
 
274
255
  # Extract copyright
@@ -278,9 +259,7 @@ module Fontisan
278
259
  name_table = @font.table(Constants::NAME_TAG)
279
260
  return nil unless name_table
280
261
 
281
- if name_table.respond_to?(:copyright)
282
- name_table.copyright(1) || name_table.copyright(3)
283
- end
262
+ name_table.english_name(Tables::Name::COPYRIGHT)
284
263
  end
285
264
 
286
265
  # Extract vendor/manufacturer
@@ -290,9 +269,7 @@ module Fontisan
290
269
  name_table = @font.table(Constants::NAME_TAG)
291
270
  return nil unless name_table
292
271
 
293
- if name_table.respond_to?(:manufacturer)
294
- name_table.manufacturer(1) || name_table.manufacturer(3)
295
- end
272
+ name_table.english_name(Tables::Name::MANUFACTURER)
296
273
  end
297
274
 
298
275
  # Extract license information
@@ -302,9 +279,7 @@ module Fontisan
302
279
  name_table = @font.table(Constants::NAME_TAG)
303
280
  return nil unless name_table
304
281
 
305
- if name_table.respond_to?(:license)
306
- name_table.license(1) || name_table.license(3)
307
- end
282
+ name_table.english_name(Tables::Name::LICENSE_DESCRIPTION)
308
283
  end
309
284
 
310
285
  # Get default PFB filename
@@ -134,15 +134,11 @@ module Fontisan
134
134
 
135
135
  # Font info
136
136
  if name_table
137
- if name_table.respond_to?(:version_string)
138
- version = name_table.version_string(1) || name_table.version_string(3)
139
- dict << "/Version (#{version}) def" if version
140
- end
137
+ version = name_table.english_name(Tables::Name::VERSION)
138
+ dict << "/Version (#{version}) def" if version
141
139
 
142
- if name_table.respond_to?(:copyright)
143
- copyright = name_table.copyright(1) || name_table.copyright(3)
144
- dict << "/Notice (#{copyright}) def" if copyright
145
- end
140
+ copyright = name_table.english_name(Tables::Name::COPYRIGHT)
141
+ dict << "/Notice (#{copyright}) def" if copyright
146
142
  end
147
143
 
148
144
  dict << "currentdict end"
@@ -161,12 +157,12 @@ module Fontisan
161
157
  # Blue values (for hinting)
162
158
  # These are typically derived from the font's alignment zones
163
159
  os2 = @font.table(Constants::OS2_TAG)
164
- if os2.respond_to?(:typo_ascender) && os2.typo_ascender
160
+ if os2&.s_typo_ascender
165
161
  blue_values = [
166
- @scaler.scale(os2.typo_descender || -200),
167
- @scaler.scale(os2.typo_descender || -200) + 20,
168
- @scaler.scale(os2.typo_ascender),
169
- @scaler.scale(os2.typo_ascender) + 10,
162
+ @scaler.scale(os2.s_typo_descender || -200),
163
+ @scaler.scale(os2.s_typo_descender || -200) + 20,
164
+ @scaler.scale(os2.s_typo_ascender),
165
+ @scaler.scale(os2.s_typo_ascender) + 10,
170
166
  ]
171
167
  dict << "/BlueValues {#{blue_values.join(' ')}} def"
172
168
  else
@@ -178,15 +174,15 @@ module Fontisan
178
174
  dict << "/BlueFuzz 1 def"
179
175
 
180
176
  # Stem snap hints
181
- if os2.respond_to?(:weight_class) && os2.weight_class
177
+ if os2&.us_weight_class
182
178
  stem_width = @scaler.scale([100, 80,
183
- 90][os2.weight_class / 100] || 80)
179
+ 90][os2.us_weight_class / 100] || 80)
184
180
  dict << "/StemSnapH [#{stem_width}] def"
185
181
  dict << "/StemSnapV [#{stem_width}] def"
186
182
  end
187
183
 
188
184
  # Force bold flag
189
- dict << if os2.respond_to?(:weight_class) && os2.weight_class && os2.weight_class >= 700
185
+ dict << if os2&.us_weight_class && os2.us_weight_class >= 700
190
186
  "/ForceBold true def"
191
187
  else
192
188
  "/ForceBold false def"
@@ -286,33 +282,33 @@ module Fontisan
286
282
 
287
283
  # Generate a simple CharString for a glyph
288
284
  #
289
- # @param glyf_table [Object] TTF glyf table
285
+ # @param glyf_table [Tables::Glyf] TTF glyf table
290
286
  # @param gid [Integer] Glyph ID
291
287
  # @return [String] Type 1 CharString data
292
288
  def simple_charstring(glyf_table, gid)
293
289
  glyph = glyf_table.glyph(gid)
294
290
 
295
291
  # Empty or compound glyph
296
- if glyph.nil? || glyph.contour_count.zero? || glyph.compound?
292
+ if glyph.nil? || glyph.num_contours.zero? || glyph.compound?
297
293
  # Return empty charstring (hsbw + endchar)
298
294
  return [0, 500, 14].pack("C*")
299
295
  end
300
296
 
301
297
  # For simple glyphs without curve conversion, generate minimal charstring
302
- lsb = @scaler.scale(glyph.left_side_bearing || 0)
303
- width = @scaler.scale(glyph.advance_width || 500)
304
- bytes = [13, lsb, width] # hsbw command (13)
298
+ lsb, width = hmtx_metrics_for(gid)
299
+ bytes = [13, @scaler.scale(lsb), @scaler.scale(width)] # hsbw command (13)
305
300
 
306
301
  # Add simple line commands (very basic)
307
- if glyph.respond_to?(:points) && glyph.points && !glyph.points.empty?
302
+ points = glyph.points
303
+ unless points.empty?
308
304
  # Just draw lines between consecutive on-curve points
309
305
  prev_point = nil
310
- glyph.points.each do |point|
311
- next unless point.on_curve?
306
+ points.each do |point|
307
+ next unless point[:on_curve]
312
308
 
313
309
  if prev_point
314
- dx = @scaler.scale(point.x) - @scaler.scale(prev_point.x)
315
- dy = @scaler.scale(point.y) - @scaler.scale(prev_point.y)
310
+ dx = @scaler.scale(point[:x]) - @scaler.scale(prev_point[:x])
311
+ dy = @scaler.scale(point[:y]) - @scaler.scale(prev_point[:y])
316
312
  bytes << 5 # rlineto
317
313
  bytes.concat(encode_number(dx))
318
314
  bytes.concat(encode_number(dy))
@@ -325,6 +321,20 @@ module Fontisan
325
321
  bytes.pack("C*")
326
322
  end
327
323
 
324
+ # Look up LSB and advance width from hmtx for a glyph.
325
+ #
326
+ # @param gid [Integer] Glyph ID
327
+ # @return [Array(Integer, Integer)] [lsb, advance_width]
328
+ def hmtx_metrics_for(gid)
329
+ hmtx = @font.table(Constants::HMTX_TAG)
330
+ return [0, 500] unless hmtx
331
+
332
+ metric = hmtx.metric_for(gid)
333
+ return [0, 500] unless metric
334
+
335
+ [metric[:lsb] || 0, metric[:advance_width] || 500]
336
+ end
337
+
328
338
  # Encode a number for Type 1 CharString
329
339
  #
330
340
  # @param value [Integer] Number to encode
@@ -159,15 +159,15 @@ module Fontisan
159
159
 
160
160
  # Stem snap hints
161
161
  os2 = @font.table(Constants::OS2_TAG)
162
- if os2.respond_to?(:weight_class)
162
+ if os2&.us_weight_class
163
163
  stem_width = @scaler.scale([100, 80,
164
- 90][os2.weight_class / 100] || 80)
164
+ 90][os2.us_weight_class / 100] || 80)
165
165
  dict << "/StemSnapH [#{stem_width}] def"
166
166
  dict << "/StemSnapV [#{stem_width}] def"
167
167
  end
168
168
 
169
169
  # Force bold flag
170
- dict << if os2.respond_to?(:weight_class) && os2.weight_class && os2.weight_class >= 700
170
+ dict << if os2&.us_weight_class && os2.us_weight_class >= 700
171
171
  "/ForceBold true def"
172
172
  else
173
173
  "/ForceBold false def"
@@ -261,30 +261,42 @@ module Fontisan
261
261
  glyph = glyf_table.glyph(gid)
262
262
 
263
263
  # Empty or compound glyph
264
- if glyph.nil? || glyph.contour_count.zero? || glyph.compound?
264
+ if glyph.nil? || glyph.num_contours.zero? || glyph.compound?
265
265
  # Return empty charstring (hsbw + endchar)
266
266
  return [0, 500, 14].pack("C*")
267
267
  end
268
268
 
269
269
  # For simple glyphs without curve conversion, generate line-based charstring
270
270
  # This is a simplified implementation
271
- lsb = @scaler.scale(glyph.left_side_bearing || 0)
272
- width = @scaler.scale(glyph.advance_width || 500)
273
- bytes = [0, lsb, width] # hsbw
271
+ lsb, width = hmtx_metrics_for(gid)
272
+ bytes = [0, @scaler.scale(lsb), @scaler.scale(width)] # hsbw
274
273
 
275
274
  # Add lines between points (simplified)
276
- if glyph.respond_to?(:points) && glyph.points && !glyph.points.empty?
277
- glyph.points.each do |point|
278
- next unless point.on_curve?
275
+ points = glyph.points
276
+ points.each do |point|
277
+ next unless point[:on_curve]
279
278
 
280
- # This is very simplified - proper implementation would handle curves
281
- end
279
+ # This is very simplified - proper implementation would handle curves
282
280
  end
283
281
 
284
282
  bytes << 14 # endchar
285
283
  bytes.pack("C*")
286
284
  end
287
285
 
286
+ # Look up LSB and advance width from hmtx for a glyph.
287
+ #
288
+ # @param gid [Integer] Glyph ID
289
+ # @return [Array(Integer, Integer)] [lsb, advance_width]
290
+ def hmtx_metrics_for(gid)
291
+ hmtx = @font.table(Constants::HMTX_TAG)
292
+ return [0, 500] unless hmtx
293
+
294
+ metric = hmtx.metric_for(gid)
295
+ return [0, 500] unless metric
296
+
297
+ [metric[:lsb] || 0, metric[:advance_width] || 500]
298
+ end
299
+
288
300
  # Build second ASCII segment (trailer)
289
301
  #
290
302
  # @return [String] ASCII trailer
@@ -145,7 +145,7 @@ module Fontisan
145
145
  return widths unless cmap
146
146
 
147
147
  # Get Unicode mappings
148
- mappings = if cmap.respond_to?(:unicode_mappings)
148
+ mappings = if cmap
149
149
  cmap.unicode_mappings || {}
150
150
  else
151
151
  {}
@@ -369,18 +369,14 @@ module Fontisan
369
369
  metrics << [0].pack("V")
370
370
 
371
371
  # etmCapHeight (4 bytes)
372
- cap_height = if os2.respond_to?(:cap_height) && os2.cap_height
373
- os2.cap_height
374
- elsif os2.respond_to?(:s_typo_ascender) && os2.s_typo_ascender
375
- os2.s_typo_ascender
376
- else
377
- @metrics.ascent || 1000
378
- end
372
+ cap_height = os2.s_cap_height ||
373
+ os2.s_typo_ascender ||
374
+ @metrics.ascent || 1000
379
375
  metrics << [@scaler.scale(cap_height)].pack("V")
380
376
 
381
377
  # etmXHeight (4 bytes)
382
- x_height = if os2.respond_to?(:x_height) && os2.x_height&.positive?
383
- os2.x_height
378
+ x_height = if os2.sx_height&.positive?
379
+ os2.sx_height
384
380
  else
385
381
  # Fallback: use roughly half the ascent for x-height
386
382
  (@metrics.ascent / 2) || 500
@@ -503,11 +499,7 @@ module Fontisan
503
499
  name_table = @font.table(Constants::NAME_TAG)
504
500
  return "" unless name_table
505
501
 
506
- if name_table.respond_to?(:copyright)
507
- name_table.copyright(1) || name_table.copyright(3) || ""
508
- else
509
- ""
510
- end
502
+ name_table.english_name(Tables::Name::COPYRIGHT) || ""
511
503
  end
512
504
 
513
505
  # Extract face name from font
@@ -515,18 +507,13 @@ module Fontisan
515
507
  # @return [String] Face name
516
508
  def extract_face_name
517
509
  name_table = @font.table(Constants::NAME_TAG)
518
- return "" unless name_table
510
+ return @font.post_script_name.to_s unless name_table
519
511
 
520
512
  # Try full font name first, then font family, then postscript name
521
- face_name = if name_table.respond_to?(:full_font_name)
522
- name_table.full_font_name(1) || name_table.full_font_name(3) || ""
523
- elsif name_table.respond_to?(:font_family)
524
- name_table.font_family(1) || name_table.font_family(3) || ""
525
- elsif name_table.respond_to?(:postscript_name)
526
- name_table.postscript_name(1) || name_table.postscript_name(3) || ""
527
- else
528
- @font.post_script_name || ""
529
- end
513
+ face_name = name_table.english_name(Tables::Name::FULL_NAME) ||
514
+ name_table.english_name(Tables::Name::FAMILY) ||
515
+ name_table.english_name(Tables::Name::POSTSCRIPT_NAME) ||
516
+ @font.post_script_name
530
517
 
531
518
  face_name.to_s
532
519
  end
@@ -538,11 +525,7 @@ module Fontisan
538
525
  os2 = @font.table(Constants::OS2_TAG)
539
526
  return 400 unless os2
540
527
 
541
- weight_class = if os2.respond_to?(:us_weight_class)
542
- os2.us_weight_class
543
- elsif os2.respond_to?(:weight_class)
544
- os2.weight_class
545
- end
528
+ weight_class = os2.us_weight_class
546
529
  return 400 unless weight_class
547
530
 
548
531
  # Map OS/2 weight class to PFM weight
@@ -564,7 +547,7 @@ module Fontisan
564
547
  # @return [Integer] Pitch and family byte
565
548
  def pitch_and_family_value
566
549
  post = @font.table(Constants::POST_TAG)
567
- is_fixed = post.respond_to?(:is_fixed_pitch) ? post.is_fixed_pitch : false
550
+ is_fixed = post&.is_fixed_pitch || false
568
551
 
569
552
  pitch = is_fixed ? FIXED_PITCH : VARIABLE_PITCH
570
553
 
@@ -97,23 +97,23 @@ module Fontisan
97
97
  end
98
98
 
99
99
  # Convert simple glyph
100
- convert_simple_glyph(glyph)
100
+ convert_simple_glyph(glyph, gid)
101
101
  end
102
102
 
103
103
  # Convert a simple glyph to CharString
104
104
  #
105
- # @param glyph [Object] TTF simple glyph
105
+ # @param glyph [Tables::SimpleGlyph] TTF simple glyph
106
+ # @param gid [Integer] Glyph ID (for hmtx lookup)
106
107
  # @return [String] Type 1 CharString data
107
- def convert_simple_glyph(glyph)
108
+ def convert_simple_glyph(glyph, gid)
108
109
  commands = []
109
110
  points = extract_points(glyph)
110
111
 
111
112
  return empty_charstring if points.empty?
112
113
 
113
114
  # Start with hsbw (horizontal side bearing and width)
114
- lsb = @scaler.scale(glyph.left_side_bearing || 0)
115
- width = @scaler.scale(glyph.advance_width || 500)
116
- commands << [HSBW, lsb, width]
115
+ lsb, width = hmtx_metrics_for(gid)
116
+ commands << [HSBW, @scaler.scale(lsb), @scaler.scale(width)]
117
117
 
118
118
  # Convert contours to Type 1 commands
119
119
  contour_commands = convert_contours(points)
@@ -126,23 +126,34 @@ module Fontisan
126
126
  encode_charstring(commands)
127
127
  end
128
128
 
129
- # Extract points from a simple glyph
129
+ # Extract points from a simple glyph, scaled to target UPM.
130
130
  #
131
- # @param glyph [Object] TTF simple glyph
132
- # @return [Array<Hash>] Array of points with on_curve flag
131
+ # @param glyph [Tables::SimpleGlyph] TTF simple glyph
132
+ # @return [Array<Hash>] Array of {x:, y:, on_curve:} hashes
133
133
  def extract_points(glyph)
134
- return [] unless glyph.respond_to?(:points)
135
-
136
- points = []
137
- glyph.points.each do |point|
138
- points << {
139
- x: @scaler.scale(point.x),
140
- y: @scaler.scale(point.y),
141
- on_curve: point.on_curve?,
134
+ return [] unless glyph.is_a?(Tables::SimpleGlyph)
135
+
136
+ glyph.points.map do |point|
137
+ {
138
+ x: @scaler.scale(point[:x]),
139
+ y: @scaler.scale(point[:y]),
140
+ on_curve: point[:on_curve],
142
141
  }
143
142
  end
143
+ end
144
+
145
+ # Look up LSB and advance width from hmtx for a glyph.
146
+ #
147
+ # @param gid [Integer] Glyph ID
148
+ # @return [Array(Integer, Integer)] [lsb, advance_width]
149
+ def hmtx_metrics_for(gid)
150
+ hmtx = @font.table(Constants::HMTX_TAG)
151
+ return [0, 500] unless hmtx
152
+
153
+ metric = hmtx.metric_for(gid)
154
+ return [0, 500] unless metric
144
155
 
145
- points
156
+ [metric[:lsb] || 0, metric[:advance_width] || 500]
146
157
  end
147
158
 
148
159
  # Convert contours to Type 1 commands
@@ -298,12 +309,7 @@ module Fontisan
298
309
  dx = tx - transform_x(prev_x, prev_y, matrix)
299
310
  dy = ty - transform_y(prev_x, prev_y, matrix)
300
311
 
301
- on_curve = pt[:on_curve].nil? || pt[:on_curve]
302
- if on_curve
303
- commands << [RLINETO, dx.to_i, dy.to_i]
304
- else
305
- commands << [RLINETO, dx.to_i, dy.to_i]
306
- end
312
+ commands << [RLINETO, dx.to_i, dy.to_i]
307
313
  prev_x = pt[:x].to_f
308
314
  prev_y = pt[:y].to_f
309
315
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Ufo
5
+ # Immutable axis-aligned bounding box over outline points.
6
+ #
7
+ # Bounds is a value object: equality and hash are based on the four
8
+ # coordinates, not identity. Bounds composes via #union so callers
9
+ # can fold bounds across many contours or many glyphs without
10
+ # mutating either side.
11
+ class Bounds
12
+ include Comparable
13
+
14
+ attr_reader :min_x, :min_y, :max_x, :max_y
15
+
16
+ def initialize(min_x:, min_y:, max_x:, max_y:)
17
+ @min_x = min_x.to_f
18
+ @min_y = min_y.to_f
19
+ @max_x = max_x.to_f
20
+ @max_y = max_y.to_f
21
+ end
22
+
23
+ # @param contours [Array<Contour>, Enumerable<Contour>]
24
+ # @return [Bounds]
25
+ def self.measure(contours)
26
+ min_x = min_y = +Float::INFINITY
27
+ max_x = max_y = -Float::INFINITY
28
+ saw_point = false
29
+
30
+ contours.each do |contour|
31
+ contour.points.each do |pt|
32
+ saw_point = true
33
+ min_x = pt.x if pt.x < min_x
34
+ min_y = pt.y if pt.y < min_y
35
+ max_x = pt.x if pt.x > max_x
36
+ max_y = pt.y if pt.y > max_y
37
+ end
38
+ end
39
+
40
+ return empty unless saw_point
41
+
42
+ new(min_x: min_x, min_y: min_y, max_x: max_x, max_y: max_y)
43
+ end
44
+
45
+ # @return [Bounds] zero-extent bounds anchored at the origin
46
+ def self.empty
47
+ new(min_x: 0, min_y: 0, max_x: 0, max_y: 0)
48
+ end
49
+
50
+ def width
51
+ max_x - min_x
52
+ end
53
+
54
+ def height
55
+ max_y - min_y
56
+ end
57
+
58
+ # A bounds is empty when it has zero extent in both axes. Note that
59
+ # a contour containing a single point yields zero-extent bounds but
60
+ # is not "empty" in the geometric sense — callers that need to
61
+ # distinguish should test the contour directly.
62
+ def empty?
63
+ width.zero? && height.zero?
64
+ end
65
+
66
+ # Smallest bounds containing both self and other.
67
+ #
68
+ # @param other [Bounds]
69
+ # @return [Bounds]
70
+ def union(other)
71
+ self.class.new(
72
+ min_x: [@min_x, other.min_x].min,
73
+ min_y: [@min_y, other.min_y].min,
74
+ max_x: [@max_x, other.max_x].max,
75
+ max_y: [@max_y, other.max_y].max,
76
+ )
77
+ end
78
+
79
+ def <=>(other)
80
+ return nil unless other.is_a?(Bounds)
81
+
82
+ [@min_x, @min_y, @max_x, @max_y] <=>
83
+ [other.min_x, other.min_y, other.max_x, other.max_y]
84
+ end
85
+
86
+ def hash
87
+ [@min_x, @min_y, @max_x, @max_y].hash
88
+ end
89
+ end
90
+ end
91
+ end
@@ -20,8 +20,8 @@ module Fontisan
20
20
  # @param instances [Array<Hash>] named instance definitions
21
21
  # @return [String, nil] fvar table bytes, or nil if no axes
22
22
  def self.build(font, axes: nil, instances: nil)
23
- axes ||= font.info.respond_to?(:axes) ? font.info.axes : nil
24
- instances ||= font.info.respond_to?(:named_instances) ? font.info.named_instances : nil
23
+ axes ||= font.info.axes
24
+ instances ||= font.info.named_instances
25
25
 
26
26
  axes ||= []
27
27
  return nil if axes.empty?
@@ -24,6 +24,11 @@ module Fontisan
24
24
  def point_count
25
25
  @points.size
26
26
  end
27
+
28
+ # @return [Bounds] extents of this contour's points
29
+ def bounds
30
+ Bounds.measure([self])
31
+ end
27
32
  end
28
33
  end
29
34
  end