fontisan 0.4.43 → 0.4.44

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 (124) 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/README.md +24 -0
  21. data/lib/fontisan/binary/base_record.rb +13 -12
  22. data/lib/fontisan/collection/builder.rb +3 -5
  23. data/lib/fontisan/collection/dfont_builder.rb +4 -11
  24. data/lib/fontisan/collection/writer.rb +1 -1
  25. data/lib/fontisan/commands/pack_command.rb +4 -4
  26. data/lib/fontisan/commands/unpack_command.rb +1 -1
  27. data/lib/fontisan/commands/validate_command.rb +1 -1
  28. data/lib/fontisan/converters/format_converter.rb +2 -2
  29. data/lib/fontisan/converters/glyf_table_builder.rb +1 -1
  30. data/lib/fontisan/converters/outline_converter.rb +2 -6
  31. data/lib/fontisan/converters/table_copier.rb +4 -12
  32. data/lib/fontisan/converters/type1_converter.rb +12 -37
  33. data/lib/fontisan/converters/woff2_encoder.rb +8 -10
  34. data/lib/fontisan/converters/woff_writer.rb +3 -3
  35. data/lib/fontisan/export/exporter.rb +8 -9
  36. data/lib/fontisan/export/table_serializer.rb +71 -186
  37. data/lib/fontisan/export/transformers/font_to_ttx.rb +18 -18
  38. data/lib/fontisan/export/transformers/head_transformer.rb +1 -1
  39. data/lib/fontisan/export/transformers/hhea_transformer.rb +1 -1
  40. data/lib/fontisan/export/transformers/maxp_transformer.rb +1 -1
  41. data/lib/fontisan/export/transformers/name_transformer.rb +1 -1
  42. data/lib/fontisan/export/transformers/os2_transformer.rb +1 -1
  43. data/lib/fontisan/export/transformers/post_transformer.rb +1 -1
  44. data/lib/fontisan/export/ttx_generator.rb +36 -37
  45. data/lib/fontisan/font_writer.rb +1 -1
  46. data/lib/fontisan/glyph_accessor.rb +7 -9
  47. data/lib/fontisan/hints/postscript_hint_extractor.rb +24 -64
  48. data/lib/fontisan/hints/truetype_hint_extractor.rb +1 -1
  49. data/lib/fontisan/models/ttx/tables/binary_table.rb +1 -1
  50. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  51. data/lib/fontisan/sfnt_font.rb +2 -0
  52. data/lib/fontisan/sfnt_source.rb +28 -0
  53. data/lib/fontisan/stitcher/source.rb +8 -10
  54. data/lib/fontisan/subset/builder.rb +2 -2
  55. data/lib/fontisan/subset/table_strategy/cff2.rb +2 -3
  56. data/lib/fontisan/svg_to_glyf/assembler.rb +59 -20
  57. data/lib/fontisan/svg_to_glyf/document.rb +28 -9
  58. data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +34 -17
  59. data/lib/fontisan/tables/cff/charstring.rb +3 -0
  60. data/lib/fontisan/tables/cff/dict.rb +6 -3
  61. data/lib/fontisan/tables/cff/index.rb +1 -1
  62. data/lib/fontisan/tables/cff/private_dict_writer.rb +2 -2
  63. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  64. data/lib/fontisan/tables/cff.rb +3 -0
  65. data/lib/fontisan/tables/cff2/table_builder.rb +2 -4
  66. data/lib/fontisan/tables/cff2.rb +3 -0
  67. data/lib/fontisan/tables/cmap.rb +3 -0
  68. data/lib/fontisan/tables/cvar.rb +3 -0
  69. data/lib/fontisan/tables/fvar.rb +3 -0
  70. data/lib/fontisan/tables/glyf/simple_glyph.rb +9 -0
  71. data/lib/fontisan/tables/glyf.rb +12 -8
  72. data/lib/fontisan/tables/glyf_table.rb +1 -1
  73. data/lib/fontisan/tables/gvar.rb +3 -0
  74. data/lib/fontisan/tables/head.rb +3 -0
  75. data/lib/fontisan/tables/hhea.rb +3 -0
  76. data/lib/fontisan/tables/hmtx.rb +3 -7
  77. data/lib/fontisan/tables/hvar.rb +4 -0
  78. data/lib/fontisan/tables/loca.rb +3 -0
  79. data/lib/fontisan/tables/maxp.rb +3 -0
  80. data/lib/fontisan/tables/mvar.rb +3 -0
  81. data/lib/fontisan/tables/name.rb +3 -0
  82. data/lib/fontisan/tables/os2.rb +3 -0
  83. data/lib/fontisan/tables/post.rb +3 -0
  84. data/lib/fontisan/tables/registry.rb +71 -0
  85. data/lib/fontisan/tables/vvar.rb +3 -0
  86. data/lib/fontisan/tables.rb +2 -0
  87. data/lib/fontisan/type1/afm_generator.rb +12 -65
  88. data/lib/fontisan/type1/charstring_converter.rb +2 -2
  89. data/lib/fontisan/type1/conversion_options.rb +3 -2
  90. data/lib/fontisan/type1/decryptor.rb +1 -1
  91. data/lib/fontisan/type1/generator.rb +14 -9
  92. data/lib/fontisan/type1/inf_generator.rb +10 -35
  93. data/lib/fontisan/type1/pfa_generator.rb +36 -26
  94. data/lib/fontisan/type1/pfb_generator.rb +24 -12
  95. data/lib/fontisan/type1/pfm_generator.rb +14 -31
  96. data/lib/fontisan/type1/ttf_to_type1_converter.rb +30 -24
  97. data/lib/fontisan/ufo/bounds.rb +91 -0
  98. data/lib/fontisan/ufo/compile/fvar.rb +2 -2
  99. data/lib/fontisan/ufo/contour.rb +5 -0
  100. data/lib/fontisan/ufo/convert/from_bin_data.rb +15 -29
  101. data/lib/fontisan/ufo/info.rb +4 -0
  102. data/lib/fontisan/ufo/point.rb +0 -7
  103. data/lib/fontisan/ufo.rb +1 -0
  104. data/lib/fontisan/utilities/brotli_wrapper.rb +3 -5
  105. data/lib/fontisan/utilities/padding.rb +9 -8
  106. data/lib/fontisan/validation/collection_validator.rb +2 -2
  107. data/lib/fontisan/validators/validator.rb +4 -4
  108. data/lib/fontisan/variable/delta_applicator.rb +4 -11
  109. data/lib/fontisan/variable/metric_delta_processor.rb +3 -3
  110. data/lib/fontisan/variable/static_font_builder.rb +19 -23
  111. data/lib/fontisan/variation/instance_font_wrapper.rb +41 -0
  112. data/lib/fontisan/variation/instance_writer.rb +19 -17
  113. data/lib/fontisan/variation/metrics_adjuster.rb +17 -20
  114. data/lib/fontisan/variation/validator.rb +26 -38
  115. data/lib/fontisan/variation/variable_svg_generator.rb +0 -48
  116. data/lib/fontisan/variation/variation_preserver.rb +2 -3
  117. data/lib/fontisan/variation.rb +1 -0
  118. data/lib/fontisan/version.rb +1 -1
  119. data/lib/fontisan/woff2/sfnt_checksum.rb +1 -1
  120. data/lib/fontisan/woff2/table_transformer.rb +22 -29
  121. data/lib/fontisan/woff2_font.rb +9 -8
  122. data/lib/fontisan/woff_font.rb +2 -0
  123. data/lib/fontisan.rb +1 -0
  124. metadata +24 -2
@@ -71,6 +71,9 @@ module Fontisan
71
71
  # puts "#{axis.axis_tag}: #{axis.min_value} - #{axis.max_value}"
72
72
  # end
73
73
  class Fvar < Binary::BaseRecord
74
+ extend Registered
75
+
76
+ register_tag "fvar"
74
77
  uint16 :major_version
75
78
  uint16 :minor_version
76
79
  uint16 :axes_array_offset
@@ -155,6 +155,15 @@ module Fontisan
155
155
  end
156
156
  end
157
157
 
158
+ # All points across every contour, in contour order.
159
+ #
160
+ # @return [Array<Hash>] Array of {x:, y:, on_curve:} hashes
161
+ def points
162
+ return [] if empty?
163
+
164
+ num_contours.times.flat_map { |c| points_for_contour(c) }
165
+ end
166
+
158
167
  private
159
168
 
160
169
  # Parse glyph header (10 bytes)
@@ -38,6 +38,9 @@ module Fontisan
38
38
  # puts glyph.simple? ? "Simple glyph" : "Compound glyph"
39
39
  # puts glyph.bounding_box # => [xMin, yMin, xMax, yMax]
40
40
  class Glyf < Binary::BaseRecord
41
+ extend Registered
42
+
43
+ register_tag "glyf"
41
44
  # Store the raw data for deferred parsing
42
45
  attr_accessor :raw_data
43
46
 
@@ -215,13 +218,13 @@ module Fontisan
215
218
  next true if glyph.nil? # Empty glyphs are OK
216
219
 
217
220
  # Simple glyphs have instructions
218
- unless glyph.nil?
221
+ if glyph.nil?
222
+ # Compound glyphs may have instructions too
223
+ true
224
+ else
219
225
  inst_len = glyph.instruction_length
220
226
  # If instructions present, length should be reasonable
221
227
  inst_len.nil? || inst_len >= 0
222
- else
223
- # Compound glyphs may have instructions too
224
- true
225
228
  end
226
229
  end
227
230
  rescue StandardError
@@ -240,7 +243,8 @@ module Fontisan
240
243
 
241
244
  # Simple glyphs: contours should be >= 0
242
245
  # Compound glyphs: numberOfContours = -1
243
- if glyph.respond_to?(:num_contours)
246
+ case glyph
247
+ when SimpleGlyph, CompoundGlyph
244
248
  glyph.num_contours >= -1
245
249
  else
246
250
  true
@@ -277,9 +281,9 @@ module Fontisan
277
281
  # @param glyph_id [Integer] Glyph ID
278
282
  # @raise [ArgumentError] If validation fails
279
283
  def validate_context!(loca, head, glyph_id)
280
- unless loca.respond_to?(:offset_for) && loca.respond_to?(:size_of)
284
+ unless loca.is_a?(Loca)
281
285
  raise ArgumentError,
282
- "loca must be a parsed Loca table with offset_for and size_of methods"
286
+ "loca must be a parsed Loca table"
283
287
  end
284
288
 
285
289
  unless loca.parsed?
@@ -287,7 +291,7 @@ module Fontisan
287
291
  "loca table must be parsed with parse_with_context before use"
288
292
  end
289
293
 
290
- unless head.respond_to?(:units_per_em)
294
+ unless head.is_a?(Head)
291
295
  raise ArgumentError,
292
296
  "head must be a parsed Head table"
293
297
  end
@@ -117,7 +117,7 @@ module Fontisan
117
117
  glyph = glyph_for(glyph_id)
118
118
  return nil if glyph.nil?
119
119
 
120
- glyph.num_contours unless glyph.nil?
120
+ glyph&.num_contours
121
121
  end
122
122
 
123
123
  # Get number of points for a simple glyph
@@ -20,6 +20,9 @@ module Fontisan
20
20
  # gvar = Fontisan::Tables::Gvar.read(data)
21
21
  # deltas = gvar.glyph_variations(glyph_id)
22
22
  class Gvar < Binary::BaseRecord
23
+ extend Registered
24
+
25
+ register_tag "gvar"
23
26
  uint16 :major_version
24
27
  uint16 :minor_version
25
28
  uint16 :axis_count
@@ -15,6 +15,9 @@ module Fontisan
15
15
  # puts head.units_per_em # => 2048
16
16
  # puts head.version_number # => 1.0
17
17
  class Head < Binary::BaseRecord
18
+ extend Registered
19
+
20
+ register_tag "head"
18
21
  # Magic number that must be present in the head table
19
22
  MAGIC_NUMBER = 0x5F0F3CF5
20
23
 
@@ -18,6 +18,9 @@ module Fontisan
18
18
  # puts hhea.descent # => -512
19
19
  # puts hhea.version_number # => 1.0
20
20
  class Hhea < Binary::BaseRecord
21
+ extend Registered
22
+
23
+ register_tag "hhea"
21
24
  # Table size in bytes (fixed size)
22
25
  TABLE_SIZE = 36
23
26
 
@@ -38,6 +38,9 @@ module Fontisan
38
38
  # puts "Advance width: #{metric[:advance_width]}"
39
39
  # puts "LSB: #{metric[:lsb]}"
40
40
  class Hmtx < Binary::BaseRecord
41
+ extend Registered
42
+
43
+ register_tag "hmtx"
41
44
  # LongHorMetric record structure
42
45
  #
43
46
  # @!attribute advance_width
@@ -47,13 +50,6 @@ module Fontisan
47
50
  class LongHorMetric < Binary::BaseRecord
48
51
  uint16 :advance_width
49
52
  int16 :lsb
50
-
51
- # Convert to hash for convenience
52
- #
53
- # @return [Hash] Hash with :advance_width and :lsb keys
54
- def to_h
55
- { advance_width: advance_width, lsb: lsb }
56
- end
57
53
  end
58
54
 
59
55
  # Store the raw data for deferred parsing
@@ -18,6 +18,10 @@ module Fontisan
18
18
  # hvar = Fontisan::Tables::Hvar.read(data)
19
19
  # advance_deltas = hvar.advance_width_deltas(glyph_id, coordinates)
20
20
  class Hvar < Binary::BaseRecord
21
+ extend Registered
22
+
23
+ register_tag "HVAR"
24
+
21
25
  uint16 :major_version
22
26
  uint16 :minor_version
23
27
  uint32 :item_variation_store_offset
@@ -42,6 +42,9 @@ module Fontisan
42
42
  # size = loca.size_of(42)
43
43
  # is_empty = loca.empty?(42)
44
44
  class Loca < Binary::BaseRecord
45
+ extend Registered
46
+
47
+ register_tag "loca"
45
48
  # Short format constant (from head.indexToLocFormat)
46
49
  FORMAT_SHORT = 0
47
50
 
@@ -27,6 +27,9 @@ module Fontisan
27
27
  # puts maxp.version # => 1.0 or 0.5
28
28
  # puts maxp.truetype? # => true or false
29
29
  class Maxp < Binary::BaseRecord
30
+ extend Registered
31
+
32
+ register_tag "maxp"
30
33
  # Version 0.5 constant (CFF fonts)
31
34
  VERSION_0_5 = 0x00005000
32
35
 
@@ -21,6 +21,9 @@ module Fontisan
21
21
  # mvar = Fontisan::Tables::Mvar.read(data)
22
22
  # hasc_deltas = mvar.metric_deltas("hasc")
23
23
  class Mvar < Binary::BaseRecord
24
+ extend Registered
25
+
26
+ register_tag "MVAR"
24
27
  uint16 :major_version
25
28
  uint16 :minor_version
26
29
  uint16 :reserved
@@ -64,6 +64,9 @@ module Fontisan
64
64
  # name = Fontisan::Tables::Name.read(data)
65
65
  # puts name.english_name(Fontisan::Tables::Name::FAMILY)
66
66
  class Name < Binary::BaseRecord
67
+ extend Registered
68
+
69
+ register_tag "name"
67
70
  # Name ID constants for common name records
68
71
  COPYRIGHT = 0
69
72
  FAMILY = 1
@@ -14,6 +14,9 @@ module Fontisan
14
14
  #
15
15
  # Reference: OpenType specification, OS/2 table
16
16
  class Os2 < Binary::BaseRecord
17
+ extend Registered
18
+
19
+ register_tag "OS/2"
17
20
  endian :big
18
21
 
19
22
  # Version 0 fields (all versions have these)
@@ -10,6 +10,9 @@ module Fontisan
10
10
  #
11
11
  # Reference: OpenType specification, post table
12
12
  class Post < Binary::BaseRecord
13
+ extend Registered
14
+
15
+ register_tag "post"
13
16
  # Standard Mac glyph names for version 1.0 (258 glyphs)
14
17
  # rubocop:disable Metrics/CollectionLiteralLength
15
18
  STANDARD_NAMES = %w[
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Single source of truth mapping OpenType table tags to Ruby classes.
6
+ #
7
+ # Each table class registers itself via `register_tag`:
8
+ #
9
+ # module Fontisan::Tables
10
+ # class Hvar < Binary::BaseRecord
11
+ # register_tag "HVAR"
12
+ # ...
13
+ # end
14
+ # end
15
+ #
16
+ # Dispatch sites use `Tables::Registry.for(tag)` instead of a
17
+ # `case tag when "HVAR"` switch. This is OCP-compliant: adding a
18
+ # new table means adding one `register_tag` line in the table's
19
+ # file, not editing every dispatch site.
20
+ module Registry
21
+ @by_tag = {}
22
+ @by_class = {}
23
+
24
+ class << self
25
+ # @api internal Called by table classes at load time.
26
+ # @param tag [String] Four-byte OpenType table tag (e.g., "HVAR").
27
+ # @param klass [Class] The Ruby class implementing the table.
28
+ def register(tag, klass)
29
+ @by_tag[tag] = klass
30
+ @by_class[klass] = tag
31
+ end
32
+
33
+ # Look up the table class for a tag.
34
+ #
35
+ # @param tag [String] Four-byte OpenType table tag.
36
+ # @return [Class, nil] The table class, or nil if unknown.
37
+ def for(tag)
38
+ @by_tag[tag]
39
+ end
40
+
41
+ # Reverse lookup: tag for a given class.
42
+ #
43
+ # @param klass [Class] A table class.
44
+ # @return [String, nil] The tag, or nil if not registered.
45
+ def tag_for(klass)
46
+ @by_class[klass]
47
+ end
48
+
49
+ # @return [Array<String>] All registered tags.
50
+ def tags
51
+ @by_tag.keys
52
+ end
53
+
54
+ # Clear all registrations (testing only).
55
+ def __clear__
56
+ @by_tag.clear
57
+ @by_class.clear
58
+ end
59
+ end
60
+ end
61
+
62
+ # Mixin applied to table classes that register themselves with the
63
+ # Registry at load time. Used via `register_tag "HVAR"` in the
64
+ # class body.
65
+ module Registered
66
+ def register_tag(tag)
67
+ Tables::Registry.register(tag, self)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -18,6 +18,9 @@ module Fontisan
18
18
  # vvar = Fontisan::Tables::Vvar.read(data)
19
19
  # advance_deltas = vvar.advance_height_deltas(glyph_id, coordinates)
20
20
  class Vvar < Binary::BaseRecord
21
+ extend Registered
22
+
23
+ register_tag "VVAR"
21
24
  uint16 :major_version
22
25
  uint16 :minor_version
23
26
  uint32 :item_variation_store_offset
@@ -57,6 +57,8 @@ module Fontisan
57
57
  autoload :Os2Table, "fontisan/tables/os2_table"
58
58
  autoload :Post, "fontisan/tables/post"
59
59
  autoload :PostTable, "fontisan/tables/post_table"
60
+ autoload :Registered, "fontisan/tables/registry"
61
+ autoload :Registry, "fontisan/tables/registry"
60
62
  autoload :Sbix, "fontisan/tables/sbix"
61
63
  autoload :SimpleGlyph, "fontisan/tables/glyf/simple_glyph"
62
64
  autoload :Svg, "fontisan/tables/svg"
@@ -167,10 +167,10 @@ module Fontisan
167
167
 
168
168
  # Underline properties
169
169
  post = @font.table(Constants::POST_TAG)
170
- underline_position = post&.underline_position if post.respond_to?(:underline_position)
170
+ underline_position = post&.underline_position
171
171
  afm_lines << "UnderlinePosition #{underline_position}" if underline_position
172
172
 
173
- underline_thickness = post&.underline_thickness if post.respond_to?(:underline_thickness)
173
+ underline_thickness = post&.underline_thickness
174
174
  afm_lines << "UnderlineThickness #{underline_thickness}" if underline_thickness
175
175
  end
176
176
 
@@ -252,11 +252,7 @@ module Fontisan
252
252
  os2 = @font.table(Constants::OS2_TAG)
253
253
  return "Regular" unless os2
254
254
 
255
- weight_class = if os2.respond_to?(:us_weight_class)
256
- os2.us_weight_class
257
- elsif os2.respond_to?(:weight_class)
258
- os2.weight_class
259
- end
255
+ weight_class = os2.us_weight_class
260
256
  return "Regular" unless weight_class
261
257
 
262
258
  case weight_class
@@ -280,11 +276,7 @@ module Fontisan
280
276
  post = @font.table(Constants::POST_TAG)
281
277
  return 0.0 unless post
282
278
 
283
- if post.respond_to?(:italic_angle)
284
- post.italic_angle
285
- else
286
- 0.0
287
- end
279
+ post.italic_angle
288
280
  end
289
281
 
290
282
  # Check if font is monospace
@@ -294,11 +286,7 @@ module Fontisan
294
286
  post = @font.table(Constants::POST_TAG)
295
287
  return false unless post
296
288
 
297
- if post.respond_to?(:is_fixed_pitch)
298
- post.is_fixed_pitch
299
- else
300
- false
301
- end
289
+ post.is_fixed_pitch
302
290
  end
303
291
 
304
292
  # Extract version from name table
@@ -308,9 +296,7 @@ module Fontisan
308
296
  name_table = @font.table(Constants::NAME_TAG)
309
297
  return nil unless name_table
310
298
 
311
- if name_table.respond_to?(:version_string)
312
- name_table.version_string(1) || name_table.version_string(3)
313
- end
299
+ name_table.english_name(Tables::Name::VERSION)
314
300
  end
315
301
 
316
302
  # Extract copyright from name table
@@ -320,9 +306,7 @@ module Fontisan
320
306
  name_table = @font.table(Constants::NAME_TAG)
321
307
  return nil unless name_table
322
308
 
323
- if name_table.respond_to?(:copyright)
324
- name_table.copyright(1) || name_table.copyright(3)
325
- end
309
+ name_table.english_name(Tables::Name::COPYRIGHT)
326
310
  end
327
311
 
328
312
  # Extract character mappings from cmap table
@@ -332,30 +316,7 @@ module Fontisan
332
316
  cmap = @font.table(Constants::CMAP_TAG)
333
317
  return {} unless cmap
334
318
 
335
- @extract_character_mappings ||= begin
336
- mappings = {}
337
-
338
- # Try to get Unicode mappings (most reliable method)
339
- if cmap.respond_to?(:unicode_mappings)
340
- mappings = cmap.unicode_mappings || {}
341
- elsif cmap.respond_to?(:unicode_bmp_mapping)
342
- mappings = cmap.unicode_bmp_mapping || {}
343
- elsif cmap.respond_to?(:subtables)
344
- # Look for Unicode BMP subtable
345
- unicode_subtable = cmap.subtables.find do |subtable|
346
- subtable.respond_to?(:platform_id) &&
347
- subtable.platform_id == 3 &&
348
- subtable.respond_to?(:encoding_id) &&
349
- subtable.encoding_id == 1
350
- end
351
-
352
- if unicode_subtable.respond_to?(:glyph_index_map)
353
- mappings = unicode_subtable.glyph_index_map
354
- end
355
- end
356
-
357
- mappings
358
- end
319
+ @extract_character_mappings ||= cmap.unicode_mappings || {}
359
320
  end
360
321
 
361
322
  # Extract font bounding box
@@ -365,12 +326,7 @@ module Fontisan
365
326
  head = @font.table(Constants::HEAD_TAG)
366
327
  return nil unless head
367
328
 
368
- if head.respond_to?(:font_bounding_box)
369
- head.font_bounding_box
370
- elsif head.respond_to?(:x_min) && head.respond_to?(:y_min) &&
371
- head.respond_to?(:x_max) && head.respond_to?(:y_max)
372
- [head.x_min, head.y_min, head.x_max, head.y_max]
373
- end
329
+ [head.x_min, head.y_min, head.x_max, head.y_max]
374
330
  end
375
331
 
376
332
  # Extract glyph bounding box
@@ -390,7 +346,7 @@ module Fontisan
390
346
  return nil unless head_table
391
347
 
392
348
  # Ensure loca is parsed with context
393
- if loca_table.respond_to?(:parse_with_context) && !loca_table.parsed?
349
+ if loca_table.is_a?(Tables::Loca) && !loca_table.parsed?
394
350
  maxp = @font.table(Constants::MAXP_TAG)
395
351
  if maxp
396
352
  loca_table.parse_with_context(head_table.index_to_loc_format,
@@ -398,17 +354,8 @@ module Fontisan
398
354
  end
399
355
  end
400
356
 
401
- if glyf_table.respond_to?(:glyph_for)
402
- glyph = glyf_table.glyph_for(glyph_id, loca_table, head_table)
403
- return nil unless glyph
404
-
405
- if glyph.respond_to?(:bounding_box)
406
- glyph.bounding_box
407
- elsif glyph.respond_to?(:x_min) && glyph.respond_to?(:y_min) &&
408
- glyph.respond_to?(:x_max) && glyph.respond_to?(:y_max)
409
- [glyph.x_min, glyph.y_min, glyph.x_max, glyph.y_max]
410
- end
411
- end
357
+ glyph = glyf_table.glyph_for(glyph_id, loca_table, head_table)
358
+ glyph&.bounding_box
412
359
  end
413
360
 
414
361
  # Extract kerning pairs from GPOS table
@@ -151,10 +151,10 @@ module Fontisan
151
151
  return base_cff unless accent_cff
152
152
 
153
153
  io = String.new(encoding: Encoding::ASCII_8BIT)
154
- io << base_cff.sub(/\x0e\z/, "")
154
+ io << base_cff.delete_suffix("\u000E")
155
155
  io << encode_cff_number(seac_data[:adx].to_i) if seac_data[:adx].to_i != 0
156
156
  io << encode_cff_number(seac_data[:ady].to_i) if seac_data[:ady].to_i != 0
157
- io << accent_cff.sub(/\x0e\z/, "")
157
+ io << accent_cff.delete_suffix("\u000E")
158
158
  io << encode_cff_operator(TYPE1_TO_CFF[:endchar])
159
159
  rescue StandardError
160
160
  encode_cff_operator(TYPE1_TO_CFF[:endchar])
@@ -99,10 +99,11 @@ module Fontisan
99
99
  @autohint
100
100
  end
101
101
 
102
- # Convert to hash
102
+ # Expose the options as a Hash. Internal accessor for code that
103
+ # passes options as a plain Hash (e.g., to legacy generators).
103
104
  #
104
105
  # @return [Hash] Options as hash
105
- def to_hash
106
+ def options_hash
106
107
  {
107
108
  upm_scale: @upm_scale,
108
109
  encoding: @encoding,
@@ -112,7 +112,7 @@ module Fontisan
112
112
  return "" if data.nil? || data.empty?
113
113
 
114
114
  # Ensure binary encoding before concatenation
115
- data = data.b if data.respond_to?(:b)
115
+ data = data.b
116
116
 
117
117
  # Prepend lenIV random bytes
118
118
  if len_iv.positive?
@@ -139,32 +139,37 @@ module Fontisan
139
139
  def generate
140
140
  result = {}
141
141
 
142
+ opts = options_hash
143
+
142
144
  # Always generate AFM
143
- result[:afm] = AFMGenerator.generate(@font, to_hash)
145
+ result[:afm] = AFMGenerator.generate(@font, opts)
144
146
 
145
147
  # Always generate PFM (for Windows compatibility)
146
- result[:pfm] = PFMGenerator.generate(@font, to_hash)
148
+ result[:pfm] = PFMGenerator.generate(@font, opts)
147
149
 
148
150
  # Generate PFB or PFA based on format option
149
151
  format = @options.format || :pfb
150
152
  if format == :pfa
151
- result[:pfa] = PFAGenerator.generate(@font, to_hash)
153
+ result[:pfa] = PFAGenerator.generate(@font, opts)
152
154
  else
153
- result[:pfb] = PFBGenerator.generate(@font, to_hash)
155
+ result[:pfb] = PFBGenerator.generate(@font, opts)
154
156
  end
155
157
 
156
158
  # Generate INF for Windows installation
157
- result[:inf] = INFGenerator.generate(@font, to_hash)
159
+ result[:inf] = INFGenerator.generate(@font, opts)
158
160
 
159
161
  result
160
162
  end
161
163
 
162
164
  private
163
165
 
164
- # Convert options to hash
166
+ # Build the options hash forwarded to the per-format generators.
167
+ # This is an internal adapter — ConversionOptions exposes typed
168
+ # accessors; the per-format generators accept a Hash for historical
169
+ # reasons.
165
170
  #
166
171
  # @return [Hash] Options as hash
167
- def to_hash
172
+ def options_hash
168
173
  {
169
174
  upm_scale: @options.upm_scale || 1000,
170
175
  encoding: @options.encoding || Encodings::AdobeStandard,
@@ -200,8 +205,8 @@ module Fontisan
200
205
  # @return [String] Base filename
201
206
  def self.extract_base_name(font)
202
207
  name_table = font.table(Constants::NAME_TAG)
203
- if name_table.respond_to?(:postscript_name)
204
- name = name_table.postscript_name(1) || name_table.postscript_name(3)
208
+ if name_table
209
+ name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
205
210
  return name if name
206
211
  end
207
212
 
@@ -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