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
@@ -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
@@ -61,8 +61,7 @@ module Fontisan
61
61
  name_table = font.table("name")
62
62
  return unless name_table
63
63
 
64
- records = name_table.respond_to?(:name_records) ? name_table.name_records : []
65
- records.each do |record|
64
+ name_table.name_records.each do |record|
66
65
  next unless record.platform_id == 3 && record.encoding_id == 1 # Windows Unicode BMP
67
66
 
68
67
  value = decode_name_value(record, name_table)
@@ -79,9 +78,7 @@ module Fontisan
79
78
  end
80
79
 
81
80
  def self.decode_name_value(record, name_table)
82
- raw = if name_table.respond_to?(:string_for_record)
83
- name_table.string_for_record(record)
84
- end
81
+ raw = name_table.string_for_record(record)
85
82
 
86
83
  if raw && raw.encoding == Encoding::UTF_16BE
87
84
  raw.encode("UTF-8")
@@ -100,12 +97,7 @@ module Fontisan
100
97
  post = font.table("post")
101
98
  return unless post
102
99
 
103
- if post.respond_to?(:italic_angle)
104
- info.italic_angle = post.italic_angle
105
- elsif post.respond_to?(:italic_angle_raw)
106
- raw = post.italic_angle_raw
107
- info.italic_angle = raw.to_i / 65536.0
108
- end
100
+ info.italic_angle = post.italic_angle
109
101
  rescue NoMethodError
110
102
  # post table may not have italic_angle
111
103
  end
@@ -128,7 +120,7 @@ module Fontisan
128
120
  cmap_table = font.table("cmap")
129
121
  return {} unless cmap_table
130
122
 
131
- mappings = cmap_table.respond_to?(:unicode_mappings) ? cmap_table.unicode_mappings : {}
123
+ mappings = cmap_table.unicode_mappings || {}
132
124
  # Invert: gid → [codepoints]
133
125
  inverted = Hash.new { |h, k| h[k] = [] }
134
126
  mappings.each { |cp, gid| inverted[gid] << cp }
@@ -146,13 +138,11 @@ module Fontisan
146
138
  num_glyphs = maxp&.num_glyphs || 0
147
139
 
148
140
  # Hmtx requires context-aware parsing before metric_for works.
149
- if hmtx.respond_to?(:parse_with_context)
150
- hmtx.parse_with_context(num_h_metrics, num_glyphs)
151
- end
141
+ hmtx.parse_with_context(num_h_metrics, num_glyphs)
152
142
 
153
143
  widths = {}
154
144
  num_glyphs.times do |gid|
155
- metric = hmtx.respond_to?(:metric_for) ? hmtx.metric_for(gid) : nil
145
+ metric = hmtx.metric_for(gid)
156
146
  widths[gid] = metric ? metric[:advance_width] : 0
157
147
  end
158
148
  widths
@@ -169,10 +159,8 @@ module Fontisan
169
159
  return unless glyf && loca && head
170
160
 
171
161
  # Tables need context-aware initialization before per-glyph access.
172
- if loca.respond_to?(:parse_with_context)
173
- loca.parse_with_context(head.index_to_loc_format,
174
- num_glyphs)
175
- end
162
+ loca.parse_with_context(head.index_to_loc_format,
163
+ num_glyphs)
176
164
 
177
165
  num_glyphs.times do |gid|
178
166
  glyph_name = glyph_name_for(font, gid) || "glyph#{gid}"
@@ -189,7 +177,7 @@ module Fontisan
189
177
 
190
178
  if simple.is_a?(Fontisan::Tables::SimpleGlyph)
191
179
  extract_simple_contours(simple, ufo_glyph)
192
- elsif simple.respond_to?(:compound?) && simple.compound?
180
+ elsif simple.is_a?(Fontisan::Tables::CompoundGlyph) && simple.compound?
193
181
  extract_compound_contours(simple, ufo_glyph, glyf, loca, head)
194
182
  end
195
183
 
@@ -309,7 +297,7 @@ module Fontisan
309
297
  visited = visited.dup.add(compound.glyph_id)
310
298
 
311
299
  compound.components.each do |component|
312
- next unless component.respond_to?(:args_are_xy?) ? component.args_are_xy? : true
300
+ next unless component.args_are_xy?
313
301
 
314
302
  raw = begin
315
303
  glyf.glyph_for(component.glyph_index, loca, head)
@@ -320,9 +308,9 @@ module Fontisan
320
308
 
321
309
  matrix = component.transformation_matrix
322
310
 
323
- if raw.respond_to?(:simple?) && raw.simple?
311
+ if raw.is_a?(Fontisan::Tables::SimpleGlyph) && raw.simple?
324
312
  flatten_simple_component(raw, ufo_glyph, matrix)
325
- elsif raw.respond_to?(:compound?) && raw.compound?
313
+ elsif raw.is_a?(Fontisan::Tables::CompoundGlyph) && raw.compound?
326
314
  flatten_compound(raw, ufo_glyph, glyf, loca, head, visited, depth + 1)
327
315
  end
328
316
  end
@@ -358,12 +346,10 @@ module Fontisan
358
346
  post = font.table("post")
359
347
  return nil unless post
360
348
 
361
- if post.respond_to?(:glyph_name)
362
- name = post.glyph_name(gid)
363
- return name unless name.nil? || name.empty?
364
- end
349
+ name = post.glyph_name(gid)
350
+ return nil if name.nil? || name.empty?
365
351
 
366
- nil
352
+ name
367
353
  rescue NoMethodError
368
354
  nil
369
355
  end
@@ -26,6 +26,10 @@ module Fontisan
26
26
 
27
27
  attr_accessor(*STANDARD_FIELDS)
28
28
 
29
+ # Variation font metadata. Populated by UFO 3 extensions or by
30
+ # explicit compiler configuration; nil when not set.
31
+ attr_accessor :axes, :named_instances
32
+
29
33
  # Catch-all for non-standard (vendor-specific) fields.
30
34
  attr_accessor :extras
31
35
 
@@ -27,13 +27,6 @@ module Fontisan
27
27
  def off_curve?
28
28
  @type == "offcurve" || @type == "qcurve"
29
29
  end
30
-
31
- # @return [Hash] suitable for `to_glif` output
32
- def to_h
33
- h = { x: @x, y: @y, type: @type }
34
- h[:smooth] = true if @smooth
35
- h
36
- end
37
30
  end
38
31
  end
39
32
  end
data/lib/fontisan/ufo.rb CHANGED
@@ -24,6 +24,7 @@ module Fontisan
24
24
  autoload :LayerSet, "fontisan/ufo/layer_set"
25
25
  autoload :Glyph, "fontisan/ufo/glyph"
26
26
  autoload :Contour, "fontisan/ufo/contour"
27
+ autoload :Bounds, "fontisan/ufo/bounds"
27
28
  autoload :Point, "fontisan/ufo/point"
28
29
  autoload :Component, "fontisan/ufo/component"
29
30
  autoload :Anchor, "fontisan/ufo/anchor"
@@ -129,13 +129,11 @@ module Fontisan
129
129
  # @param data [String] Data to validate
130
130
  # @raise [ArgumentError] If data is invalid
131
131
  def validate_data!(data)
132
- if data.nil?
133
- raise ArgumentError, "Data cannot be nil"
134
- end
132
+ raise ArgumentError, "Data cannot be nil" if data.nil?
135
133
 
136
- unless data.respond_to?(:bytesize)
134
+ unless data.is_a?(String)
137
135
  raise ArgumentError,
138
- "Data must be a String-like object, got #{data.class}"
136
+ "Data must be a String, got #{data.class}"
139
137
  end
140
138
  end
141
139
  end
@@ -10,6 +10,9 @@ module Fontisan
10
10
  # across nine call sites before extraction; this module is the
11
11
  # single source of truth.
12
12
  #
13
+ # All public methods take an Integer size. Callers with a String
14
+ # compute `.bytesize` first.
15
+ #
13
16
  # @example Get padding length
14
17
  # Padding.boundary(13) # => 3
15
18
  # Padding.boundary(13, boundary: 4) # => 3
@@ -21,14 +24,13 @@ module Fontisan
21
24
  module Padding
22
25
  # Count of trailing pad bytes needed to align `size` to `boundary`.
23
26
  #
24
- # @param size [Integer, #bytesize] Either an Integer size, or any
25
- # object responding to `#bytesize` (e.g. a String).
27
+ # @param size [Integer] Size in bytes (callers with a String
28
+ # compute `.bytesize` first).
26
29
  # @param boundary [Integer] Alignment boundary in bytes (default
27
30
  # `Constants::TABLE_ALIGNMENT`).
28
31
  # @return [Integer] Number of pad bytes in 0...boundary-1
29
32
  def self.boundary(size, boundary: Constants::TABLE_ALIGNMENT)
30
- length = size.respond_to?(:bytesize) ? size.bytesize : size
31
- (boundary - (length % boundary)) % boundary
33
+ (boundary - (size % boundary)) % boundary
32
34
  end
33
35
 
34
36
  # Return `bytes` followed by null padding to the requested boundary.
@@ -38,7 +40,7 @@ module Fontisan
38
40
  # @return [String] The original string if already aligned, else a
39
41
  # new binary string with trailing nulls.
40
42
  def self.pad(bytes, boundary: Constants::TABLE_ALIGNMENT)
41
- pad_count = boundary(bytes, boundary:)
43
+ pad_count = boundary(bytes.bytesize, boundary:)
42
44
  return bytes if pad_count.zero?
43
45
 
44
46
  out = bytes.dup.force_encoding(Encoding::BINARY)
@@ -48,12 +50,11 @@ module Fontisan
48
50
 
49
51
  # Aligned size of `size` after padding to `boundary`.
50
52
  #
51
- # @param size [Integer, #bytesize] Original size or bytes-like.
53
+ # @param size [Integer] Original size in bytes.
52
54
  # @param boundary [Integer] Alignment boundary in bytes.
53
55
  # @return [Integer] Size after alignment (multiple of `boundary`).
54
56
  def self.aligned_size(size, boundary: Constants::TABLE_ALIGNMENT)
55
- length = size.respond_to?(:bytesize) ? size.bytesize : size
56
- length + boundary(length, boundary:)
57
+ size + boundary(size, boundary:)
57
58
  end
58
59
  end
59
60
  end
@@ -246,7 +246,7 @@ module Fontisan
246
246
  # @param font [Object] Font object
247
247
  # @return [Boolean] true if TrueType
248
248
  def truetype_font?(font)
249
- return false unless font.respond_to?(:has_table?)
249
+ return false unless font.is_a?(SfntSource)
250
250
 
251
251
  font.has_table?("glyf")
252
252
  end
@@ -256,7 +256,7 @@ module Fontisan
256
256
  # @param font [Object] Font object
257
257
  # @return [Boolean] true if CFF
258
258
  def cff_font?(font)
259
- return false unless font.respond_to?(:has_table?)
259
+ return false unless font.is_a?(SfntSource)
260
260
 
261
261
  font.has_table?("CFF ") || font.has_table?("CFF2")
262
262
  end
@@ -290,10 +290,10 @@ module Fontisan
290
290
  }
291
291
  end
292
292
 
293
- # Get field value
294
- value = if table.respond_to?(field_key)
295
- table.public_send(field_key)
296
- end
293
+ # Get field value via the table's snapshot hash (BinData records
294
+ # expose all declared fields through #snapshot).
295
+ snapshot = table.is_a?(BinData::Record) ? table.snapshot : {}
296
+ value = snapshot[field_key.to_s] || snapshot[field_key.to_sym]
297
297
 
298
298
  result = check_def[:block].call(table, value)
299
299
  passed = result != false && !result.nil?
@@ -260,20 +260,13 @@ module Fontisan
260
260
  # @param tag [String] Table tag
261
261
  # @return [Object, nil] Table object or nil
262
262
  def load_table(tag)
263
- return nil unless @font.respond_to?(:table_data)
263
+ return nil unless @font.is_a?(SfntSource)
264
264
 
265
265
  data = @font.table_data(tag)
266
266
  return nil if data.nil? || data.empty?
267
267
 
268
- # Map tag to table class
269
- table_class = case tag
270
- when "fvar" then Tables::Fvar
271
- when "gvar" then Tables::Gvar
272
- when "HVAR" then Tables::Hvar
273
- when "VVAR" then Tables::Vvar
274
- when "MVAR" then Tables::Mvar
275
- else return nil
276
- end
268
+ table_class = Tables::Registry.for(tag)
269
+ return nil unless table_class
277
270
 
278
271
  table_class.read(data)
279
272
  rescue StandardError => e
@@ -293,7 +286,7 @@ module Fontisan
293
286
  end
294
287
 
295
288
  # Try VVAR
296
- if @vvar.respond_to?(:item_variation_store) && @vvar.item_variation_store
289
+ if @vvar.is_a?(Tables::Vvar) && @vvar.item_variation_store
297
290
  return @vvar.item_variation_store.variation_region_list
298
291
  end
299
292
 
@@ -221,17 +221,17 @@ module Fontisan
221
221
 
222
222
  # Similar to HVAR but for vertical metrics
223
223
  # VVAR has the same structure as HVAR
224
- if @vvar.respond_to?(:advance_height_delta_set) && (delta_set = @vvar.advance_height_delta_set(glyph_id))
224
+ if (delta_set = @vvar.advance_height_delta_set(glyph_id))
225
225
  accumulated = calculate_accumulated_delta(delta_set, region_scalars)
226
226
  result[:advance_height] = apply_rounding(accumulated)
227
227
  end
228
228
 
229
- if @vvar.respond_to?(:tsb_delta_set) && (delta_set = @vvar.tsb_delta_set(glyph_id))
229
+ if (delta_set = @vvar.tsb_delta_set(glyph_id))
230
230
  accumulated = calculate_accumulated_delta(delta_set, region_scalars)
231
231
  result[:tsb] = apply_rounding(accumulated)
232
232
  end
233
233
 
234
- if @vvar.respond_to?(:bsb_delta_set) && (delta_set = @vvar.bsb_delta_set(glyph_id))
234
+ if (delta_set = @vvar.bsb_delta_set(glyph_id))
235
235
  accumulated = calculate_accumulated_delta(delta_set, region_scalars)
236
236
  result[:bsb] = apply_rounding(accumulated)
237
237
  end
@@ -65,19 +65,22 @@ options = {})
65
65
  File.binwrite(output_path, binary)
66
66
  end
67
67
 
68
+ # Per-tag table update dispatch. Each entry maps a tag to
69
+ # [method_name, context_key] for looking up the right args.
70
+ TABLE_UPDATERS = {
71
+ "hmtx" => %i[update_hmtx_table varied_metrics],
72
+ "hhea" => %i[update_hhea_table font_metrics],
73
+ "OS/2" => %i[update_os2_table font_metrics],
74
+ "head" => %i[update_head_table options],
75
+ }.freeze
76
+
68
77
  private
69
78
 
70
- # Collect all tables for static font
71
- #
72
- # @param varied_metrics [Hash] Varied glyph metrics
73
- # @param font_metrics [Hash] Varied font metrics
74
- # @param options [Hash] Build options
75
- # @return [Hash<String, String>] Map of table tag to binary data
76
79
  def collect_tables(varied_metrics, font_metrics, options)
77
80
  tables = {}
78
81
 
79
82
  # Get all table tags from font
80
- table_tags = @font.respond_to?(:tables) ? @font.tables.keys : []
83
+ table_tags = @font.is_a?(SfntSource) ? @font.table_names : []
81
84
 
82
85
  table_tags.each do |tag|
83
86
  # Skip variation tables
@@ -88,17 +91,14 @@ options = {})
88
91
  next if original_data.nil? || original_data.empty?
89
92
 
90
93
  # Update specific tables with varied data
91
- tables[tag] = case tag
92
- when "hmtx"
93
- update_hmtx_table(original_data, varied_metrics)
94
- when "hhea"
95
- update_hhea_table(original_data, font_metrics)
96
- when "OS/2"
97
- update_os2_table(original_data, font_metrics)
98
- when "head"
99
- update_head_table(original_data, options)
94
+ updater = TABLE_UPDATERS[tag]
95
+ tables[tag] = if updater
96
+ method_name, ctx_key = updater
97
+ ctx = { varied_metrics: varied_metrics,
98
+ font_metrics: font_metrics,
99
+ options: options }
100
+ method(method_name).call(original_data, ctx[ctx_key])
100
101
  else
101
- # Copy unchanged
102
102
  original_data
103
103
  end
104
104
  end
@@ -182,12 +182,8 @@ options = {})
182
182
  data = @font.table_data(tag)
183
183
  return nil if data.nil? || data.empty?
184
184
 
185
- table_class = case tag
186
- when "hhea" then Tables::Hhea
187
- when "maxp" then Tables::Maxp
188
- when "head" then Tables::Head
189
- else return nil
190
- end
185
+ table_class = Tables::Registry.for(tag)
186
+ return nil unless table_class
191
187
 
192
188
  table_class.read(data)
193
189
  rescue StandardError
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module Fontisan
6
+ module Variation
7
+ # Wraps a variable font to substitute specific table tags with
8
+ # instance-generated equivalents while forwarding everything else
9
+ # to the original font. Used by VariableSvgGenerator to feed the
10
+ # SVG generator an instance-shaped font without rewriting it.
11
+ #
12
+ # Delegation uses SimpleDelegator so the wrapper is transparent:
13
+ # the original font's interface is preserved verbatim. Only
14
+ # #has_table? is overridden to also report instance-supplied tags.
15
+ #
16
+ # NOTE: #table currently delegates to the original font for any tag
17
+ # not in the instance map. The previous implementation always
18
+ # delegated regardless of @table_data contents, which SvgGenerator
19
+ # depended on (it expects parsed BinData records, not raw bytes).
20
+ # Until SvgGenerator can consume raw instance bytes, this wrapper
21
+ # preserves that behavior. The @table_data map is still exposed so
22
+ # downstream code that wants the raw bytes can read them.
23
+ class InstanceFontWrapper < SimpleDelegator
24
+ # @return [Hash<String, String>] instance-generated table bytes by tag
25
+ attr_reader :table_data
26
+
27
+ # @param original_font [TrueTypeFont, OpenTypeFont] the variable font
28
+ # @param instance_tables [Hash<String, String>] instance-generated bytes
29
+ def initialize(original_font, instance_tables)
30
+ super(original_font)
31
+ @table_data = instance_tables
32
+ end
33
+
34
+ # @param tag [String] table tag
35
+ # @return [Boolean]
36
+ def has_table?(tag)
37
+ @table_data.key?(tag) || __getobj__.has_table?(tag)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -251,24 +251,26 @@ module Fontisan
251
251
  # Parse table data into table object
252
252
  #
253
253
  # @param tag [String] Table tag
254
- # @param data [String] Table binary data
255
- # @return [Object] Parsed table object
254
+ public
255
+
256
+ # Per-tag table parser dispatch. Each entry maps tag → [class, init_mode].
257
+ # :parse means call obj.parse(data); :data= means call obj.data = data.
258
+ TABLE_PARSERS = {
259
+ "head" => [Tables::Head, :parse],
260
+ "maxp" => [Tables::Maxp, :parse],
261
+ "loca" => [Tables::Loca, :data_assign],
262
+ "glyf" => [Tables::Glyf, :data_assign],
263
+ "CFF " => [Tables::Cff, :parse],
264
+ "CFF2" => [Tables::Cff2, :parse],
265
+ }.freeze
266
+
256
267
  def parse_table(tag, data)
257
- # For OutlineConverter, we need head, maxp, loca, glyf for TTF
258
- # and CFF for OTF
259
- case tag
260
- when "head"
261
- Tables::Head.new.tap { |t| t.parse(data) }
262
- when "maxp"
263
- Tables::Maxp.new.tap { |t| t.parse(data) }
264
- when "loca"
265
- Tables::Loca.new.tap { |t| t.data = data }
266
- when "glyf"
267
- Tables::Glyf.new.tap { |t| t.data = data }
268
- when "CFF "
269
- Tables::Cff.new.tap { |t| t.parse(data) }
270
- when "CFF2"
271
- Tables::Cff2.new.tap { |t| t.parse(data) }
268
+ entry = TABLE_PARSERS[tag]
269
+ if entry
270
+ klass, init_mode = entry
271
+ obj = klass.new
272
+ init_mode == :parse ? obj.parse(data) : obj.data = data
273
+ obj
272
274
  else
273
275
  # For other tables, return a simple object that just holds data
274
276
  Object.new.tap do |obj|
@@ -213,26 +213,23 @@ module Fontisan
213
213
  end
214
214
  end
215
215
 
216
- # Get base metric value by tag
217
- #
218
- # @param tag [String] Metric tag (e.g., "hasc", "hdsc")
219
- # @return [Integer, nil] Base metric value
216
+ public
217
+
218
+ # Maps MVAR metric tags to [table_tag, method] for base value lookup.
219
+ METRIC_SOURCES = {
220
+ "hasc" => ["hhea", :ascender],
221
+ "hdsc" => ["hhea", :descender],
222
+ "hlgp" => ["hhea", :line_gap],
223
+ "xhgt" => ["OS/2", :sx_height],
224
+ "cpht" => ["OS/2", :s_cap_height],
225
+ }.freeze
226
+
220
227
  def get_base_metric_value(tag)
221
- case tag
222
- when "hasc"
223
- variation_table("hhea")&.ascender
224
- when "hdsc"
225
- variation_table("hhea")&.descender
226
- when "hlgp"
227
- variation_table("hhea")&.line_gap
228
- when "xhgt"
229
- os2 = variation_table("OS/2")
230
- os2&.s_x_height if os2.respond_to?(:s_x_height)
231
- when "cpht"
232
- os2 = variation_table("OS/2")
233
- os2&.s_cap_height if os2.respond_to?(:s_cap_height)
234
- # Add more metrics as needed
235
- end
228
+ source = METRIC_SOURCES[tag]
229
+ return nil unless source
230
+
231
+ table_tag, method = source
232
+ variation_table(table_tag)&.public_send(method)
236
233
  end
237
234
 
238
235
  # Update font metric in appropriate table
@@ -307,7 +304,7 @@ module Fontisan
307
304
  return unless hhea
308
305
 
309
306
  # Update the field if hhea supports it
310
- hhea.number_of_h_metrics = count if hhea.respond_to?(:number_of_h_metrics=)
307
+ hhea.number_of_h_metrics = count
311
308
  end
312
309
  end
313
310
  end