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
@@ -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
@@ -103,11 +103,9 @@ module Fontisan
103
103
  # Check CFF2 if present
104
104
  if @font.has_table?("CFF2")
105
105
  cff2 = @font.table("CFF2")
106
- if cff2.respond_to?(:num_axes)
107
- cff2_axes = cff2.num_axes || 0
108
- if cff2_axes != axis_count && cff2_axes.positive?
109
- @errors << "CFF2 axis count (#{cff2_axes}) doesn't match fvar (#{axis_count})"
110
- end
106
+ cff2_axes = cff2&.num_axes || 0
107
+ if cff2_axes != axis_count && cff2_axes.positive?
108
+ @errors << "CFF2 axis count (#{cff2_axes}) doesn't match fvar (#{axis_count})"
111
109
  end
112
110
  end
113
111
 
@@ -133,20 +131,17 @@ module Fontisan
133
131
  return unless @font.has_table?(table_tag)
134
132
 
135
133
  table = @font.table(table_tag)
136
- return unless table.respond_to?(:item_variation_store)
137
-
138
- store = table.item_variation_store
134
+ store = table&.item_variation_store
139
135
  return unless store
140
136
 
141
- # Check region list axis count
142
- if store.respond_to?(:region_list) && store.region_list
143
- region_list = store.region_list
144
- if region_list.respond_to?(:axis_count)
145
- region_axes = region_list.axis_count
146
- if region_axes != expected_axes
147
- @errors << "#{table_tag} region axis count (#{region_axes}) doesn't match fvar (#{expected_axes})"
148
- end
149
- end
137
+ region_list = store.variation_region_list
138
+ return unless region_list
139
+
140
+ region_axes = region_list.axis_count
141
+ return unless region_axes
142
+
143
+ if region_axes != expected_axes
144
+ @errors << "#{table_tag} region axis count (#{region_axes}) doesn't match fvar (#{expected_axes})"
150
145
  end
151
146
  end
152
147
 
@@ -195,24 +190,15 @@ module Fontisan
195
190
  hvar = @font.table("HVAR")
196
191
  return unless hvar
197
192
 
198
- # Check for item_variation_store
199
- unless hvar.respond_to?(:item_variation_store)
200
- @warnings << "HVAR table doesn't support item_variation_store"
201
- return
202
- end
203
-
204
193
  store = hvar.item_variation_store
205
194
  unless store
206
195
  @warnings << "HVAR has no item variation store"
207
196
  return
208
197
  end
209
198
 
210
- # Check that variation data exists
211
- if store.respond_to?(:item_variation_data)
212
- data = store.item_variation_data
213
- if data.nil? || data.empty?
214
- @warnings << "HVAR has no variation data"
215
- end
199
+ data = store.item_variation_data_entries
200
+ if data.nil? || data.empty?
201
+ @warnings << "HVAR has no variation data"
216
202
  end
217
203
  rescue StandardError => e
218
204
  @warnings << "Failed to check HVAR delta integrity: #{e.message}"
@@ -272,24 +258,26 @@ module Fontisan
272
258
  # @param axes [Array] Variation axes
273
259
  def check_metrics_region_coverage(table_tag, axes)
274
260
  table = @font.table(table_tag)
275
- return unless table.respond_to?(:item_variation_store)
261
+ return unless table
276
262
 
277
263
  store = table.item_variation_store
278
- return unless store.respond_to?(:region_list)
264
+ return unless store
279
265
 
280
- region_list = store.region_list
281
- return unless region_list.respond_to?(:regions)
266
+ region_list = store.variation_region_list
267
+ return unless region_list
282
268
 
283
- # Check each region
284
269
  regions = region_list.regions
285
- regions.each_with_index do |region, idx|
286
- next unless region.respond_to?(:region_axes)
270
+ return unless regions
271
+
272
+ # Each region is an Array<RegionAxisCoordinates>; check coords
273
+ # are within the valid [-1, 1] normalized range.
274
+ regions.each_with_index do |region_axis_coords, idx|
275
+ next unless region_axis_coords.is_a?(Array)
287
276
 
288
- region.region_axes.each_with_index do |reg_axis, axis_idx|
277
+ region_axis_coords.each_with_index do |reg_axis, axis_idx|
289
278
  next if axis_idx >= axes.length
290
279
  next unless reg_axis
291
280
 
292
- # Check coordinates are in valid range [-1, 1]
293
281
  coords = {
294
282
  start_coord: reg_axis.start_coord,
295
283
  peak_coord: reg_axis.peak_coord,
@@ -178,8 +178,6 @@ module Fontisan
178
178
  # @param tables [Hash<String, String>] Font tables
179
179
  # @return [Object] Font-like object
180
180
  def build_font_from_tables(tables)
181
- # Create a simple font wrapper that implements the minimal
182
- # interface needed by SvgGenerator
183
181
  InstanceFontWrapper.new(@font, tables)
184
182
  end
185
183
 
@@ -214,52 +212,6 @@ module Fontisan
214
212
  end
215
213
  coords
216
214
  end
217
-
218
- # Wrapper class for instance font tables
219
- #
220
- # Provides minimal interface needed by SvgGenerator while using
221
- # instance tables instead of original font tables.
222
- class InstanceFontWrapper
223
- # @return [Hash<String, String>] Font tables
224
- attr_reader :table_data
225
-
226
- # Initialize wrapper
227
- #
228
- # @param original_font [Object] Original variable font
229
- # @param instance_tables [Hash<String, String>] Instance tables
230
- def initialize(original_font, instance_tables)
231
- @original_font = original_font
232
- @table_data = instance_tables
233
- end
234
-
235
- # Get table by tag
236
- #
237
- # @param tag [String] Table tag
238
- # @return [Object, nil] Table or nil
239
- def table(tag)
240
- # Use instance table if available, otherwise fall back to original
241
- if @table_data.key?(tag)
242
- end
243
- @original_font.table(tag)
244
- end
245
-
246
- # Check if table exists
247
- #
248
- # @param tag [String] Table tag
249
- # @return [Boolean] True if table exists
250
- def has_table?(tag)
251
- @table_data.key?(tag) || @original_font.has_table?(tag)
252
- end
253
-
254
- # Forward other methods to original font
255
- def method_missing(method, ...)
256
- @original_font.send(method, ...)
257
- end
258
-
259
- def respond_to_missing?(method, include_private = false)
260
- @original_font.respond_to?(method, include_private) || super
261
- end
262
- end
263
215
  end
264
216
  end
265
217
  end
@@ -148,10 +148,9 @@ module Fontisan
148
148
  def validate_source!
149
149
  raise ArgumentError, "Source font cannot be nil" if @source_font.nil?
150
150
 
151
- unless @source_font.respond_to?(:has_table?) &&
152
- @source_font.respond_to?(:table_data)
151
+ unless @source_font.is_a?(SfntSource)
153
152
  raise ArgumentError,
154
- "Source font must respond to :has_table? and :table_data"
153
+ "Source font must be an SfntSource instance"
155
154
  end
156
155
 
157
156
  if @target_tables.nil?
@@ -13,6 +13,7 @@ module Fontisan
13
13
  autoload :DeltaParser, "fontisan/variation/delta_parser"
14
14
  autoload :Inspector, "fontisan/variation/inspector"
15
15
  autoload :InstanceGenerator, "fontisan/variation/instance_generator"
16
+ autoload :InstanceFontWrapper, "fontisan/variation/instance_font_wrapper"
16
17
  autoload :InstanceWriter, "fontisan/variation/instance_writer"
17
18
  autoload :Interpolator, "fontisan/variation/interpolator"
18
19
  autoload :MetricsAdjuster, "fontisan/variation/metrics_adjuster"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.43"
4
+ VERSION = "0.4.44"
5
5
  end
@@ -79,7 +79,7 @@ module Fontisan
79
79
  offsets = {}
80
80
  @tables.each do |t|
81
81
  offsets[t.tag] = cursor
82
- cursor += Utilities::Padding.aligned_size(t.bytes)
82
+ cursor += Utilities::Padding.aligned_size(t.bytes.bytesize)
83
83
  end
84
84
 
85
85
  bytes = String.new(encoding: Encoding::BINARY)
@@ -32,41 +32,34 @@ module Fontisan
32
32
  #
33
33
  # @param tag [String] Table tag
34
34
  # @return [String, nil] Transformed table data
35
+ # Per-tag transform method dispatch.
36
+ TRANSFORM_DISPATCH = {
37
+ "glyf" => :transform_glyf,
38
+ "loca" => :transform_loca,
39
+ "hmtx" => :transform_hmtx,
40
+ }.freeze
41
+
42
+ # Per-tag transform version constant.
43
+ TRANSFORM_VERSIONS = {
44
+ "glyf" => Directory::TRANSFORM_GLYF_LOCA,
45
+ "loca" => Directory::TRANSFORM_GLYF_LOCA,
46
+ "hmtx" => Directory::TRANSFORM_HMTX,
47
+ }.freeze
48
+
35
49
  def transform_table(tag)
36
- case tag
37
- when "glyf"
38
- transform_glyf
39
- when "loca"
40
- transform_loca
41
- when "hmtx"
42
- transform_hmtx
43
- else
44
- # No transformation, return original data
45
- get_table_data(tag)
46
- end
50
+ transform_method = TRANSFORM_DISPATCH[tag]
51
+ return method(transform_method).call if transform_method
52
+
53
+ # No transformation, return original data
54
+ get_table_data(tag)
47
55
  end
48
56
 
49
- # Check if a table can be transformed
50
- #
51
- # @param tag [String] Table tag
52
- # @return [Boolean] True if table supports transformation
53
57
  def transformable?(tag)
54
- %w[glyf loca hmtx].include?(tag)
58
+ TRANSFORM_DISPATCH.key?(tag)
55
59
  end
56
60
 
57
- # Determine transformation version for a table
58
- #
59
- # @param tag [String] Table tag
60
- # @return [Integer] Transformation version
61
61
  def transformation_version(tag)
62
- case tag
63
- when "glyf", "loca"
64
- Directory::TRANSFORM_GLYF_LOCA
65
- when "hmtx"
66
- Directory::TRANSFORM_HMTX
67
- else
68
- Directory::TRANSFORM_NONE
69
- end
62
+ TRANSFORM_VERSIONS.fetch(tag, Directory::TRANSFORM_NONE)
70
63
  end
71
64
 
72
65
  private
@@ -150,7 +143,7 @@ module Fontisan
150
143
  # @param tag [String] Table tag
151
144
  # @return [String, nil] Table data or nil if not found
152
145
  def get_table_data(tag)
153
- return nil unless font.respond_to?(:table_data)
146
+ return nil unless font.is_a?(SfntSource)
154
147
 
155
148
  font.table_data[tag]
156
149
  end