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
@@ -1,42 +1,17 @@
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
- # TTF Offset Table structure
10
- class OffsetTable < BinData::Record
11
- endian :big
12
- uint32 :sfnt_version
13
- uint16 :num_tables
14
- uint16 :search_range
15
- uint16 :entry_selector
16
- uint16 :range_shift
17
- end
18
-
19
- # TTF Table Directory Entry structure
20
- class TableDirectory < BinData::Record
21
- endian :big
22
- string :tag, length: 4
23
- uint32 :checksum
24
- uint32 :offset
25
- uint32 :table_length
26
- end
27
-
28
- # TrueType Font domain object using BinData
29
- #
30
- # Represents a complete TrueType Font file using BinData's declarative
31
- # DSL for binary structure definition. The structure definition IS the
32
- # documentation, and BinData handles all low-level reading/writing.
6
+ # TrueType Font domain object
33
7
  #
34
- # Extended from ExtractTTC to support table parsing for analysis.
8
+ # Represents a TrueType Font file (glyf outlines). Inherits all shared
9
+ # SFNT functionality from SfntFont and adds TrueType-specific behavior.
35
10
  #
36
11
  # @example Reading and analyzing a font
37
12
  # ttf = TrueTypeFont.from_file("font.ttf")
38
13
  # puts ttf.header.num_tables # => 14
39
- # name_table = ttf.table("name") # Fontisan extension
14
+ # name_table = ttf.table("name")
40
15
  # puts name_table.english_name(Tables::Name::FAMILY)
41
16
  #
42
17
  # @example Loading with metadata mode
@@ -46,34 +21,15 @@ module Fontisan
46
21
  #
47
22
  # @example Writing a font
48
23
  # ttf.to_file("output.ttf")
49
- class TrueTypeFont < BinData::Record
50
- endian :big
51
-
52
- offset_table :header
53
- array :tables, type: :table_directory, initial_length: lambda {
54
- header.num_tables
55
- }
56
-
57
- # Table data is stored separately since it's at variable offsets
58
- attr_accessor :table_data
59
-
60
- # Parsed table instances cache (Fontisan extension)
61
- attr_accessor :parsed_tables
62
-
63
- # Loading mode for this font (:metadata or :full)
64
- attr_accessor :loading_mode
65
-
66
- # IO source for lazy loading
67
- attr_accessor :io_source
68
-
69
- # Whether lazy loading is enabled
70
- attr_accessor :lazy_load_enabled
71
-
24
+ #
25
+ # @example Reading from TTC collection
26
+ # ttf = TrueTypeFont.from_ttc(io, offset)
27
+ class TrueTypeFont < SfntFont
72
28
  # Read TrueType Font from a file
73
29
  #
74
30
  # @param path [String] Path to the TTF file
75
31
  # @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
76
- # @param lazy [Boolean] If true, load tables on demand (default: false for eager loading)
32
+ # @param lazy [Boolean] If true, load tables on demand (default: false)
77
33
  # @return [TrueTypeFont] A new instance
78
34
  # @raise [ArgumentError] if path is nil or empty, or if mode is invalid
79
35
  # @raise [Errno::ENOENT] if file does not exist
@@ -127,153 +83,6 @@ module Fontisan
127
83
  font
128
84
  end
129
85
 
130
- # Initialize storage hashes (Fontisan extension)
131
- #
132
- # @return [void]
133
- def initialize_storage
134
- @table_data = {}
135
- @parsed_tables = {}
136
- @loading_mode = LoadingModes::FULL
137
- @lazy_load_enabled = false
138
- @io_source = nil
139
- end
140
-
141
- # Read table data for all tables
142
- #
143
- # In metadata mode, only reads metadata tables. In full mode, reads all tables.
144
- # In lazy load mode, doesn't read data upfront.
145
- #
146
- # @param io [IO] Open file handle
147
- # @return [void]
148
- def read_table_data(io)
149
- @table_data = {}
150
-
151
- if @lazy_load_enabled
152
- # Don't read data, just keep IO reference
153
- @io_source = io
154
- return
155
- end
156
-
157
- if @loading_mode == LoadingModes::METADATA
158
- # Only read metadata tables for performance
159
- # Use page-aware batched reading to maximize filesystem prefetching
160
- read_metadata_tables_batched(io)
161
- else
162
- # Read all tables
163
- tables.each do |entry|
164
- io.seek(entry.offset)
165
- # Force UTF-8 encoding on tag for hash key consistency
166
- tag_key = entry.tag.dup.force_encoding("UTF-8")
167
- @table_data[tag_key] = io.read(entry.table_length)
168
- end
169
- end
170
- end
171
-
172
- # Read metadata tables using page-aware batching
173
- #
174
- # Groups adjacent tables within page boundaries and reads them together
175
- # to maximize filesystem prefetching and minimize random seeks.
176
- #
177
- # @param io [IO] Open file handle
178
- # @return [void]
179
- def read_metadata_tables_batched(io)
180
- # Typical filesystem page size (4KB is common, but 8KB gives better prefetch window)
181
- page_threshold = 8192
182
-
183
- # Get metadata tables sorted by offset for sequential access
184
- metadata_entries = tables.select { |entry| LoadingModes::METADATA_TABLES_SET.include?(entry.tag) }
185
- metadata_entries.sort_by!(&:offset)
186
-
187
- return if metadata_entries.empty?
188
-
189
- # Group adjacent tables within page threshold for batched reading
190
- i = 0
191
- while i < metadata_entries.size
192
- batch_start = metadata_entries[i]
193
- batch_end = batch_start
194
- batch_entries = [batch_start]
195
-
196
- # Extend batch while next table is within page threshold
197
- j = i + 1
198
- while j < metadata_entries.size
199
- next_entry = metadata_entries[j]
200
- gap = next_entry.offset - (batch_end.offset + batch_end.table_length)
201
-
202
- # If gap is small (within page threshold), include in batch
203
- if gap <= page_threshold
204
- batch_end = next_entry
205
- batch_entries << next_entry
206
- j += 1
207
- else
208
- break
209
- end
210
- end
211
-
212
- # Read batch
213
- if batch_entries.size == 1
214
- # Single table, read normally
215
- io.seek(batch_start.offset)
216
- tag_key = batch_start.tag.dup.force_encoding("UTF-8")
217
- @table_data[tag_key] = io.read(batch_start.table_length)
218
- else
219
- # Multiple tables, read contiguous segment
220
- batch_offset = batch_start.offset
221
- batch_length = (batch_end.offset + batch_end.table_length) - batch_start.offset
222
-
223
- io.seek(batch_offset)
224
- batch_data = io.read(batch_length)
225
-
226
- # Extract individual tables from batch
227
- batch_entries.each do |entry|
228
- relative_offset = entry.offset - batch_offset
229
- tag_key = entry.tag.dup.force_encoding("UTF-8")
230
- @table_data[tag_key] =
231
- batch_data[relative_offset, entry.table_length]
232
- end
233
- end
234
-
235
- i = j
236
- end
237
- end
238
-
239
- # Write TrueType Font to a file
240
- #
241
- # Writes the complete TTF structure to disk, including proper checksum
242
- # calculation and table alignment.
243
- #
244
- # @param path [String] Path where the TTF file will be written
245
- # @return [Integer] Number of bytes written
246
- # @raise [IOError] if writing fails
247
- def to_file(path)
248
- File.open(path, "wb") do |io|
249
- # Write header and tables (directory)
250
- write_structure(io)
251
-
252
- # Write table data with updated offsets
253
- write_table_data_with_offsets(io)
254
-
255
- io.pos
256
- end
257
-
258
- # Update checksum adjustment in head table
259
- update_checksum_adjustment_in_file(path) if head_table
260
-
261
- File.size(path)
262
- end
263
-
264
- # Validate format correctness
265
- #
266
- # @return [Boolean] true if the TTF format is valid, false otherwise
267
- def valid?
268
- return false unless header
269
- return false unless tables.respond_to?(:length)
270
- return false unless @table_data.is_a?(Hash)
271
- return false if tables.length != header.num_tables
272
- return false unless head_table
273
-
274
- true
275
- end
276
-
277
86
  # Check if font is TrueType flavored
278
87
  #
279
88
  # @return [Boolean] true for TrueType fonts
@@ -288,194 +97,11 @@ module Fontisan
288
97
  false
289
98
  end
290
99
 
291
- # Check if font has a specific table
292
- #
293
- # @param tag [String] The table tag to check for
294
- # @return [Boolean] true if table exists, false otherwise
295
- def has_table?(tag)
296
- tables.any? { |entry| entry.tag == tag }
297
- end
298
-
299
- # Check if a table is available in the current loading mode
300
- #
301
- # @param tag [String] The table tag to check
302
- # @return [Boolean] true if table is available in current mode
303
- def table_available?(tag)
304
- return false unless has_table?(tag)
305
-
306
- LoadingModes.table_allowed?(@loading_mode, tag)
307
- end
308
-
309
- # Find a table entry by tag
310
- #
311
- # @param tag [String] The table tag to find
312
- # @return [TableDirectory, nil] The table entry or nil
313
- def find_table_entry(tag)
314
- tables.find { |entry| entry.tag == tag }
315
- end
316
-
317
- # Get the head table entry
318
- #
319
- # @return [TableDirectory, nil] The head table entry or nil
320
- def head_table
321
- find_table_entry(Constants::HEAD_TAG)
322
- end
323
-
324
- # Get list of all table tags (Fontisan extension)
325
- #
326
- # @return [Array<String>] Array of table tag strings
327
- def table_names
328
- tables.map(&:tag)
329
- end
330
-
331
- # Get parsed table instance (Fontisan extension)
332
- #
333
- # This method parses the raw table data into a structured table object
334
- # and caches the result for subsequent calls. Enforces mode restrictions.
335
- #
336
- # @param tag [String] The table tag to retrieve
337
- # @return [Tables::*, nil] Parsed table object or nil if not found
338
- # @raise [ArgumentError] if table is not available in current loading mode
339
- def table(tag)
340
- # Check mode restrictions
341
- unless table_available?(tag)
342
- if has_table?(tag)
343
- raise ArgumentError,
344
- "Table '#{tag}' is not available in #{@loading_mode} mode. " \
345
- "Available tables: #{LoadingModes.tables_for(@loading_mode).inspect}"
346
- else
347
- return nil
348
- end
349
- end
350
-
351
- return @parsed_tables[tag] if @parsed_tables.key?(tag)
352
-
353
- # Lazy load table data if enabled
354
- if @lazy_load_enabled && !@table_data.key?(tag)
355
- load_table_data(tag)
356
- end
357
-
358
- @parsed_tables[tag] ||= parse_table(tag)
359
- end
360
-
361
- # Get units per em from head table (Fontisan extension)
362
- #
363
- # @return [Integer, nil] Units per em value
364
- def units_per_em
365
- head = table(Constants::HEAD_TAG)
366
- head&.units_per_em
367
- end
368
-
369
- # Convenience methods for accessing common name table fields
370
- # These are particularly useful in minimal mode
371
-
372
- # Get font family name
373
- #
374
- # @return [String, nil] Family name or nil if not found
375
- def family_name
376
- name_table = table(Constants::NAME_TAG)
377
- name_table&.english_name(Tables::Name::FAMILY)
378
- end
379
-
380
- # Get font subfamily name (e.g., Regular, Bold, Italic)
381
- #
382
- # @return [String, nil] Subfamily name or nil if not found
383
- def subfamily_name
384
- name_table = table(Constants::NAME_TAG)
385
- name_table&.english_name(Tables::Name::SUBFAMILY)
386
- end
387
-
388
- # Get full font name
389
- #
390
- # @return [String, nil] Full name or nil if not found
391
- def full_name
392
- name_table = table(Constants::NAME_TAG)
393
- name_table&.english_name(Tables::Name::FULL_NAME)
394
- end
395
-
396
- # Get PostScript name
397
- #
398
- # @return [String, nil] PostScript name or nil if not found
399
- def post_script_name
400
- name_table = table(Constants::NAME_TAG)
401
- name_table&.english_name(Tables::Name::POSTSCRIPT_NAME)
402
- end
403
-
404
- # Get preferred family name
405
- #
406
- # @return [String, nil] Preferred family name or nil if not found
407
- def preferred_family_name
408
- name_table = table(Constants::NAME_TAG)
409
- name_table&.english_name(Tables::Name::PREFERRED_FAMILY)
410
- end
411
-
412
- # Get preferred subfamily name
413
- #
414
- # @return [String, nil] Preferred subfamily name or nil if not found
415
- def preferred_subfamily_name
416
- name_table = table(Constants::NAME_TAG)
417
- name_table&.english_name(Tables::Name::PREFERRED_SUBFAMILY)
418
- end
419
-
420
- # Close the IO source (for lazy loading)
421
- #
422
- # @return [void]
423
- def close
424
- @io_source&.close
425
- @io_source = nil
426
- end
427
-
428
- # Setup finalizer for cleanup
429
- #
430
- # @return [void]
431
- def setup_finalizer
432
- ObjectSpace.define_finalizer(self, self.class.finalize(@io_source))
433
- end
434
-
435
- # Finalizer proc for closing IO
436
- #
437
- # @param io [IO] The IO object to close
438
- # @return [Proc] The finalizer proc
439
- def self.finalize(io)
440
- proc { io&.close }
441
- end
442
-
443
100
  private
444
101
 
445
- # Load a single table's data on demand
446
- #
447
- # Uses direct seek-and-read for minimal overhead. This ensures lazy loading
448
- # performance is comparable to eager loading when accessing all tables.
102
+ # Map table tag to parser class
449
103
  #
450
- # @param tag [String] The table tag to load
451
- # @return [void]
452
- def load_table_data(tag)
453
- return unless @io_source
454
-
455
- entry = find_table_entry(tag)
456
- return nil unless entry
457
-
458
- # Direct seek and read - same as eager loading but on-demand
459
- @io_source.seek(entry.offset)
460
- tag_key = tag.dup.force_encoding("UTF-8")
461
- @table_data[tag_key] = @io_source.read(entry.table_length)
462
- end
463
-
464
- # Parse a table from raw data (Fontisan extension)
465
- #
466
- # @param tag [String] The table tag to parse
467
- # @return [Tables::*, nil] Parsed table object or nil
468
- def parse_table(tag)
469
- raw_data = @table_data[tag]
470
- return nil unless raw_data
471
-
472
- table_class = table_class_for(tag)
473
- return nil unless table_class
474
-
475
- table_class.read(raw_data)
476
- end
477
-
478
- # Map table tag to parser class (Fontisan extension)
104
+ # TrueType-specific mapping includes glyf/loca tables.
479
105
  #
480
106
  # @param tag [String] The table tag
481
107
  # @return [Class, nil] Table parser class or nil
@@ -502,83 +128,5 @@ module Fontisan
502
128
  "sbix" => Tables::Sbix,
503
129
  }[tag]
504
130
  end
505
-
506
- # Write the structure (header + table directory) to IO
507
- #
508
- # @param io [IO] Open file handle
509
- # @return [void]
510
- def write_structure(io)
511
- # Write header
512
- header.write(io)
513
-
514
- # Write table directory with placeholder offsets
515
- tables.each do |entry|
516
- io.write(entry.tag)
517
- io.write([entry.checksum].pack("N"))
518
- io.write([0].pack("N")) # Placeholder offset
519
- io.write([entry.table_length].pack("N"))
520
- end
521
- end
522
-
523
- # Write table data and update offsets in directory
524
- #
525
- # @param io [IO] Open file handle
526
- # @return [void]
527
- def write_table_data_with_offsets(io)
528
- tables.each_with_index do |entry, index|
529
- # Record current position
530
- current_position = io.pos
531
-
532
- # Write table data
533
- data = @table_data[entry.tag]
534
- raise IOError, "Missing table data for tag '#{entry.tag}'" if data.nil?
535
-
536
- io.write(data)
537
-
538
- # Add padding to align to 4-byte boundary
539
- padding = (Constants::TABLE_ALIGNMENT - (io.pos % Constants::TABLE_ALIGNMENT)) % Constants::TABLE_ALIGNMENT
540
- io.write("\x00" * padding) if padding.positive?
541
-
542
- # Zero out checksumAdjustment field in head table
543
- if entry.tag == Constants::HEAD_TAG
544
- current_pos = io.pos
545
- io.seek(current_position + 8)
546
- io.write([0].pack("N"))
547
- io.seek(current_pos)
548
- end
549
-
550
- # Update offset in table directory
551
- # Table directory starts at byte 12, each entry is 16 bytes
552
- # Offset field is at byte 8 within each entry
553
- directory_offset_position = 12 + (index * 16) + 8
554
- current_pos = io.pos
555
- io.seek(directory_offset_position)
556
- io.write([current_position].pack("N")) # Offset is now known
557
- io.seek(current_pos)
558
- end
559
- end
560
-
561
- # Update checksumAdjustment field in head table
562
- #
563
- # @param path [String] Path to the TTF file
564
- # @return [void]
565
- def update_checksum_adjustment_in_file(path)
566
- # Use tempfile-based checksum calculation for Windows compatibility
567
- # This keeps the tempfile alive until we're done with the checksum
568
- File.open(path, "r+b") do |io|
569
- checksum, _tmpfile = Utilities::ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
570
-
571
- # Calculate adjustment
572
- adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
573
-
574
- # Find head table position
575
- head_entry = head_table
576
- return unless head_entry
577
-
578
- # Write adjustment to head table (offset 8 within head table)
579
- io.seek(head_entry.offset + 8)
580
- io.write([adjustment].pack("N"))
581
- end
582
- end
583
131
  end
584
132
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "stringio"
4
- require "tempfile"
5
4
  require_relative "../constants"
6
5
 
7
6
  module Fontisan
@@ -101,49 +100,6 @@ module Fontisan
101
100
 
102
101
  sum
103
102
  end
104
-
105
- # Calculate checksum from an IO object using a tempfile for Windows compatibility.
106
- #
107
- # This method creates a temporary file from the IO content to ensure proper
108
- # file handle semantics on Windows, where file handles must remain open
109
- # for checksum calculation. The tempfile reference is returned alongside
110
- # the checksum to prevent premature garbage collection on Windows.
111
- #
112
- # @param io [IO] the IO object to read from (must be rewindable)
113
- # @return [Array<Integer, Tempfile>] array containing [checksum, tempfile]
114
- # The checksum value and the tempfile that must be kept alive until
115
- # the caller is done with the checksum.
116
- #
117
- # @example
118
- # checksum, tmpfile = ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
119
- # # Use checksum...
120
- # # tmpfile will be GC'd when it goes out of scope, which is safe
121
- #
122
- # @note On Windows, Ruby's Tempfile automatically deletes the temp file when
123
- # the Tempfile object is garbage collected. In multi-threaded environments,
124
- # this can cause PermissionDenied errors if the file is deleted while
125
- # another thread is still using it. By returning the tempfile reference,
126
- # the caller can ensure it remains alive until all operations complete.
127
- def self.calculate_checksum_from_io_with_tempfile(io)
128
- io.rewind
129
-
130
- # Create a tempfile to handle Windows file locking issues
131
- tmpfile = Tempfile.new(["font", ".ttf"])
132
- tmpfile.binmode
133
-
134
- # Copy IO content to tempfile
135
- IO.copy_stream(io, tmpfile)
136
- tmpfile.close
137
-
138
- # Calculate checksum from the tempfile
139
- checksum = calculate_file_checksum(tmpfile.path)
140
-
141
- # Return both checksum and tempfile to keep it alive
142
- # The caller must keep the tempfile reference until done with checksum
143
- [checksum, tmpfile]
144
- end
145
-
146
- private_class_method :calculate_checksum_from_io
147
103
  end
148
104
  end
149
105
  end
@@ -68,7 +68,8 @@ module Fontisan
68
68
  issues = []
69
69
 
70
70
  return ["Font array cannot be empty"] if fonts.nil? || fonts.empty?
71
- return ["Invalid format: #{format}"] unless %i[ttc otc dfont].include?(format)
71
+ return ["Invalid format: #{format}"] unless %i[ttc otc
72
+ dfont].include?(format)
72
73
 
73
74
  case format
74
75
  when :ttc
@@ -100,7 +101,8 @@ module Fontisan
100
101
  # @raise [ArgumentError] if invalid
101
102
  def validate_format!(format)
102
103
  unless %i[ttc otc dfont].include?(format)
103
- raise ArgumentError, "Invalid format: #{format}. Must be :ttc, :otc, or :dfont"
104
+ raise ArgumentError,
105
+ "Invalid format: #{format}. Must be :ttc, :otc, or :dfont"
104
106
  end
105
107
  end
106
108
 
@@ -46,39 +46,29 @@ module Fontisan
46
46
  end
47
47
 
48
48
  # Check 2: Name table version must be valid (0 or 1)
49
- check_table :name_version, "name", severity: :error do |table|
50
- table.valid_version?
51
- end
49
+ check_table :name_version, "name", severity: :error, &:valid_version?
52
50
 
53
51
  # Check 3: Family name must be present and non-empty
54
- check_table :family_name, "name", severity: :error do |table|
55
- table.family_name_present?
56
- end
52
+ check_table :family_name, "name", severity: :error,
53
+ &:family_name_present?
57
54
 
58
55
  # Check 4: PostScript name must be valid (alphanumeric + hyphens)
59
- check_table :postscript_name, "name", severity: :error do |table|
60
- table.postscript_name_valid?
61
- end
56
+ check_table :postscript_name, "name", severity: :error,
57
+ &:postscript_name_valid?
62
58
 
63
59
  # Check 5: Head table magic number must be correct
64
- check_table :head_magic, "head", severity: :error do |table|
65
- table.valid_magic?
66
- end
60
+ check_table :head_magic, "head", severity: :error, &:valid_magic?
67
61
 
68
62
  # Check 6: Units per em must be valid (16-16384)
69
- check_table :units_per_em, "head", severity: :error do |table|
70
- table.valid_units_per_em?
71
- end
63
+ check_table :units_per_em, "head", severity: :error,
64
+ &:valid_units_per_em?
72
65
 
73
66
  # Check 7: Number of glyphs must be at least 1 (.notdef)
74
- check_table :num_glyphs, "maxp", severity: :error do |table|
75
- table.valid_num_glyphs?
76
- end
67
+ check_table :num_glyphs, "maxp", severity: :error, &:valid_num_glyphs?
77
68
 
78
69
  # Check 8: Maxp metrics should be reasonable (not absurd values)
79
- check_table :reasonable_metrics, "maxp", severity: :warning do |table|
80
- table.reasonable_metrics?
81
- end
70
+ check_table :reasonable_metrics, "maxp", severity: :warning,
71
+ &:reasonable_metrics?
82
72
  end
83
73
  end
84
74
  end