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
@@ -0,0 +1,50 @@
1
+ # 27 — Table class registry (OCP: eliminate case/when tag dispatch)
2
+
3
+ ## Priority
4
+ P2
5
+
6
+ ## Problem
7
+ The codebase has 8+ `case tag` dispatches that map a string tag like
8
+ `"HVAR"` to a Ruby class like `Tables::Hvar`. Each new table requires
9
+ updating every dispatch site. This violates OCP and DRY.
10
+
11
+ ## Sites
12
+ - `lib/fontisan/variable/delta_applicator.rb:269` — case tag → table class
13
+ - `lib/fontisan/variable/static_font_builder.rb:91` — case tag → update method
14
+ - `lib/fontisan/variable/static_font_builder.rb:185` — case tag → table class
15
+ - `lib/fontisan/woff2/table_transformer.rb:36` — case tag → transform strategy
16
+ - `lib/fontisan/woff2/table_transformer.rb:62` — case tag → inverse transform
17
+ - `lib/fontisan/export/ttx_generator.rb:100` — case tag → transformer
18
+ - `lib/fontisan/variation/metrics_adjuster.rb:221` — case tag → metric
19
+ - `lib/fontisan/variation/instance_writer.rb:259` — case tag → writer
20
+
21
+ ## Approach
22
+ Create `Tables::Registry` as a single source of truth for tag → class.
23
+ Each table file registers itself via a `register_tag` call:
24
+
25
+ ```ruby
26
+ # lib/fontisan/tables/hvar.rb
27
+ module Fontisan
28
+ module Tables
29
+ class Hvar < Binary::BaseRecord
30
+ register_tag "HVAR"
31
+ ...
32
+ end
33
+ end
34
+ end
35
+ ```
36
+
37
+ `Tables::Registry.for(tag)` returns the class. Dispatch sites become:
38
+
39
+ ```ruby
40
+ table_class = Tables::Registry.for(tag)
41
+ return nil unless table_class
42
+ ```
43
+
44
+ This is OCP-compliant: adding a new table means adding `register_tag`
45
+ in the table's file, not modifying dispatch sites.
46
+
47
+ ## Acceptance criteria
48
+ - 0 `case tag` dispatches in `lib/fontisan/`
49
+ - All callers use `Tables::Registry.for(tag)` instead
50
+ - All existing specs pass
@@ -0,0 +1,40 @@
1
+ # 28 — Spec doubles cleanup
2
+
3
+ ## Priority
4
+ P3
5
+
6
+ ## Problem
7
+ **343 `double()` / `instance_double()` calls remain in specs** despite
8
+ the absolute rule "NEVER use `double()` in specs". Per file:
9
+
10
+ | File | Count |
11
+ |------|-------|
12
+ | spec/fontisan/converters/type1_converter_spec.rb | 58 |
13
+ | spec/fontisan/converters/format_converter_spec.rb | 33 |
14
+ | spec/fontisan/converters/type1_property_spec.rb | 28 |
15
+ | spec/fontisan/tables/cff2/table_builder_spec.rb | 25 |
16
+ | spec/fontisan/variation/validator_spec.rb | 24 |
17
+ | spec/fontisan/variation/subsetter_spec.rb | 17 |
18
+ | spec/fontisan/variation/metrics_adjuster_spec.rb | 15 |
19
+ | spec/fontisan/variation/converter_spec.rb | 14 |
20
+ | spec/fontisan/collection/variable_font_builder_spec.rb | 12 |
21
+ | spec/fontisan/converters/woff2_encoder_spec.rb | 11 |
22
+ | spec/fontisan/collection/table_deduplicator_spec.rb | 9 |
23
+ | spec/fontisan/collection/table_analyzer_spec.rb | 7 |
24
+ | spec/fontisan/tables/cff/cff_glyph_spec.rb | 6 |
25
+ | spec/fontisan/hints/truetype_hint_extractor_spec.rb | 6 |
26
+ | spec/fontisan/commands/unpack_command_spec.rb | 6 |
27
+ | (other files, 5 or fewer each) | ~74 |
28
+
29
+ ## Approach
30
+ Each spec migration replaces `double("Foo", attr: value)` with either:
31
+ 1. A real instance of the doubled class, OR
32
+ 2. A `Struct`-based fake in a uniquely-named module (e.g., `XxxFakes::Foo`).
33
+ 3. The shared `SpecHelpers::FakeFont` where the double is font-shaped.
34
+
35
+ PR #135/#136/#137 migrated ~10 spec files this way. The pattern is
36
+ established; remaining files are mechanical work.
37
+
38
+ ## Acceptance criteria
39
+ - 0 `double()` / `instance_double()` in `spec/fontisan/`
40
+ - All specs pass
@@ -0,0 +1,30 @@
1
+ # 29 — Strategy registries for per-tag dispatch (OCP completion)
2
+
3
+ ## Priority
4
+ P2
5
+
6
+ ## Problem
7
+ 6 remaining `case tag` dispatches map a table tag to per-table
8
+ behavior (update, transform, emit, etc.). These are OCP violations:
9
+ adding a new table's behavior requires editing every dispatch site.
10
+
11
+ ## Sites
12
+ - `export/ttx_generator.rb:100` — 13-way case → generate_X_table
13
+ - `variable/static_font_builder.rb:91` — 4-way case → update_X_table
14
+ - `variation/instance_writer.rb:259` — 5-way case → table class parse
15
+ - `variation/metrics_adjuster.rb:221` — 5-way case → MVAR metric
16
+ - `woff2/table_transformer.rb:36` — 3-way case → transform_X
17
+ - `woff2/table_transformer.rb:62` — 3-way case → transform version
18
+
19
+ ## Approach
20
+ Each case/when becomes a class-level dispatch hash mapping tag →
21
+ method name. The dispatch site reads from the hash. Adding a new
22
+ table means adding one line to the hash in the same file.
23
+
24
+ For dispatches that use `Tables::Registry.for(tag)` to look up a
25
+ class (like instance_writer), extend the Registry to also know how
26
+ to construct from raw data.
27
+
28
+ ## Acceptance criteria
29
+ - 0 `case tag` dispatches in `lib/fontisan/`
30
+ - All specs pass
@@ -23,3 +23,27 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
23
23
  - [x] ~~[07 — Encapsulation violations (instance_variable_get/set, send to private)](07-encapsulation-violations.md)~~ ✓ Done (v0.4.42)
24
24
  - [x] ~~[08 — respond_to? duck typing violations](08-respond-to-violations.md)~~ ✓ Done (v0.4.42)
25
25
  - [x] ~~[11 — Remaining respond_to? violations](11-remaining-respond-to.md)~~ ✓ Done
26
+
27
+ ### P3 — Encapsulation and architecture cleanup (post-#135 audit)
28
+ - [x] ~~[12 — Type 1 generators duck-typing](12-type1-generators-duck-typing.md)~~ ✓ Done (#136)
29
+ - [x] ~~[13 — Hint extractor duck-typing](13-hint-extractor-duck-typing.md)~~ ✓ Done (#136)
30
+ - [x] ~~[14 — Font interface type checks](14-font-interface-type-checks.md)~~ ✓ Done (#136)
31
+ - [x] ~~[15 — Glyph type checks + SimpleGlyph#points bug](15-glyph-type-checks-and-simple-glyph-points-bug.md)~~ ✓ Done (#136)
32
+ - [x] ~~[16 — Variation table checks](16-variation-table-checks.md)~~ ✓ Done (#137)
33
+ - [x] ~~[17 — UFO and Stitcher cleanup](17-ufo-and-stitcher-cleanup.md)~~ ✓ Done (#136)
34
+ - [x] ~~[18 — Generic value and IO checks](18-generic-value-and-io-checks.md)~~ ✓ Done (#137)
35
+ - [x] ~~[19 — Hand-rolled serialization migration to lutaml-model](19-serialization-migration.md)~~ ✓ Done (#137)
36
+
37
+ ### P3 — Final encapsulation cleanup (post-#136 audit)
38
+ - [x] ~~[20 — Variation validator type checks](20-variation-validator-type-checks.md)~~ ✓ Done (#137)
39
+ - [x] ~~[21 — XML builder send() → tag!](21-xml-builder-send.md)~~ ✓ Done (#137)
40
+ - [x] ~~[22 — Type1Converter font_info fields](22-type1-converter-font-info.md)~~ ✓ Done (#137)
41
+ - [x] ~~[23 — Boundary type checks (padding, base_record)](23-boundary-type-checks.md)~~ ✓ Done (#137)
42
+ - [x] ~~[24 — Validator dynamic field_key](24-validator-dynamic-field-key.md)~~ ✓ Done (#137)
43
+ - [x] ~~[25 — Serialization migration to lutaml-model](25-serialization-migration.md)~~ ✓ Done (#137)
44
+ - [x] ~~[26 — Variation preserver spec migration](26-variation-preserver-spec-migration.md)~~ ✓ Done (#137)
45
+
46
+ ### P3 — Architecture polish (post-#137 audit)
47
+ - [x] ~~[27 — Table class registry (OCP: eliminate case/when tag dispatch)](27-table-class-registry.md)~~ ✓ Done (#138)
48
+ - [ ] [28 — Spec doubles cleanup](28-spec-doubles-cleanup.md)
49
+ - [x] ~~[29 — Strategy registries for per-tag dispatch](29-strategy-registries.md)~~ ✓ Done
@@ -22,21 +22,22 @@ module Fontisan
22
22
  class BaseRecord < BinData::Record
23
23
  endian :big # OpenType uses big-endian byte order
24
24
 
25
- # Override read to handle nil data gracefully
25
+ # Override read to handle nil/empty/String inputs gracefully.
26
+ #
27
+ # String inputs are wrapped in a StringIO so the rest of the
28
+ # read path always works against an IO-like object.
29
+ #
30
+ # @param io [String, IO, StringIO, nil] binary data or IO source
26
31
  def self.read(io)
27
- return new if io.nil? || (io.respond_to?(:empty?) && io.empty?)
32
+ return new if io.nil?
33
+
34
+ io = StringIO.new(io) if io.is_a?(String)
35
+ return new if io.is_a?(StringIO) && io.eof?
28
36
 
29
37
  # Store the original data for later parsing
30
- # Convert IO to string if needed
31
- if io.is_a?(String)
32
- data = io
33
- instance = super(StringIO.new(data))
34
- else
35
- # For IO objects, read to string first
36
- data = io.read
37
- io.rewind if io.respond_to?(:rewind)
38
- instance = super(io)
39
- end
38
+ data = io.read
39
+ io.rewind
40
+ instance = super(io)
40
41
 
41
42
  instance.raw_data = data
42
43
  instance
@@ -56,11 +56,9 @@ module Fontisan
56
56
  end
57
57
  raise ArgumentError, "fonts must be an array" unless fonts.is_a?(Array)
58
58
 
59
- unless fonts.all? do |f|
60
- f.respond_to?(:table_data)
61
- end
59
+ unless fonts.all?(SfntSource)
62
60
  raise ArgumentError,
63
- "all fonts must respond to table_data"
61
+ "all fonts must be SfntSource instances (TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font)"
64
62
  end
65
63
 
66
64
  @fonts = fonts
@@ -320,7 +318,7 @@ module Fontisan
320
318
  return [] unless font.has_table?("fvar")
321
319
 
322
320
  fvar_table = font.table("fvar")
323
- return [] unless fvar_table.respond_to?(:axes)
321
+ return [] unless fvar_table&.axes
324
322
 
325
323
  fvar_table.axes.map(&:axis_tag).sort
326
324
  end
@@ -42,8 +42,8 @@ module Fontisan
42
42
  end
43
43
  raise ArgumentError, "fonts must be an array" unless fonts.is_a?(Array)
44
44
 
45
- unless fonts.all? { |f| f.respond_to?(:table_data) }
46
- raise ArgumentError, "all fonts must respond to table_data"
45
+ unless fonts.all?(SfntSource)
46
+ raise ArgumentError, "all fonts must be SfntSource instances"
47
47
  end
48
48
 
49
49
  @fonts = fonts
@@ -150,17 +150,10 @@ module Fontisan
150
150
 
151
151
  # Detect sfnt version from font
152
152
  #
153
- # @param font [TrueTypeFont, OpenTypeFont] Font object
153
+ # @param font [SfntFont] Font object
154
154
  # @return [Integer] sfnt version
155
155
  def detect_sfnt_version(font)
156
- if font.respond_to?(:header) && font.header.respond_to?(:sfnt_version)
157
- font.header.sfnt_version
158
- elsif font.respond_to?(:table_data)
159
- # Auto-detect from tables
160
- FontWriter.detect_sfnt_version(font.table_data)
161
- else
162
- 0x00010000 # Default to TrueType
163
- end
156
+ FontWriter.detect_sfnt_version(font.table_data)
164
157
  end
165
158
 
166
159
  # Build resource fork header (16 bytes)
@@ -213,7 +213,7 @@ module Fontisan
213
213
  binary << table_info[:data]
214
214
 
215
215
  # Pad to 4-byte boundary
216
- padding = Utilities::Padding.boundary(table_info[:data])
216
+ padding = Utilities::Padding.boundary(table_info[:data].bytesize)
217
217
  binary << ("\x00" * padding) if padding.positive?
218
218
  end
219
219
  end
@@ -174,9 +174,9 @@ module Fontisan
174
174
  # @param font [Object] Font object
175
175
  # @return [Boolean]
176
176
  def truetype_font?(font)
177
- return false unless font.respond_to?(:header)
177
+ return false unless font.is_a?(SfntSource)
178
178
 
179
- sfnt = font.header.sfnt_version
179
+ sfnt = font.sfnt_version
180
180
  [0x00010000, 0x74727565].include?(sfnt) # 0x74727565 = 'true'
181
181
  end
182
182
 
@@ -185,9 +185,9 @@ module Fontisan
185
185
  # @param font [Object] Font object
186
186
  # @return [Boolean]
187
187
  def opentype_font?(font)
188
- return false unless font.respond_to?(:header)
188
+ return false unless font.is_a?(SfntSource)
189
189
 
190
- sfnt = font.header.sfnt_version
190
+ sfnt = font.sfnt_version
191
191
  sfnt == 0x4F54544F # 'OTTO'
192
192
  end
193
193
 
@@ -207,7 +207,7 @@ module Fontisan
207
207
  def generate_filename(font, index)
208
208
  # Try to get font name from name table
209
209
  base_name = nil
210
- if font.respond_to?(:table) && font.table("name")
210
+ if font.is_a?(SfntSource) && font.table("name")
211
211
  name_table = font.table("name")
212
212
  # Try to get PostScript name, then family name
213
213
  base_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME) ||
@@ -213,7 +213,7 @@ module Fontisan
213
213
  # @param index [Integer] Font index (fallback)
214
214
  # @return [String] Font name
215
215
  def extract_font_name(font, index)
216
- return "Font #{index}" unless font.respond_to?(:table)
216
+ return "Font #{index}" unless font.is_a?(SfntSource)
217
217
 
218
218
  name_table = font.table("name")
219
219
  return "Font #{index}" unless name_table
@@ -412,9 +412,9 @@ _options)
412
412
  # rather than the SFNT table interface
413
413
  is_type1 = font.is_a?(Type1Font)
414
414
 
415
- unless is_type1 || font.respond_to?(:table)
415
+ unless is_type1 || font.is_a?(SfntSource)
416
416
  raise ArgumentError,
417
- "Font must respond to :table method or be a Type1Font"
417
+ "Font must be an SfntSource instance or a Type1Font"
418
418
  end
419
419
 
420
420
  unless target_format.is_a?(Symbol)
@@ -34,7 +34,7 @@ module Fontisan
34
34
  glyf_data << glyph_data
35
35
 
36
36
  # Add padding to 4-byte boundary
37
- padding = Utilities::Padding.boundary(glyf_data)
37
+ padding = Utilities::Padding.boundary(glyf_data.bytesize)
38
38
  glyf_data << ("\x00" * padding) if padding.positive?
39
39
  end
40
40
 
@@ -275,12 +275,8 @@ module Fontisan
275
275
  def validate(font, target_format)
276
276
  raise ArgumentError, "Font cannot be nil" if font.nil?
277
277
 
278
- unless font.respond_to?(:tables)
279
- raise ArgumentError, "Font must respond to :tables"
280
- end
281
-
282
- unless font.respond_to?(:table)
283
- raise ArgumentError, "Font must respond to :table"
278
+ unless font.is_a?(SfntSource)
279
+ raise ArgumentError, "Font must be an SfntSource instance"
284
280
  end
285
281
 
286
282
  source_format = detect_format(font)
@@ -30,12 +30,8 @@ module Fontisan
30
30
  def convert(font, _options = {})
31
31
  raise ArgumentError, "Font cannot be nil" if font.nil?
32
32
 
33
- unless font.respond_to?(:tables)
34
- raise ArgumentError, "Font must respond to :tables"
35
- end
36
-
37
- unless font.respond_to?(:table_data)
38
- raise ArgumentError, "Font must respond to :table_data"
33
+ unless font.is_a?(SfntSource)
34
+ raise ArgumentError, "Font must be an SfntSource instance"
39
35
  end
40
36
 
41
37
  target_format = detect_format(font)
@@ -73,12 +69,8 @@ module Fontisan
73
69
  def validate(font, target_format)
74
70
  raise ArgumentError, "Font cannot be nil" if font.nil?
75
71
 
76
- unless font.respond_to?(:tables)
77
- raise ArgumentError, "Font must respond to :tables"
78
- end
79
-
80
- unless font.respond_to?(:table_data)
81
- raise ArgumentError, "Font must respond to :table_data"
72
+ unless font.is_a?(SfntSource)
73
+ raise ArgumentError, "Font must be an SfntSource instance"
82
74
  end
83
75
 
84
76
  # Detect source format and verify it matches target
@@ -112,9 +112,9 @@ module Fontisan
112
112
  def validate(font, target_format)
113
113
  raise ArgumentError, "Font cannot be nil" if font.nil?
114
114
 
115
- unless font.respond_to?(:font_dictionary) || font.respond_to?(:tables)
115
+ unless font.is_a?(Type1Font) || font.is_a?(SfntSource)
116
116
  raise ArgumentError,
117
- "Font must be Type1Font or have :tables method"
117
+ "Font must be a Type1Font or SfntSource instance"
118
118
  end
119
119
 
120
120
  source_format = detect_format(font)
@@ -156,10 +156,9 @@ module Fontisan
156
156
  decompose_seac_glyphs(font)
157
157
  end
158
158
 
159
- # Read all font dictionary records if requested
160
- if conv_options.opening_option?(:read_all_records) && font.font_dictionary.respond_to?(:reload)
161
- # Ensure full font dictionary is loaded
162
- font.font_dictionary.reload
159
+ # Read all font dictionary records if requested.
160
+ if conv_options.opening_option?(:read_all_records) && font.font_dictionary
161
+ font.font_dictionary.parse(font.font_dictionary.raw_data)
163
162
  end
164
163
  end
165
164
 
@@ -219,7 +218,7 @@ module Fontisan
219
218
  :otf
220
219
  else
221
220
  # Try to detect from tables
222
- if font.respond_to?(:tables)
221
+ if font.is_a?(SfntSource)
223
222
  if font.tables.key?("glyf")
224
223
  :ttf
225
224
  elsif font.tables.key?("CFF ") || font.tables.key?("CFF2")
@@ -720,37 +719,13 @@ module Fontisan
720
719
 
721
720
  # Extract font names with fallbacks
722
721
  font_name = font.font_name || font_dict&.font_name || "Unnamed"
723
- family_name = if font_info.respond_to?(:family_name)
724
- font_info.family_name || font_dict&.family_name || font_name
725
- else
726
- font_dict&.family_name || font_name
727
- end
728
- full_name = if font_info.respond_to?(:full_name)
729
- font_info.full_name || font_dict&.full_name || family_name
730
- else
731
- font_dict&.full_name || family_name
732
- end
733
- version = if font_info.respond_to?(:version)
734
- font_info.version || font.version || "001.000"
735
- else
736
- font.version || "001.000"
737
- end
738
- copyright = if font_info.respond_to?(:copyright)
739
- font_info.copyright || font_dict&.raw_data&.dig(:copyright) || ""
740
- else
741
- font_dict&.raw_data&.dig(:copyright) || ""
742
- end
722
+ family_name = font_info&.family_name || font_dict&.family_name || font_name
723
+ full_name = font_info&.full_name || font_dict&.full_name || family_name
724
+ version = font_info&.version || font.version || "001.000"
725
+ copyright = font_info&.copyright || font_dict&.raw_data&.dig(:copyright) || ""
743
726
  postscript_name = font_name
744
- weight = if font_info.respond_to?(:weight)
745
- font_info.weight
746
- else
747
- "Regular"
748
- end
749
- notice = if font_info.respond_to?(:notice)
750
- font_info.notice
751
- else
752
- ""
753
- end
727
+ weight = font_info&.weight || "Regular"
728
+ notice = font_info&.notice || ""
754
729
 
755
730
  # Build name records (Windows Unicode, English US)
756
731
  # Platform ID 3 (Windows), Encoding ID 1 (Unicode BMP), Language ID 0x0409 (US English)
@@ -218,7 +218,7 @@ module Fontisan
218
218
  # are applied by `Woff2::EncoderRules`, not here — this method is a
219
219
  # pure reader.
220
220
  def collect_tables(font, _options = {})
221
- table_names = if font.respond_to?(:table_names)
221
+ table_names = if font.is_a?(SfntSource)
222
222
  font.table_names
223
223
  else
224
224
  %w[head hhea maxp OS/2 name cmap post hmtx glyf loca
@@ -247,7 +247,7 @@ module Fontisan
247
247
  #
248
248
  # @return [Hash{Symbol => Object}, nil]
249
249
  def apply_glyf_loca_transform!(table_data, font)
250
- return nil unless font.respond_to?(:has_table?)
250
+ return nil unless font.is_a?(SfntSource)
251
251
  return nil unless font.has_table?("glyf") && font.has_table?("loca")
252
252
 
253
253
  glyf_data = table_data["glyf"]
@@ -310,12 +310,10 @@ module Fontisan
310
310
  end
311
311
 
312
312
  def get_table_data(font, tag)
313
- if font.respond_to?(:table_data)
314
- font.table_data[tag]
315
- elsif font.respond_to?(:table)
316
- table = font.table(tag)
317
- table&.to_binary_s if table.respond_to?(:to_binary_s)
318
- end
313
+ return font.table_data[tag] if font.is_a?(SfntSource)
314
+
315
+ table = font.table(tag)
316
+ table&.to_binary_s
319
317
  end
320
318
 
321
319
  # Build table directory entries.
@@ -418,7 +416,7 @@ module Fontisan
418
416
  size += 16 if glyf_transform # synthetic loca directory entry
419
417
 
420
418
  table_data.each_value do |data|
421
- size += Utilities::Padding.aligned_size(data)
419
+ size += Utilities::Padding.aligned_size(data.bytesize)
422
420
  end
423
421
 
424
422
  if glyf_transform
@@ -506,7 +504,7 @@ module Fontisan
506
504
  # font block MUST be padded with null bytes so the total file
507
505
  # length is a multiple of 4. fontTools' WOFF2Writer does the
508
506
  # same: `pad(directory + compressedFont, size=4)`.
509
- pad = Utilities::Padding.boundary(woff2_data)
507
+ pad = Utilities::Padding.boundary(woff2_data.bytesize)
510
508
  woff2_data << ("\x00" * pad) if pad.positive?
511
509
  update_woff2_length!(woff2_data)
512
510
  woff2_data
@@ -113,8 +113,8 @@ module Fontisan
113
113
 
114
114
  raise ArgumentError, "Font cannot be nil" if font.nil?
115
115
 
116
- unless font.respond_to?(:tables) && font.respond_to?(:table_data)
117
- raise ArgumentError, "Font must respond to :tables and :table_data"
116
+ unless font.is_a?(SfntSource)
117
+ raise ArgumentError, "Font must be an SfntSource instance"
118
118
  end
119
119
  end
120
120
 
@@ -302,7 +302,7 @@ module Fontisan
302
302
  def write_woff_header(io, font, total_size, total_sfnt_size, num_tables,
303
303
  compressed_metadata, metadata_offset, metadata_size,
304
304
  private_offset, private_size)
305
- flavor = if font.respond_to?(:cff?) && font.cff?
305
+ flavor = if font.is_a?(SfntSource) && font.cff?
306
306
  Constants::SFNT_VERSION_OTTO
307
307
  else
308
308
  Constants::SFNT_VERSION_TRUETYPE
@@ -181,7 +181,7 @@ module Fontisan
181
181
  # @return [void]
182
182
  def export_binary_fallback(export_model, tag, error)
183
183
  table = @font.table(tag)
184
- binary_data = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
184
+ binary_data = table ? table.to_binary_s : ""
185
185
  checksum = calculate_table_checksum(tag)
186
186
 
187
187
  export_model.add_table(
@@ -201,14 +201,13 @@ module Fontisan
201
201
  table_entry = @font.tables.find { |entry| entry.tag == tag }
202
202
  return 0 unless table_entry
203
203
 
204
- if table_entry.respond_to?(:checksum)
205
- table_entry.checksum.to_i
206
- else
207
- # Calculate from binary data
208
- table = @font.table(tag)
209
- data = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
210
- Utilities::ChecksumCalculator.calculate(data)
211
- end
204
+ checksum = table_entry.checksum
205
+ return checksum.to_i if checksum
206
+
207
+ # Calculate from binary data
208
+ table = @font.table(tag)
209
+ data = table ? table.to_binary_s : ""
210
+ Utilities::ChecksumCalculator.calculate(data)
212
211
  end
213
212
 
214
213
  # Format checksum as hex string