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
@@ -5,44 +5,46 @@ require "json"
5
5
 
6
6
  module Fontisan
7
7
  module Export
8
- # TableSerializer handles serialization of individual font tables
8
+ # Serializes individual font tables for export.
9
9
  #
10
- # Uses strategy pattern to serialize different table types:
11
- # - Fully parsed tables: Use Lutaml::Model serialization
12
- # - Binary tables: Encode as hex or base64
13
- # - Special tables: Custom serialization logic
10
+ # Tables fall into three categories:
11
+ # - Fully parsed: BinData records whose `snapshot` hash is exported
12
+ # as JSON fields.
13
+ # - Binary-only: opaque blobs encoded as hex or base64.
14
+ # - Mixed: tables with a small typed summary alongside the binary
15
+ # payload (glyf, loca, cmap, CFF).
14
16
  #
15
- # @example Serializing a parsed table
16
- # serializer = TableSerializer.new(binary_format: :hex)
17
- # data = serializer.serialize(head_table, "head")
18
- #
19
- # @example Serializing binary table
20
- # data = serializer.serialize_binary(raw_data, "DSIG")
17
+ # All table objects are BinData::Record subclasses; we trust that
18
+ # interface rather than duck-typing each field.
21
19
  class TableSerializer
22
- # Tables that have full Lutaml::Model parsing support
20
+ # Tables whose BinData fields are exported as JSON.
23
21
  FULLY_PARSED_TABLES = %w[
24
22
  head hhea maxp post OS/2 name
25
23
  fvar HVAR VVAR MVAR cvar gvar
26
24
  ].freeze
27
25
 
28
- # Tables that should be stored as binary
26
+ # Tables stored as opaque binary with no field extraction.
29
27
  BINARY_ONLY_TABLES = %w[
30
28
  cvt fpgm prep gasp DSIG GDEF GPOS GSUB
31
29
  ].freeze
32
30
 
33
- # Initialize table serializer
34
- #
35
- # @param binary_format [Symbol] Format for binary data (:hex or :base64)
31
+ # Tables that ship a summary alongside the raw binary.
32
+ MIXED_SUMMARIES = {
33
+ "glyf" => GlyphSummary,
34
+ "loca" => LocaSummary,
35
+ "cmap" => CmapSummary,
36
+ "CFF" => CffSummary,
37
+ }.freeze
38
+
39
+ # @param binary_format [Symbol] :hex or :base64
36
40
  def initialize(binary_format: :hex)
37
41
  @binary_format = binary_format
38
42
  validate_binary_format!
39
43
  end
40
44
 
41
- # Serialize a table to exportable format
42
- #
43
- # @param table [Object] The table object
44
- # @param tag [String] The table tag
45
- # @return [Hash] Serialized table data
45
+ # @param table [BinData::Record] the table object
46
+ # @param tag [String] table tag
47
+ # @return [Hash] serialized payload
46
48
  def serialize(table, tag)
47
49
  if fully_parsed?(tag)
48
50
  serialize_parsed(table, tag)
@@ -53,185 +55,41 @@ module Fontisan
53
55
  end
54
56
  end
55
57
 
56
- # Serialize a parsed table
57
- #
58
- # @param table [Object] The table object with Lutaml::Model
59
- # @param tag [String] The table tag
60
- # @return [Hash] Serialized data with parsed flag
61
- def serialize_parsed(table, tag)
62
- fields = extract_fields(table)
63
- {
64
- tag: tag,
65
- parsed: true,
66
- fields: fields.to_json,
67
- data: nil,
68
- }
69
- end
70
-
71
- # Serialize a binary-only table
72
- #
73
- # @param data [String] Binary data
74
- # @param tag [String] The table tag
75
- # @return [Hash] Serialized data with binary content
58
+ # @param data [String] binary bytes
59
+ # @param tag [String] table tag
60
+ # @return [Hash]
76
61
  def serialize_binary(data, tag)
77
- encoded = encode_binary(data)
78
- {
79
- tag: tag,
80
- parsed: false,
81
- data: encoded,
82
- fields: nil,
83
- }
84
- end
85
-
86
- # Serialize tables with mixed content (summary + binary)
87
- #
88
- # @param table [Object] The table object
89
- # @param tag [String] The table tag
90
- # @return [Hash] Serialized data with both fields and binary
91
- def serialize_mixed(table, tag)
92
- summary = create_summary(table, tag)
93
- binary = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
94
-
95
- {
96
- tag: tag,
97
- parsed: true,
98
- fields: summary.to_json,
99
- data: encode_binary(binary),
100
- }
62
+ { tag: tag, parsed: false, data: encode_binary(data), fields: nil }
101
63
  end
102
64
 
103
65
  private
104
66
 
105
- # Check if table is fully parsed
106
- #
107
- # @param tag [String] Table tag
108
- # @return [Boolean]
109
67
  def fully_parsed?(tag)
110
68
  FULLY_PARSED_TABLES.include?(tag)
111
69
  end
112
70
 
113
- # Check if table is binary-only
114
- #
115
- # @param tag [String] Table tag
116
- # @return [Boolean]
117
71
  def binary_only?(tag)
118
72
  BINARY_ONLY_TABLES.include?(tag)
119
73
  end
120
74
 
121
- # Extract fields from a parsed table
122
- #
123
- # @param table [Object] The table object
124
- # @return [Hash] Field names and values
125
- def extract_fields(table)
126
- fields = {}
127
-
128
- # Get all instance variables
129
- table.instance_variables.each do |var|
130
- name = var.to_s.delete("@")
131
- value = table.instance_variable_get(var)
132
- fields[name] = serialize_value(value)
133
- end
134
-
135
- fields
136
- end
137
-
138
- # Serialize individual field value
139
- #
140
- # @param value [Object] The value to serialize
141
- # @return [Object] Serialized value
142
- def serialize_value(value)
143
- case value
144
- when Integer, Float, String, TrueClass, FalseClass, NilClass
145
- value
146
- when Array
147
- value.map { |v| serialize_value(v) }
148
- when Hash
149
- value.transform_values { |v| serialize_value(v) }
150
- when Time
151
- value.iso8601
152
- else
153
- # For complex objects, try to extract fields
154
- if value.respond_to?(:instance_variables)
155
- extract_fields(value)
156
- else
157
- value.to_s
158
- end
159
- end
160
- end
161
-
162
- # Create summary for mixed-content tables
163
- #
164
- # @param table [Object] The table object
165
- # @param tag [String] Table tag
166
- # @return [Hash] Summary information
167
- def create_summary(table, tag)
168
- case tag
169
- when "glyf"
170
- create_glyf_summary(table)
171
- when "loca"
172
- create_loca_summary(table)
173
- when "cmap"
174
- create_cmap_summary(table)
175
- when "CFF"
176
- create_cff_summary(table)
177
- else
178
- { type: "binary", size: table.to_binary_s.bytesize }
179
- end
180
- end
181
-
182
- # Create glyf table summary
183
- #
184
- # @param table [Object] glyf table
185
- # @return [Hash] Summary
186
- def create_glyf_summary(table)
187
- {
188
- type: "glyf",
189
- num_glyphs: table.respond_to?(:glyphs) ? table.glyphs.length : 0,
190
- note: "Outline data stored as binary",
191
- }
75
+ def serialize_parsed(table, tag)
76
+ { tag: tag, parsed: true, fields: table.snapshot.to_json, data: nil }
192
77
  end
193
78
 
194
- # Create loca table summary
195
- #
196
- # @param table [Object] loca table
197
- # @return [Hash] Summary
198
- def create_loca_summary(table)
199
- {
200
- type: "loca",
201
- num_offsets: table.respond_to?(:offsets) ? table.offsets.length : 0,
202
- format: table.respond_to?(:format) ? table.format : nil,
203
- }
79
+ def serialize_mixed(table, tag)
80
+ summary = summary_for(table, tag)
81
+ { tag: tag, parsed: true,
82
+ fields: summary.to_json,
83
+ data: encode_binary(table.to_binary_s) }
204
84
  end
205
85
 
206
- # Create cmap table summary
207
- #
208
- # @param table [Object] cmap table
209
- # @return [Hash] Summary
210
- def create_cmap_summary(table)
211
- {
212
- type: "cmap",
213
- version: table.respond_to?(:version) ? table.version : 0,
214
- note: "Character mappings stored as binary",
215
- }
216
- end
86
+ def summary_for(table, tag)
87
+ builder = MIXED_SUMMARIES[tag]
88
+ return { type: "binary", size: table.to_binary_s.bytesize } unless builder
217
89
 
218
- # Create CFF table summary
219
- #
220
- # @param table [Object] CFF table
221
- # @return [Hash] Summary
222
- def create_cff_summary(_table)
223
- {
224
- type: "CFF",
225
- note: "CharString data stored as binary",
226
- }
90
+ builder.call(table)
227
91
  end
228
92
 
229
- public
230
-
231
- # Encode binary data based on format
232
- #
233
- # @param data [String] Binary data
234
- # @return [String] Encoded data
235
93
  def encode_binary(data)
236
94
  case @binary_format
237
95
  when :hex
@@ -241,16 +99,43 @@ module Fontisan
241
99
  end
242
100
  end
243
101
 
244
- # Validate binary format option
245
- #
246
- # @raise [ArgumentError] if format is invalid
247
102
  def validate_binary_format!
248
- valid_formats = %i[hex base64]
249
- return if valid_formats.include?(@binary_format)
103
+ valid = %i[hex base64]
104
+ return if valid.include?(@binary_format)
250
105
 
251
106
  raise ArgumentError,
252
107
  "Invalid binary format: #{@binary_format}. " \
253
- "Must be one of: #{valid_formats.join(', ')}"
108
+ "Must be one of: #{valid.join(', ')}"
109
+ end
110
+
111
+ # Summary builders for mixed-content tables. Each is a callable
112
+ # that receives the typed table and returns a JSON-able Hash.
113
+
114
+ module GlyphSummary
115
+ def self.call(table)
116
+ { type: "glyf", num_glyphs: table.glyphs.length,
117
+ note: "Outline data stored as binary" }
118
+ end
119
+ end
120
+
121
+ module LocaSummary
122
+ def self.call(table)
123
+ { type: "loca", num_offsets: table.offsets.length,
124
+ format: table.format }
125
+ end
126
+ end
127
+
128
+ module CmapSummary
129
+ def self.call(table)
130
+ { type: "cmap", version: table.version,
131
+ note: "Character mappings stored as binary" }
132
+ end
133
+ end
134
+
135
+ module CffSummary
136
+ def self.call(_table)
137
+ { type: "CFF", note: "CharString data stored as binary" }
138
+ end
254
139
  end
255
140
  end
256
141
  end
@@ -63,26 +63,26 @@ module Fontisan
63
63
 
64
64
  # Transform individual table
65
65
  #
66
- # @param ttx [Models::Ttx::TtFont] TTX model being built
67
- # @param tag [String] Table tag
68
- # @return [void]
66
+ public
67
+
68
+ # Per-tag TTX model transformer dispatch.
69
+ TABLE_TRANSFORMERS = {
70
+ "head" => [HeadTransformer, :head_table=],
71
+ "hhea" => [HheaTransformer, :hhea_table=],
72
+ "maxp" => [MaxpTransformer, :maxp_table=],
73
+ "name" => [NameTransformer, :name_table=],
74
+ "OS/2" => [Os2Transformer, :os2_table=],
75
+ "post" => [PostTransformer, :post_table=],
76
+ }.freeze
77
+
69
78
  def transform_table(ttx, tag)
70
79
  table = @font.table(tag)
71
80
  return unless table
72
81
 
73
- case tag
74
- when "head"
75
- ttx.head_table = HeadTransformer.transform(table)
76
- when "hhea"
77
- ttx.hhea_table = HheaTransformer.transform(table)
78
- when "maxp"
79
- ttx.maxp_table = MaxpTransformer.transform(table)
80
- when "name"
81
- ttx.name_table = NameTransformer.transform(table)
82
- when "OS/2"
83
- ttx.os2_table = Os2Transformer.transform(table)
84
- when "post"
85
- ttx.post_table = PostTransformer.transform(table)
82
+ entry = TABLE_TRANSFORMERS[tag]
83
+ if entry
84
+ transformer, setter = entry
85
+ ttx.public_send(setter, transformer.transform(table))
86
86
  else
87
87
  # Fallback to binary table
88
88
  binary_table = transform_binary_table(tag, table)
@@ -103,7 +103,7 @@ module Fontisan
103
103
  # @param table [Object] Table object
104
104
  # @return [Models::Ttx::Tables::BinaryTable, nil] Binary table model
105
105
  def transform_binary_table(tag, table)
106
- binary_data = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
106
+ binary_data = table ? table.to_binary_s : ""
107
107
  return nil if binary_data.empty?
108
108
 
109
109
  Models::Ttx::Tables::BinaryTable.new.tap do |bin_table|
@@ -140,7 +140,7 @@ module Fontisan
140
140
  # @return [String] Glyph name
141
141
  def get_glyph_name(glyph_id)
142
142
  post = @font.table("post")
143
- if post.respond_to?(:glyph_names) && post.glyph_names
143
+ if post.is_a?(Tables::Post) && post.glyph_names
144
144
  post.glyph_names[glyph_id] || ".notdef"
145
145
  elsif glyph_id.zero?
146
146
  ".notdef"
@@ -42,7 +42,7 @@ module Fontisan
42
42
  # @param value [Object] BinData value or integer
43
43
  # @return [Integer] Native integer
44
44
  def self.to_int(value)
45
- value.respond_to?(:to_i) ? value.to_i : value
45
+ Integer(value)
46
46
  end
47
47
 
48
48
  # Format fixed-point number (16.16)
@@ -41,7 +41,7 @@ module Fontisan
41
41
  # @param value [Object] BinData value or integer
42
42
  # @return [Integer] Native integer
43
43
  def self.to_int(value)
44
- value.respond_to?(:to_i) ? value.to_i : value
44
+ Integer(value)
45
45
  end
46
46
 
47
47
  # Format hex value
@@ -45,7 +45,7 @@ module Fontisan
45
45
  # @param value [Object] BinData value or integer
46
46
  # @return [Integer] Native integer
47
47
  def self.to_int(value)
48
- value.respond_to?(:to_i) ? value.to_i : value
48
+ Integer(value)
49
49
  end
50
50
 
51
51
  # Format hex value
@@ -44,7 +44,7 @@ module Fontisan
44
44
  # @param value [Object] BinData value or integer
45
45
  # @return [Integer] Native integer
46
46
  def self.to_int(value)
47
- value.respond_to?(:to_i) ? value.to_i : value
47
+ Integer(value)
48
48
  end
49
49
 
50
50
  # Format hex value
@@ -101,7 +101,7 @@ module Fontisan
101
101
  # @param value [Object] BinData value or integer
102
102
  # @return [Integer] Native integer
103
103
  def self.to_int(value)
104
- value.respond_to?(:to_i) ? value.to_i : value
104
+ Integer(value)
105
105
  end
106
106
 
107
107
  # Format binary flags
@@ -33,7 +33,7 @@ module Fontisan
33
33
  # @param value [Object] BinData value or integer
34
34
  # @return [Integer] Native integer
35
35
  def self.to_int(value)
36
- value.respond_to?(:to_i) ? value.to_i : value
36
+ Integer(value)
37
37
  end
38
38
 
39
39
  # Format fixed-point number (16.16)
@@ -64,7 +64,7 @@ module Fontisan
64
64
  # @param value [Object] BinData value or integer
65
65
  # @return [Integer] Native integer
66
66
  def to_int(value)
67
- value.respond_to?(:to_i) ? value.to_i : value
67
+ Integer(value)
68
68
  end
69
69
 
70
70
  # Generate GlyphOrder section (required first)
@@ -84,8 +84,28 @@ module Fontisan
84
84
  # Generate individual table XML
85
85
  #
86
86
  # @param xml [Nokogiri::XML::Builder] XML builder
87
- # @param tag [String] Table tag
88
- # @return [void]
87
+ public
88
+
89
+ # Per-tag emitter dispatch table. Adding a new table's TTX
90
+ # emitter means adding one entry here, not editing generate_table.
91
+ TABLE_EMITTERS = {
92
+ "head" => :generate_head_table,
93
+ "hhea" => :generate_hhea_table,
94
+ "maxp" => :generate_maxp_table,
95
+ "post" => :generate_post_table,
96
+ "name" => :generate_name_table,
97
+ "cmap" => :generate_cmap_table,
98
+ "loca" => :generate_loca_table,
99
+ "glyf" => :generate_glyf_table,
100
+ "CFF" => :generate_cff_table,
101
+ "CFF " => :generate_cff_table,
102
+ "hmtx" => :generate_hmtx_table,
103
+ "fvar" => :generate_fvar_table,
104
+ }.freeze
105
+
106
+ # Variation tags all share the same emitter shape.
107
+ VARIATION_TAGS = %w[gvar cvar HVAR VVAR MVAR].freeze
108
+
89
109
  def generate_table(xml, tag)
90
110
  table = @font.table(tag)
91
111
 
@@ -97,36 +117,15 @@ module Fontisan
97
117
  return
98
118
  end
99
119
 
100
- case tag
101
- when "head"
102
- generate_head_table(xml, table)
103
- when "hhea"
104
- generate_hhea_table(xml, table)
105
- when "maxp"
106
- generate_maxp_table(xml, table)
107
- when "post"
108
- generate_post_table(xml, table)
109
- when "name"
110
- generate_name_table(xml, table)
111
- when "OS/2"
112
- # Skip OS/2 for now - Nokogiri builder can't handle slashes in element names
113
- # TODO: Implement OS/2 table generation with proper XML escaping
120
+ if tag == "OS/2"
114
121
  xml.comment(" OS/2 table skipped - requires special XML handling ")
115
- when "cmap"
116
- generate_cmap_table(xml, table)
117
- when "loca"
118
- generate_loca_table(xml, table)
119
- when "glyf"
120
- generate_glyf_table(xml, table)
121
- when "CFF"
122
- generate_cff_table(xml, table)
123
- when "CFF "
124
- generate_cff_table(xml, table)
125
- when "hmtx"
126
- generate_hmtx_table(xml, table)
127
- when "fvar"
128
- generate_fvar_table(xml, table)
129
- when "gvar", "cvar", "HVAR", "VVAR", "MVAR"
122
+ return
123
+ end
124
+
125
+ emitter = TABLE_EMITTERS[tag]
126
+ if emitter
127
+ method(emitter).call(xml, table)
128
+ elsif VARIATION_TAGS.include?(tag)
130
129
  generate_variation_table(xml, tag, table)
131
130
  else
132
131
  generate_binary_table(xml, tag, table)
@@ -293,8 +292,8 @@ module Fontisan
293
292
  # @param table [Object] Table object
294
293
  # @return [void]
295
294
  def generate_binary_table(xml, tag, table)
296
- binary_data = table.respond_to?(:to_binary_s) ? table.to_binary_s : ""
297
- xml.send(tag.to_sym) do
295
+ binary_data = table.to_binary_s
296
+ xml.public_send(tag.to_sym) do
298
297
  xml.hexdata do
299
298
  xml.text("\n #{format_hex_data(binary_data)}\n ")
300
299
  end
@@ -310,7 +309,7 @@ module Fontisan
310
309
  def generate_binary_table_from_data(xml, tag, data)
311
310
  # Remove trailing space from tag for XML element name
312
311
  clean_tag = tag.strip
313
- xml.send(clean_tag.to_sym) do
312
+ xml.public_send(clean_tag.to_sym) do
314
313
  xml.hexdata do
315
314
  xml.text("\n #{format_hex_data(data)}\n ")
316
315
  end
@@ -432,7 +431,7 @@ module Fontisan
432
431
  # @return [String] Glyph name
433
432
  def get_glyph_name(glyph_id)
434
433
  post = @font.table("post")
435
- if post.respond_to?(:glyph_names) && post.glyph_names
434
+ if post.is_a?(Tables::Post) && post.glyph_names
436
435
  post.glyph_names[glyph_id] || ".notdef"
437
436
  elsif glyph_id.zero?
438
437
  ".notdef"
@@ -471,7 +470,7 @@ module Fontisan
471
470
  # @param width [Integer] Minimum hex width
472
471
  # @return [String] Hex string (e.g., "0x1234")
473
472
  def format_hex(value, width: 8)
474
- int_value = value.respond_to?(:to_i) ? value.to_i : value
473
+ int_value = Integer(value)
475
474
  "0x#{int_value.to_s(16).rjust(width, '0')}"
476
475
  end
477
476
 
@@ -165,7 +165,7 @@ module Fontisan
165
165
  checksum = calculate_table_checksum(data)
166
166
 
167
167
  # Calculate padding to 4-byte boundary
168
- padding_length = Utilities::Padding.boundary(data)
168
+ padding_length = Utilities::Padding.boundary(data.bytesize)
169
169
  padding = "\0" * padding_length
170
170
 
171
171
  entries << {
@@ -352,15 +352,13 @@ module Fontisan
352
352
  next unless glyph&.compound? && glyph.compound?
353
353
 
354
354
  # Add component glyph IDs
355
- if glyph&.components
356
- glyph.components.each do |component|
357
- component_id = component[:glyph_index]
358
- next unless glyph_exists?(component_id)
359
-
360
- unless result.include?(component_id)
361
- result.add(component_id)
362
- to_process << component_id
363
- end
355
+ glyph&.components&.each do |component|
356
+ component_id = component[:glyph_index]
357
+ next unless glyph_exists?(component_id)
358
+
359
+ unless result.include?(component_id)
360
+ result.add(component_id)
361
+ to_process << component_id
364
362
  end
365
363
  end
366
364
  end