fontisan 0.4.42 → 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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.bug-fixes/09-type1-cff-seac-expansion.md +22 -0
  3. data/TODO.bug-fixes/10-ttf-type1-composite-glyphs.md +23 -0
  4. data/TODO.bug-fixes/11-remaining-respond-to.md +25 -0
  5. data/TODO.bug-fixes/12-type1-generators-duck-typing.md +80 -0
  6. data/TODO.bug-fixes/13-hint-extractor-duck-typing.md +30 -0
  7. data/TODO.bug-fixes/14-font-interface-type-checks.md +82 -0
  8. data/TODO.bug-fixes/15-glyph-type-checks-and-simple-glyph-points-bug.md +49 -0
  9. data/TODO.bug-fixes/16-variation-table-checks.md +31 -0
  10. data/TODO.bug-fixes/17-ufo-and-stitcher-cleanup.md +32 -0
  11. data/TODO.bug-fixes/18-generic-value-and-io-checks.md +56 -0
  12. data/TODO.bug-fixes/19-serialization-migration.md +52 -0
  13. data/TODO.bug-fixes/20-variation-validator-type-checks.md +29 -0
  14. data/TODO.bug-fixes/21-xml-builder-send.md +24 -0
  15. data/TODO.bug-fixes/22-type1-converter-font-info.md +25 -0
  16. data/TODO.bug-fixes/23-boundary-type-checks.md +31 -0
  17. data/TODO.bug-fixes/24-validator-dynamic-field-key.md +25 -0
  18. data/TODO.bug-fixes/25-serialization-migration.md +49 -0
  19. data/TODO.bug-fixes/26-variation-preserver-spec-migration.md +25 -0
  20. data/TODO.bug-fixes/27-table-class-registry.md +50 -0
  21. data/TODO.bug-fixes/28-spec-doubles-cleanup.md +40 -0
  22. data/TODO.bug-fixes/29-strategy-registries.md +30 -0
  23. data/TODO.bug-fixes/README.md +29 -2
  24. data/lib/fontisan/binary/base_record.rb +13 -12
  25. data/lib/fontisan/collection/builder.rb +3 -5
  26. data/lib/fontisan/collection/dfont_builder.rb +4 -11
  27. data/lib/fontisan/collection/writer.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/converters/format_converter.rb +2 -2
  32. data/lib/fontisan/converters/glyf_table_builder.rb +1 -1
  33. data/lib/fontisan/converters/outline_converter.rb +2 -6
  34. data/lib/fontisan/converters/table_copier.rb +4 -12
  35. data/lib/fontisan/converters/type1_converter.rb +12 -37
  36. data/lib/fontisan/converters/woff2_encoder.rb +8 -10
  37. data/lib/fontisan/converters/woff_writer.rb +3 -3
  38. data/lib/fontisan/export/exporter.rb +8 -9
  39. data/lib/fontisan/export/table_serializer.rb +71 -186
  40. data/lib/fontisan/export/transformers/font_to_ttx.rb +18 -18
  41. data/lib/fontisan/export/transformers/head_transformer.rb +1 -1
  42. data/lib/fontisan/export/transformers/hhea_transformer.rb +1 -1
  43. data/lib/fontisan/export/transformers/maxp_transformer.rb +1 -1
  44. data/lib/fontisan/export/transformers/name_transformer.rb +1 -1
  45. data/lib/fontisan/export/transformers/os2_transformer.rb +1 -1
  46. data/lib/fontisan/export/transformers/post_transformer.rb +1 -1
  47. data/lib/fontisan/export/ttx_generator.rb +36 -37
  48. data/lib/fontisan/font_writer.rb +1 -1
  49. data/lib/fontisan/glyph_accessor.rb +9 -15
  50. data/lib/fontisan/hints/postscript_hint_extractor.rb +24 -64
  51. data/lib/fontisan/hints/truetype_hint_extractor.rb +1 -1
  52. data/lib/fontisan/models/ttx/tables/binary_table.rb +1 -1
  53. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  54. data/lib/fontisan/outline_extractor.rb +1 -5
  55. data/lib/fontisan/sfnt_font.rb +3 -3
  56. data/lib/fontisan/sfnt_source.rb +28 -0
  57. data/lib/fontisan/stitcher/source.rb +8 -10
  58. data/lib/fontisan/subset/builder.rb +2 -2
  59. data/lib/fontisan/subset/table_strategy/cff2.rb +2 -3
  60. data/lib/fontisan/svg/font_face_generator.rb +0 -4
  61. data/lib/fontisan/svg/font_generator.rb +0 -4
  62. data/lib/fontisan/svg_to_glyf/assembler.rb +59 -20
  63. data/lib/fontisan/svg_to_glyf/document.rb +28 -9
  64. data/lib/fontisan/svg_to_glyf/geometry/normalizer.rb +34 -17
  65. data/lib/fontisan/tables/cff/charstring.rb +3 -0
  66. data/lib/fontisan/tables/cff/dict.rb +6 -3
  67. data/lib/fontisan/tables/cff/index.rb +1 -1
  68. data/lib/fontisan/tables/cff/private_dict_writer.rb +2 -2
  69. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  70. data/lib/fontisan/tables/cff.rb +3 -0
  71. data/lib/fontisan/tables/cff2/table_builder.rb +2 -4
  72. data/lib/fontisan/tables/cff2.rb +3 -0
  73. data/lib/fontisan/tables/cmap.rb +3 -0
  74. data/lib/fontisan/tables/cvar.rb +3 -0
  75. data/lib/fontisan/tables/fvar.rb +3 -0
  76. data/lib/fontisan/tables/glyf/simple_glyph.rb +9 -0
  77. data/lib/fontisan/tables/glyf.rb +12 -8
  78. data/lib/fontisan/tables/glyf_table.rb +4 -4
  79. data/lib/fontisan/tables/gvar.rb +3 -0
  80. data/lib/fontisan/tables/head.rb +3 -0
  81. data/lib/fontisan/tables/hhea.rb +3 -0
  82. data/lib/fontisan/tables/hmtx.rb +3 -7
  83. data/lib/fontisan/tables/hvar.rb +4 -0
  84. data/lib/fontisan/tables/loca.rb +3 -0
  85. data/lib/fontisan/tables/maxp.rb +3 -0
  86. data/lib/fontisan/tables/mvar.rb +3 -0
  87. data/lib/fontisan/tables/name.rb +3 -0
  88. data/lib/fontisan/tables/os2.rb +3 -0
  89. data/lib/fontisan/tables/post.rb +3 -0
  90. data/lib/fontisan/tables/registry.rb +71 -0
  91. data/lib/fontisan/tables/vvar.rb +3 -0
  92. data/lib/fontisan/tables.rb +2 -0
  93. data/lib/fontisan/type1/afm_generator.rb +12 -65
  94. data/lib/fontisan/type1/charstring_converter.rb +21 -23
  95. data/lib/fontisan/type1/conversion_options.rb +3 -2
  96. data/lib/fontisan/type1/decryptor.rb +1 -1
  97. data/lib/fontisan/type1/generator.rb +14 -9
  98. data/lib/fontisan/type1/inf_generator.rb +10 -35
  99. data/lib/fontisan/type1/pfa_generator.rb +36 -26
  100. data/lib/fontisan/type1/pfb_generator.rb +24 -12
  101. data/lib/fontisan/type1/pfm_generator.rb +14 -31
  102. data/lib/fontisan/type1/ttf_to_type1_converter.rb +88 -26
  103. data/lib/fontisan/ufo/bounds.rb +91 -0
  104. data/lib/fontisan/ufo/compile/fvar.rb +2 -2
  105. data/lib/fontisan/ufo/contour.rb +5 -0
  106. data/lib/fontisan/ufo/convert/from_bin_data.rb +15 -29
  107. data/lib/fontisan/ufo/info.rb +4 -0
  108. data/lib/fontisan/ufo/point.rb +0 -7
  109. data/lib/fontisan/ufo.rb +1 -0
  110. data/lib/fontisan/utilities/brotli_wrapper.rb +3 -5
  111. data/lib/fontisan/utilities/padding.rb +9 -8
  112. data/lib/fontisan/validation/collection_validator.rb +2 -2
  113. data/lib/fontisan/validators/validator.rb +4 -4
  114. data/lib/fontisan/variable/delta_applicator.rb +4 -11
  115. data/lib/fontisan/variable/metric_delta_processor.rb +3 -3
  116. data/lib/fontisan/variable/static_font_builder.rb +19 -23
  117. data/lib/fontisan/variation/instance_font_wrapper.rb +41 -0
  118. data/lib/fontisan/variation/instance_writer.rb +19 -17
  119. data/lib/fontisan/variation/metrics_adjuster.rb +17 -20
  120. data/lib/fontisan/variation/validator.rb +26 -38
  121. data/lib/fontisan/variation/variable_svg_generator.rb +0 -48
  122. data/lib/fontisan/variation/variation_preserver.rb +2 -3
  123. data/lib/fontisan/variation.rb +1 -0
  124. data/lib/fontisan/version.rb +1 -1
  125. data/lib/fontisan/woff2/sfnt_checksum.rb +1 -1
  126. data/lib/fontisan/woff2/table_transformer.rb +22 -29
  127. data/lib/fontisan/woff2_font.rb +11 -10
  128. data/lib/fontisan/woff_font.rb +2 -1
  129. data/lib/fontisan.rb +1 -0
  130. metadata +27 -2
@@ -161,9 +161,7 @@ module Fontisan
161
161
  # @return [Integer] Total stem count (hstem + vstem)
162
162
  def calculate_stem_count
163
163
  return 0 unless @hint_set
164
-
165
- # Get font-level hints (from private_dict_hints JSON)
166
- return 0 unless @hint_set.respond_to?(:private_dict_hints)
164
+ return 0 unless @hint_set.is_a?(Models::HintSet)
167
165
 
168
166
  begin
169
167
  font_hints = JSON.parse(@hint_set.private_dict_hints || "{}")
@@ -194,7 +192,7 @@ module Fontisan
194
192
  #
195
193
  # @return [Boolean] True if private_dict_hints are present
196
194
  def has_font_level_hints?
197
- return false unless @hint_set.respond_to?(:private_dict_hints)
195
+ return false unless @hint_set.is_a?(Models::HintSet)
198
196
 
199
197
  hints = JSON.parse(@hint_set.private_dict_hints || "{}")
200
198
  !hints.empty?
@@ -20,6 +20,9 @@ module Fontisan
20
20
  # cff2 = Fontisan::Tables::Cff2.read(data)
21
21
  # num_glyphs = cff2.glyph_count
22
22
  class Cff2 < Binary::BaseRecord
23
+ extend Registered
24
+
25
+ register_tag "CFF2"
23
26
  # Inner namespace autoloads — declared here so Cff2::* constants
24
27
  # resolve on first reference without require_relative.
25
28
  autoload :BlendOperator, "fontisan/tables/cff2/blend_operator"
@@ -14,6 +14,9 @@ module Fontisan
14
14
  #
15
15
  # Reference: OpenType specification, cmap table
16
16
  class Cmap < Binary::BaseRecord
17
+ extend Registered
18
+
19
+ register_tag "cmap"
17
20
  # Platform IDs
18
21
  PLATFORM_UNICODE = 0
19
22
  PLATFORM_MACINTOSH = 1
@@ -20,6 +20,9 @@ module Fontisan
20
20
  # cvar = Fontisan::Tables::Cvar.read(data)
21
21
  # cvt_deltas = cvar.cvt_variations
22
22
  class Cvar < Binary::BaseRecord
23
+ extend Registered
24
+
25
+ register_tag "cvar"
23
26
  uint16 :major_version
24
27
  uint16 :minor_version
25
28
  uint16 :tuple_variation_count
@@ -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
- if glyph.respond_to?(:instruction_length)
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
@@ -76,7 +76,7 @@ module Fontisan
76
76
  glyph = glyph_for(glyph_id)
77
77
  return false if glyph.nil?
78
78
 
79
- glyph.respond_to?(:num_contours) && glyph.num_contours >= 0
79
+ !glyph.nil? && glyph.num_contours >= 0
80
80
  end
81
81
 
82
82
  # Check if a glyph is compound
@@ -87,7 +87,7 @@ module Fontisan
87
87
  glyph = glyph_for(glyph_id)
88
88
  return false if glyph.nil?
89
89
 
90
- glyph.respond_to?(:num_contours) && glyph.num_contours == -1
90
+ !glyph.nil? && glyph.num_contours == -1
91
91
  end
92
92
 
93
93
  # Check if a glyph is empty
@@ -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 if glyph.respond_to?(:num_contours)
120
+ glyph&.num_contours
121
121
  end
122
122
 
123
123
  # Get number of points for a simple glyph
@@ -126,7 +126,7 @@ module Fontisan
126
126
  # @return [Integer, nil] Number of points, or nil
127
127
  def glyph_point_count(glyph_id)
128
128
  glyph = glyph_for(glyph_id)
129
- return nil if glyph.nil? || !glyph.respond_to?(:num_points)
129
+ return nil if glyph.nil? || glyph.num_points.nil?
130
130
 
131
131
  glyph.num_points
132
132
  end
@@ -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
@@ -136,29 +136,27 @@ module Fontisan
136
136
  # @param seac_data [Hash] seac component data
137
137
  # @return [String] CFF CharString bytecode with expanded seac
138
138
  def expand_seac(seac_data)
139
- # seac format: adx ady bchar achar seac
140
- # adx, ady: accent offset
141
- # bchar: base character code
142
- # achar: accent character code
143
- # The accent is positioned at (adx, ady) relative to the base
144
-
145
- seac_data[:base]
146
- seac_data[:accent]
147
- seac_data[:adx]
148
- seac_data[:ady]
149
-
150
- # For now, we'll create a simple placeholder that indicates seac expansion
151
- # In a full implementation, we would:
152
- # 1. Parse the base glyph's CharString
153
- # 2. Parse the accent glyph's CharString
154
- # 3. Merge them with the appropriate offset
155
- # 4. Convert to CFF format
156
-
157
- # This is a simplified implementation that creates a composite reference
158
- # CFF doesn't have native seac, so we need to actually merge the outlines
159
-
160
- # For now, return endchar as placeholder
161
- # TODO: Implement full seac expansion by merging glyph outlines
139
+ return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless @charstrings
140
+
141
+ base_name = @charstrings.encoding[seac_data[:base]]
142
+ accent_name = @charstrings.encoding[seac_data[:accent]]
143
+ return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless base_name && accent_name
144
+
145
+ base_cs = @charstrings[base_name]
146
+ accent_cs = @charstrings[accent_name]
147
+ return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless base_cs && accent_cs
148
+
149
+ base_cff = convert(base_cs)
150
+ accent_cff = convert(accent_cs)
151
+ return base_cff unless accent_cff
152
+
153
+ io = String.new(encoding: Encoding::ASCII_8BIT)
154
+ io << base_cff.delete_suffix("\u000E")
155
+ io << encode_cff_number(seac_data[:adx].to_i) if seac_data[:adx].to_i != 0
156
+ io << encode_cff_number(seac_data[:ady].to_i) if seac_data[:ady].to_i != 0
157
+ io << accent_cff.delete_suffix("\u000E")
158
+ io << encode_cff_operator(TYPE1_TO_CFF[:endchar])
159
+ rescue StandardError
162
160
  encode_cff_operator(TYPE1_TO_CFF[:endchar])
163
161
  end
164
162
 
@@ -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?