fontisan 0.2.7 → 0.2.9

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +103 -0
  3. data/.rubocop_todo.yml +65 -361
  4. data/CHANGELOG.md +116 -0
  5. data/Gemfile +1 -1
  6. data/README.adoc +106 -27
  7. data/Rakefile +12 -7
  8. data/benchmark/variation_quick_bench.rb +1 -1
  9. data/docs/APPLE_LEGACY_FONTS.adoc +173 -0
  10. data/docs/COLLECTION_VALIDATION.adoc +143 -0
  11. data/docs/COLOR_FONTS.adoc +127 -0
  12. data/docs/DOCUMENTATION_SUMMARY.md +141 -0
  13. data/docs/FONT_HINTING.adoc +9 -1
  14. data/docs/VALIDATION.adoc +254 -0
  15. data/docs/WOFF_WOFF2_FORMATS.adoc +94 -0
  16. data/lib/fontisan/cli.rb +45 -13
  17. data/lib/fontisan/collection/dfont_builder.rb +2 -1
  18. data/lib/fontisan/commands/convert_command.rb +2 -4
  19. data/lib/fontisan/commands/info_command.rb +3 -3
  20. data/lib/fontisan/commands/pack_command.rb +2 -1
  21. data/lib/fontisan/commands/validate_command.rb +157 -6
  22. data/lib/fontisan/converters/collection_converter.rb +22 -13
  23. data/lib/fontisan/converters/svg_generator.rb +2 -1
  24. data/lib/fontisan/converters/woff2_encoder.rb +6 -6
  25. data/lib/fontisan/converters/woff_writer.rb +3 -1
  26. data/lib/fontisan/font_loader.rb +7 -6
  27. data/lib/fontisan/formatters/text_formatter.rb +18 -14
  28. data/lib/fontisan/hints/hint_converter.rb +1 -1
  29. data/lib/fontisan/hints/hint_validator.rb +13 -10
  30. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
  31. data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
  32. data/lib/fontisan/models/collection_validation_report.rb +104 -0
  33. data/lib/fontisan/models/font_report.rb +24 -0
  34. data/lib/fontisan/models/validation_report.rb +7 -2
  35. data/lib/fontisan/open_type_font.rb +18 -425
  36. data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
  37. data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
  38. data/lib/fontisan/sfnt_font.rb +699 -0
  39. data/lib/fontisan/sfnt_table.rb +264 -0
  40. data/lib/fontisan/subset/glyph_mapping.rb +2 -0
  41. data/lib/fontisan/subset/table_subsetter.rb +2 -2
  42. data/lib/fontisan/tables/cblc.rb +8 -4
  43. data/lib/fontisan/tables/cff/index.rb +2 -0
  44. data/lib/fontisan/tables/cff.rb +6 -3
  45. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
  46. data/lib/fontisan/tables/cff2.rb +1 -1
  47. data/lib/fontisan/tables/cmap.rb +5 -5
  48. data/lib/fontisan/tables/cmap_table.rb +231 -0
  49. data/lib/fontisan/tables/glyf.rb +8 -10
  50. data/lib/fontisan/tables/glyf_table.rb +255 -0
  51. data/lib/fontisan/tables/head.rb +3 -3
  52. data/lib/fontisan/tables/head_table.rb +111 -0
  53. data/lib/fontisan/tables/hhea.rb +4 -4
  54. data/lib/fontisan/tables/hhea_table.rb +255 -0
  55. data/lib/fontisan/tables/hmtx_table.rb +191 -0
  56. data/lib/fontisan/tables/loca_table.rb +212 -0
  57. data/lib/fontisan/tables/maxp.rb +2 -2
  58. data/lib/fontisan/tables/maxp_table.rb +258 -0
  59. data/lib/fontisan/tables/name.rb +1 -1
  60. data/lib/fontisan/tables/name_table.rb +176 -0
  61. data/lib/fontisan/tables/os2.rb +8 -8
  62. data/lib/fontisan/tables/os2_table.rb +329 -0
  63. data/lib/fontisan/tables/post.rb +2 -2
  64. data/lib/fontisan/tables/post_table.rb +183 -0
  65. data/lib/fontisan/tables/sbix.rb +5 -4
  66. data/lib/fontisan/true_type_font.rb +12 -464
  67. data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
  68. data/lib/fontisan/validation/collection_validator.rb +4 -2
  69. data/lib/fontisan/validators/basic_validator.rb +11 -21
  70. data/lib/fontisan/validators/font_book_validator.rb +29 -50
  71. data/lib/fontisan/validators/opentype_validator.rb +24 -28
  72. data/lib/fontisan/validators/validator.rb +87 -66
  73. data/lib/fontisan/validators/web_font_validator.rb +16 -21
  74. data/lib/fontisan/version.rb +1 -1
  75. data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
  76. data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
  77. data/lib/fontisan/woff2/table_transformer.rb +4 -2
  78. data/lib/fontisan/woff2_font.rb +4 -2
  79. data/lib/fontisan/woff_font.rb +46 -30
  80. data/lib/fontisan.rb +2 -2
  81. data/scripts/compare_stack_aware.rb +1 -1
  82. data/scripts/measure_optimization.rb +1 -2
  83. metadata +23 -2
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "font_report"
4
+ require_relative "validation_report"
5
+ require "lutaml/model"
6
+
7
+ module Fontisan
8
+ module Models
9
+ # CollectionValidationReport aggregates validation results for all fonts
10
+ # in a TTC/OTC/dfont collection.
11
+ #
12
+ # Provides collection-level summary statistics and per-font validation
13
+ # details with clear formatting.
14
+ class CollectionValidationReport < Lutaml::Model::Serializable
15
+ attribute :collection_path, :string
16
+ attribute :collection_type, :string
17
+ attribute :num_fonts, :integer
18
+ attribute :font_reports, FontReport, collection: true,
19
+ initialize_empty: true
20
+ attribute :valid, :boolean, default: -> { true }
21
+
22
+ key_value do
23
+ map "collection_path", to: :collection_path
24
+ map "collection_type", to: :collection_type
25
+ map "num_fonts", to: :num_fonts
26
+ map "font_reports", to: :font_reports
27
+ map "valid", to: :valid
28
+ end
29
+
30
+ # Add a font report to the collection
31
+ #
32
+ # @param font_report [FontReport] The font report to add
33
+ # @return [void]
34
+ def add_font_report(font_report)
35
+ font_reports << font_report
36
+ # Mark that we're no longer using the default value
37
+ value_set_for(:font_reports)
38
+ # Update overall validity
39
+ self.valid = valid && font_report.report.valid?
40
+ end
41
+
42
+ # Get overall validation status for the collection
43
+ #
44
+ # @return [String] "valid", "invalid", or "valid_with_warnings"
45
+ def overall_status
46
+ return "invalid" unless font_reports.all? { |fr| fr.report.valid? }
47
+ return "valid_with_warnings" if font_reports.any? do |fr|
48
+ fr.report.has_warnings?
49
+ end
50
+
51
+ "valid"
52
+ end
53
+
54
+ # Generate text summary with collection header and per-font sections
55
+ #
56
+ # @return [String] Formatted validation report
57
+ def text_summary
58
+ lines = []
59
+ lines << "Collection: #{collection_path}"
60
+ lines << "Type: #{collection_type}"
61
+ lines << "Fonts: #{num_fonts}"
62
+ lines << ""
63
+ lines << "Summary:"
64
+ lines << " Total Errors: #{total_errors}"
65
+ lines << " Total Warnings: #{total_warnings}"
66
+ lines << " Total Info: #{total_info}"
67
+
68
+ if font_reports.any?
69
+ lines << ""
70
+ font_reports.each do |font_report|
71
+ lines << "=== Font #{font_report.font_index}: #{font_report.font_name} ==="
72
+ # Indent each line of the font's report
73
+ font_lines = font_report.report.text_summary.split("\n")
74
+ lines.concat(font_lines)
75
+ lines << "" unless font_report == font_reports.last
76
+ end
77
+ end
78
+
79
+ lines.join("\n")
80
+ end
81
+
82
+ # Calculate total errors across all fonts
83
+ #
84
+ # @return [Integer] Total error count
85
+ def total_errors
86
+ font_reports.sum { |fr| fr.report.summary.errors }
87
+ end
88
+
89
+ # Calculate total warnings across all fonts
90
+ #
91
+ # @return [Integer] Total warning count
92
+ def total_warnings
93
+ font_reports.sum { |fr| fr.report.summary.warnings }
94
+ end
95
+
96
+ # Calculate total info messages across all fonts
97
+ #
98
+ # @return [Integer] Total info count
99
+ def total_info
100
+ font_reports.sum { |fr| fr.report.summary.info }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validation_report"
4
+ require "lutaml/model"
5
+
6
+ module Fontisan
7
+ module Models
8
+ # FontReport wraps a single font's validation report with collection context
9
+ #
10
+ # Used within CollectionValidationReport to associate validation results
11
+ # with a specific font index and name in the collection.
12
+ class FontReport < Lutaml::Model::Serializable
13
+ attribute :font_index, :integer
14
+ attribute :font_name, :string
15
+ attribute :report, ValidationReport
16
+
17
+ key_value do
18
+ map "font_index", to: :font_index
19
+ map "font_name", to: :font_name
20
+ map "report", to: :report
21
+ end
22
+ end
23
+ end
24
+ end
@@ -101,7 +101,9 @@ module Fontisan
101
101
  attribute :status, :string
102
102
  attribute :use_case, :string
103
103
  attribute :checks_performed, :string, collection: true, default: -> { [] }
104
- attribute :check_results, CheckResult, collection: true, default: -> { [] }
104
+ attribute :check_results, CheckResult, collection: true, default: -> {
105
+ []
106
+ }
105
107
 
106
108
  yaml do
107
109
  map "font_path", to: :font_path
@@ -340,7 +342,9 @@ module Fontisan
340
342
  # @param field_name [String, Symbol] Field name
341
343
  # @return [Array<CheckResult>] Array of check results for the field
342
344
  def field_issues(table_tag, field_name)
343
- check_results.select { |cr| cr.table == table_tag.to_s && cr.field == field_name.to_s }
345
+ check_results.select do |cr|
346
+ cr.table == table_tag.to_s && cr.field == field_name.to_s
347
+ end
344
348
  end
345
349
 
346
350
  # Check filtering methods
@@ -374,6 +378,7 @@ module Fontisan
374
378
  # @return [Float] Failure rate (0.0 to 1.0)
375
379
  def failure_rate
376
380
  return 0.0 if check_results.empty?
381
+
377
382
  failed_checks.length.to_f / check_results.length
378
383
  end
379
384
 
@@ -1,15 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bindata"
4
- require_relative "constants"
5
- require_relative "loading_modes"
6
- require_relative "utilities/checksum_calculator"
3
+ require_relative "sfnt_font"
7
4
 
8
5
  module Fontisan
9
- # OpenType Font domain object using BinData
6
+ # OpenType Font domain object
10
7
  #
11
- # Represents a complete OpenType Font file (CFF outlines) using BinData's declarative
12
- # DSL for binary structure definition. Parallel to TrueTypeFont but for CFF format.
8
+ # Represents an OpenType Font file (CFF outlines). Inherits all shared
9
+ # SFNT functionality from SfntFont and adds OpenType-specific behavior
10
+ # including page-aligned lazy loading for optimal performance.
13
11
  #
14
12
  # @example Reading and analyzing a font
15
13
  # otf = OpenTypeFont.from_file("font.otf")
@@ -24,29 +22,10 @@ module Fontisan
24
22
  #
25
23
  # @example Writing a font
26
24
  # otf.to_file("output.otf")
27
- class OpenTypeFont < BinData::Record
28
- endian :big
29
-
30
- offset_table :header
31
- array :tables, type: :table_directory, initial_length: lambda {
32
- header.num_tables
33
- }
34
-
35
- # Table data is stored separately since it's at variable offsets
36
- attr_accessor :table_data
37
-
38
- # Parsed table instances cache
39
- attr_accessor :parsed_tables
40
-
41
- # Loading mode for this font (:metadata or :full)
42
- attr_accessor :loading_mode
43
-
44
- # IO source for lazy loading
45
- attr_accessor :io_source
46
-
47
- # Whether lazy loading is enabled
48
- attr_accessor :lazy_load_enabled
49
-
25
+ #
26
+ # @example Reading from TTC collection
27
+ # otf = OpenTypeFont.from_collection(io, offset)
28
+ class OpenTypeFont < SfntFont
50
29
  # Page cache for lazy loading (maps page_start_offset => page_data)
51
30
  attr_accessor :page_cache
52
31
 
@@ -57,7 +36,7 @@ module Fontisan
57
36
  #
58
37
  # @param path [String] Path to the OTF file
59
38
  # @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
60
- # @param lazy [Boolean] If true, load tables on demand (default: false for eager loading)
39
+ # @param lazy [Boolean] If true, load tables on demand (default: false)
61
40
  # @return [OpenTypeFont] A new instance
62
41
  # @raise [ArgumentError] if path is nil or empty, or if mode is invalid
63
42
  # @raise [Errno::ENOENT] if file does not exist
@@ -93,167 +72,23 @@ module Fontisan
93
72
  raise "Invalid OTF file: #{e.message}"
94
73
  end
95
74
 
96
- # Read OpenType Font from collection at specific offset
97
- #
98
- # @param io [IO] Open file handle
99
- # @param offset [Integer] Byte offset to the font
100
- # @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
101
- # @return [OpenTypeFont] A new instance
102
- def self.from_collection(io, offset, mode: LoadingModes::FULL)
103
- LoadingModes.validate_mode!(mode)
104
-
105
- io.seek(offset)
106
- font = read(io)
107
- font.initialize_storage
108
- font.loading_mode = mode
109
- font.read_table_data(io)
110
- font
111
- end
112
-
113
75
  # Initialize storage hashes
114
76
  #
77
+ # Extends base class to add page_cache for lazy loading.
78
+ #
115
79
  # @return [void]
116
80
  def initialize_storage
117
- @table_data = {}
118
- @parsed_tables = {}
119
- @loading_mode = LoadingModes::FULL
120
- @lazy_load_enabled = false
121
- @io_source = nil
81
+ super
122
82
  @page_cache = {}
123
83
  end
124
84
 
125
- # Read table data for all tables
126
- #
127
- # In metadata mode, only reads metadata tables. In full mode, reads all tables.
128
- # In lazy load mode, doesn't read data upfront.
129
- #
130
- # @param io [IO] Open file handle
131
- # @return [void]
132
- def read_table_data(io)
133
- @table_data = {}
134
-
135
- if @lazy_load_enabled
136
- # Don't read data, just keep IO reference
137
- @io_source = io
138
- return
139
- end
140
-
141
- if @loading_mode == LoadingModes::METADATA
142
- # Only read metadata tables for performance
143
- # Use page-aware batched reading to maximize filesystem prefetching
144
- read_metadata_tables_batched(io)
145
- else
146
- # Read all tables
147
- tables.each do |entry|
148
- io.seek(entry.offset)
149
- # Force UTF-8 encoding on tag for hash key consistency
150
- tag_key = entry.tag.dup.force_encoding("UTF-8")
151
- @table_data[tag_key] = io.read(entry.table_length)
152
- end
153
- end
154
- end
155
-
156
- # Read metadata tables using page-aware batching
157
- #
158
- # Groups adjacent tables within page boundaries and reads them together
159
- # to maximize filesystem prefetching and minimize random seeks.
160
- #
161
- # @param io [IO] Open file handle
162
- # @return [void]
163
- def read_metadata_tables_batched(io)
164
- # Typical filesystem page size (4KB is common, but 8KB gives better prefetch window)
165
- page_threshold = 8192
166
-
167
- # Get metadata tables sorted by offset for sequential access
168
- metadata_entries = tables.select { |entry| LoadingModes::METADATA_TABLES_SET.include?(entry.tag) }
169
- metadata_entries.sort_by!(&:offset)
170
-
171
- return if metadata_entries.empty?
172
-
173
- # Group adjacent tables within page threshold for batched reading
174
- i = 0
175
- while i < metadata_entries.size
176
- batch_start = metadata_entries[i]
177
- batch_end = batch_start
178
- batch_entries = [batch_start]
179
-
180
- # Extend batch while next table is within page threshold
181
- j = i + 1
182
- while j < metadata_entries.size
183
- next_entry = metadata_entries[j]
184
- gap = next_entry.offset - (batch_end.offset + batch_end.table_length)
185
-
186
- # If gap is small (within page threshold), include in batch
187
- if gap <= page_threshold
188
- batch_end = next_entry
189
- batch_entries << next_entry
190
- j += 1
191
- else
192
- break
193
- end
194
- end
195
-
196
- # Read batch
197
- if batch_entries.size == 1
198
- # Single table, read normally
199
- io.seek(batch_start.offset)
200
- tag_key = batch_start.tag.dup.force_encoding("UTF-8")
201
- @table_data[tag_key] = io.read(batch_start.table_length)
202
- else
203
- # Multiple tables, read contiguous segment
204
- batch_offset = batch_start.offset
205
- batch_length = (batch_end.offset + batch_end.table_length) - batch_start.offset
206
-
207
- io.seek(batch_offset)
208
- batch_data = io.read(batch_length)
209
-
210
- # Extract individual tables from batch
211
- batch_entries.each do |entry|
212
- relative_offset = entry.offset - batch_offset
213
- tag_key = entry.tag.dup.force_encoding("UTF-8")
214
- @table_data[tag_key] =
215
- batch_data[relative_offset, entry.table_length]
216
- end
217
- end
218
-
219
- i = j
220
- end
221
- end
222
-
223
- # Write OpenType Font to a file
224
- #
225
- # Writes the complete OTF structure to disk, including proper checksum
226
- # calculation and table alignment.
227
- #
228
- # @param path [String] Path where the OTF file will be written
229
- # @return [Integer] Number of bytes written
230
- # @raise [IOError] if writing fails
231
- def to_file(path)
232
- File.open(path, "wb") do |io|
233
- # Write header and tables (directory)
234
- write_structure(io)
235
-
236
- # Write table data with updated offsets
237
- write_table_data_with_offsets(io)
238
-
239
- io.pos
240
- end
241
-
242
- # Update checksum adjustment in head table
243
- update_checksum_adjustment_in_file(path) if head_table
244
-
245
- File.size(path)
246
- end
247
-
248
85
  # Validate format correctness
249
86
  #
87
+ # Extends base class to check for CFF table (OpenType-specific).
88
+ #
250
89
  # @return [Boolean] true if the OTF format is valid, false otherwise
251
90
  def valid?
252
- return false unless header
253
- return false unless tables.respond_to?(:length)
254
- return false unless @table_data.is_a?(Hash)
255
- return false if tables.length != header.num_tables
256
- return false unless head_table
91
+ return false unless super
257
92
  return false unless has_table?(Constants::CFF_TAG)
258
93
 
259
94
  true
@@ -273,158 +108,6 @@ module Fontisan
273
108
  true
274
109
  end
275
110
 
276
- # Check if font has a specific table
277
- #
278
- # @param tag [String] The table tag to check for
279
- # @return [Boolean] true if table exists, false otherwise
280
- def has_table?(tag)
281
- tables.any? { |entry| entry.tag == tag }
282
- end
283
-
284
- # Check if a table is available in the current loading mode
285
- #
286
- # @param tag [String] The table tag to check
287
- # @return [Boolean] true if table is available in current mode
288
- def table_available?(tag)
289
- return false unless has_table?(tag)
290
-
291
- LoadingModes.table_allowed?(@loading_mode, tag)
292
- end
293
-
294
- # Find a table entry by tag
295
- #
296
- # @param tag [String] The table tag to find
297
- # @return [TableDirectory, nil] The table entry or nil
298
- def find_table_entry(tag)
299
- tables.find { |entry| entry.tag == tag }
300
- end
301
-
302
- # Get the head table entry
303
- #
304
- # @return [TableDirectory, nil] The head table entry or nil
305
- def head_table
306
- find_table_entry(Constants::HEAD_TAG)
307
- end
308
-
309
- # Get list of all table tags
310
- #
311
- # @return [Array<String>] Array of table tag strings
312
- def table_names
313
- tables.map(&:tag)
314
- end
315
-
316
- # Get parsed table instance
317
- #
318
- # This method parses the raw table data into a structured table object
319
- # and caches the result for subsequent calls. Enforces mode restrictions.
320
- #
321
- # @param tag [String] The table tag to retrieve
322
- # @return [Tables::*, nil] Parsed table object or nil if not found
323
- # @raise [ArgumentError] if table is not available in current loading mode
324
- def table(tag)
325
- # Check mode restrictions
326
- unless table_available?(tag)
327
- if has_table?(tag)
328
- raise ArgumentError,
329
- "Table '#{tag}' is not available in #{@loading_mode} mode. " \
330
- "Available tables: #{LoadingModes.tables_for(@loading_mode).inspect}"
331
- else
332
- return nil
333
- end
334
- end
335
-
336
- return @parsed_tables[tag] if @parsed_tables.key?(tag)
337
-
338
- # Lazy load table data if enabled
339
- if @lazy_load_enabled && !@table_data.key?(tag)
340
- load_table_data(tag)
341
- end
342
-
343
- @parsed_tables[tag] ||= parse_table(tag)
344
- end
345
-
346
- # Get units per em from head table
347
- #
348
- # @return [Integer, nil] Units per em value
349
- def units_per_em
350
- head = table(Constants::HEAD_TAG)
351
- head&.units_per_em
352
- end
353
-
354
- # Convenience methods for accessing common name table fields
355
- # These are particularly useful in minimal mode
356
-
357
- # Get font family name
358
- #
359
- # @return [String, nil] Family name or nil if not found
360
- def family_name
361
- name_table = table(Constants::NAME_TAG)
362
- name_table&.english_name(Tables::Name::FAMILY)
363
- end
364
-
365
- # Get font subfamily name (e.g., Regular, Bold, Italic)
366
- #
367
- # @return [String, nil] Subfamily name or nil if not found
368
- def subfamily_name
369
- name_table = table(Constants::NAME_TAG)
370
- name_table&.english_name(Tables::Name::SUBFAMILY)
371
- end
372
-
373
- # Get full font name
374
- #
375
- # @return [String, nil] Full name or nil if not found
376
- def full_name
377
- name_table = table(Constants::NAME_TAG)
378
- name_table&.english_name(Tables::Name::FULL_NAME)
379
- end
380
-
381
- # Get PostScript name
382
- #
383
- # @return [String, nil] PostScript name or nil if not found
384
- def post_script_name
385
- name_table = table(Constants::NAME_TAG)
386
- name_table&.english_name(Tables::Name::POSTSCRIPT_NAME)
387
- end
388
-
389
- # Get preferred family name
390
- #
391
- # @return [String, nil] Preferred family name or nil if not found
392
- def preferred_family_name
393
- name_table = table(Constants::NAME_TAG)
394
- name_table&.english_name(Tables::Name::PREFERRED_FAMILY)
395
- end
396
-
397
- # Get preferred subfamily name
398
- #
399
- # @return [String, nil] Preferred subfamily name or nil if not found
400
- def preferred_subfamily_name
401
- name_table = table(Constants::NAME_TAG)
402
- name_table&.english_name(Tables::Name::PREFERRED_SUBFAMILY)
403
- end
404
-
405
- # Close the IO source (for lazy loading)
406
- #
407
- # @return [void]
408
- def close
409
- @io_source&.close
410
- @io_source = nil
411
- end
412
-
413
- # Setup finalizer for cleanup
414
- #
415
- # @return [void]
416
- def setup_finalizer
417
- ObjectSpace.define_finalizer(self, self.class.finalize(@io_source))
418
- end
419
-
420
- # Finalizer proc for closing IO
421
- #
422
- # @param io [IO] The IO object to close
423
- # @return [Proc] The finalizer proc
424
- def self.finalize(io)
425
- proc { io&.close }
426
- end
427
-
428
111
  private
429
112
 
430
113
  # Load a single table's data on demand
@@ -478,22 +161,10 @@ module Fontisan
478
161
  @table_data[tag_key] = table_data_parts.join
479
162
  end
480
163
 
481
- # Parse a table from raw data
482
- #
483
- # @param tag [String] The table tag to parse
484
- # @return [Tables::*, nil] Parsed table object or nil
485
- def parse_table(tag)
486
- raw_data = @table_data[tag]
487
- return nil unless raw_data
488
-
489
- table_class = table_class_for(tag)
490
- return nil unless table_class
491
-
492
- table_class.read(raw_data)
493
- end
494
-
495
164
  # Map table tag to parser class
496
165
  #
166
+ # OpenType-specific mapping includes CFF table.
167
+ #
497
168
  # @param tag [String] The table tag
498
169
  # @return [Class, nil] Table parser class or nil
499
170
  def table_class_for(tag)
@@ -520,83 +191,5 @@ module Fontisan
520
191
  "sbix" => Tables::Sbix,
521
192
  }[tag]
522
193
  end
523
-
524
- # Write the structure (header + table directory) to IO
525
- #
526
- # @param io [IO] Open file handle
527
- # @return [void]
528
- def write_structure(io)
529
- # Write header
530
- header.write(io)
531
-
532
- # Write table directory with placeholder offsets
533
- tables.each do |entry|
534
- io.write(entry.tag)
535
- io.write([entry.checksum].pack("N"))
536
- io.write([0].pack("N")) # Placeholder offset
537
- io.write([entry.table_length].pack("N"))
538
- end
539
- end
540
-
541
- # Write table data and update offsets in directory
542
- #
543
- # @param io [IO] Open file handle
544
- # @return [void]
545
- def write_table_data_with_offsets(io)
546
- tables.each_with_index do |entry, index|
547
- # Record current position
548
- current_position = io.pos
549
-
550
- # Write table data
551
- data = @table_data[entry.tag]
552
- raise IOError, "Missing table data for tag '#{entry.tag}'" if data.nil?
553
-
554
- io.write(data)
555
-
556
- # Add padding to align to 4-byte boundary
557
- padding = (Constants::TABLE_ALIGNMENT - (io.pos % Constants::TABLE_ALIGNMENT)) % Constants::TABLE_ALIGNMENT
558
- io.write("\x00" * padding) if padding.positive?
559
-
560
- # Zero out checksumAdjustment field in head table
561
- if entry.tag == Constants::HEAD_TAG
562
- current_pos = io.pos
563
- io.seek(current_position + 8)
564
- io.write([0].pack("N"))
565
- io.seek(current_pos)
566
- end
567
-
568
- # Update offset in table directory
569
- # Table directory starts at byte 12, each entry is 16 bytes
570
- # Offset field is at byte 8 within each entry
571
- directory_offset_position = 12 + (index * 16) + 8
572
- current_pos = io.pos
573
- io.seek(directory_offset_position)
574
- io.write([current_position].pack("N"))
575
- io.seek(current_pos)
576
- end
577
- end
578
-
579
- # Update checksumAdjustment field in head table
580
- #
581
- # @param path [String] Path to the OTF file
582
- # @return [void]
583
- def update_checksum_adjustment_in_file(path)
584
- # Use tempfile-based checksum calculation for Windows compatibility
585
- # This keeps the tempfile alive until we're done with the checksum
586
- File.open(path, "r+b") do |io|
587
- checksum, _tmpfile = Utilities::ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
588
-
589
- # Calculate adjustment
590
- adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
591
-
592
- # Find head table position
593
- head_entry = head_table
594
- return unless head_entry
595
-
596
- # Write adjustment to head table (offset 8 within head table)
597
- io.seek(head_entry.offset + 8)
598
- io.write([adjustment].pack("N"))
599
- end
600
- end
601
194
  end
602
195
  end
@@ -151,7 +151,7 @@ module Fontisan
151
151
  # @param charstring [String] CharString to search
152
152
  # @param pattern [Pattern] pattern to find
153
153
  # @return [Array<Integer>] array of start positions
154
- def find_pattern_positions(charstring, pattern, glyph_id = nil)
154
+ def find_pattern_positions(charstring, pattern, _glyph_id = nil)
155
155
  positions = []
156
156
  offset = 0
157
157
 
@@ -32,7 +32,9 @@ module Fontisan
32
32
  selected = []
33
33
  # Sort by savings (descending), then by length (descending), then by min glyph ID,
34
34
  # then by byte values for complete determinism across platforms
35
- remaining = @patterns.sort_by { |p| [-p.savings, -p.length, p.glyphs.min, p.bytes.bytes] }
35
+ remaining = @patterns.sort_by do |p|
36
+ [-p.savings, -p.length, p.glyphs.min, p.bytes.bytes]
37
+ end
36
38
 
37
39
  remaining.each do |pattern|
38
40
  break if selected.length >= @max_subrs
@@ -53,7 +55,9 @@ module Fontisan
53
55
  def optimize_ordering(subroutines)
54
56
  # Higher frequency = lower ID (shorter encoding)
55
57
  # Use same comprehensive sort keys as optimize_selection for consistency
56
- subroutines.sort_by { |subr| [-subr.frequency, -subr.length, subr.glyphs.min, subr.bytes.bytes] }
58
+ subroutines.sort_by do |subr|
59
+ [-subr.frequency, -subr.length, subr.glyphs.min, subr.bytes.bytes]
60
+ end
57
61
  end
58
62
 
59
63
  # Check if nesting would be beneficial