fontisan 0.2.1 → 0.2.3

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +58 -392
  3. data/README.adoc +1509 -1430
  4. data/Rakefile +3 -2
  5. data/benchmark/variation_quick_bench.rb +4 -4
  6. data/docs/FONT_HINTING.adoc +562 -0
  7. data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
  8. data/lib/fontisan/base_collection.rb +296 -0
  9. data/lib/fontisan/cli.rb +10 -3
  10. data/lib/fontisan/collection/builder.rb +2 -1
  11. data/lib/fontisan/collection/offset_calculator.rb +2 -0
  12. data/lib/fontisan/commands/base_command.rb +5 -2
  13. data/lib/fontisan/commands/convert_command.rb +6 -2
  14. data/lib/fontisan/commands/info_command.rb +129 -5
  15. data/lib/fontisan/commands/instance_command.rb +8 -7
  16. data/lib/fontisan/commands/validate_command.rb +4 -1
  17. data/lib/fontisan/constants.rb +24 -24
  18. data/lib/fontisan/converters/format_converter.rb +8 -4
  19. data/lib/fontisan/converters/outline_converter.rb +21 -16
  20. data/lib/fontisan/converters/woff_writer.rb +8 -3
  21. data/lib/fontisan/font_loader.rb +120 -30
  22. data/lib/fontisan/font_writer.rb +2 -0
  23. data/lib/fontisan/formatters/text_formatter.rb +116 -19
  24. data/lib/fontisan/hints/hint_converter.rb +43 -47
  25. data/lib/fontisan/hints/hint_validator.rb +284 -0
  26. data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
  27. data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
  28. data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
  29. data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
  30. data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
  31. data/lib/fontisan/loading_modes.rb +4 -4
  32. data/lib/fontisan/models/collection_brief_info.rb +37 -0
  33. data/lib/fontisan/models/collection_info.rb +6 -1
  34. data/lib/fontisan/models/font_export.rb +2 -2
  35. data/lib/fontisan/models/font_info.rb +3 -30
  36. data/lib/fontisan/models/hint.rb +22 -23
  37. data/lib/fontisan/models/outline.rb +4 -1
  38. data/lib/fontisan/models/validation_report.rb +1 -1
  39. data/lib/fontisan/open_type_collection.rb +17 -220
  40. data/lib/fontisan/open_type_font.rb +3 -1
  41. data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
  42. data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
  43. data/lib/fontisan/pipeline/output_writer.rb +8 -3
  44. data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
  45. data/lib/fontisan/subset/table_subsetter.rb +5 -5
  46. data/lib/fontisan/tables/cff/charstring.rb +38 -12
  47. data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
  48. data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
  49. data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
  50. data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
  51. data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
  52. data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
  53. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  54. data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
  55. data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
  56. data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
  57. data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
  58. data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
  59. data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
  60. data/lib/fontisan/tables/cff2.rb +1 -1
  61. data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
  62. data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
  63. data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
  64. data/lib/fontisan/tables/name.rb +4 -4
  65. data/lib/fontisan/true_type_collection.rb +29 -113
  66. data/lib/fontisan/true_type_font.rb +3 -1
  67. data/lib/fontisan/validation/checksum_validator.rb +2 -2
  68. data/lib/fontisan/variation/cache.rb +3 -1
  69. data/lib/fontisan/variation/converter.rb +2 -1
  70. data/lib/fontisan/variation/delta_applier.rb +2 -1
  71. data/lib/fontisan/variation/inspector.rb +2 -1
  72. data/lib/fontisan/variation/instance_generator.rb +2 -1
  73. data/lib/fontisan/variation/optimizer.rb +6 -3
  74. data/lib/fontisan/variation/subsetter.rb +32 -10
  75. data/lib/fontisan/variation/variation_preserver.rb +4 -1
  76. data/lib/fontisan/version.rb +1 -1
  77. data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
  78. data/lib/fontisan/woff2_font.rb +31 -15
  79. data/lib/fontisan.rb +42 -2
  80. data/scripts/measure_optimization.rb +15 -7
  81. metadata +9 -2
@@ -61,7 +61,7 @@ module Fontisan
61
61
  major_version: read_uint8,
62
62
  minor_version: read_uint8,
63
63
  header_size: read_uint8,
64
- top_dict_length: read_uint16
64
+ top_dict_length: read_uint16,
65
65
  }
66
66
 
67
67
  # Validate CFF2 version
@@ -105,7 +105,7 @@ module Fontisan
105
105
  # Parse Variable Store structure
106
106
  @variable_store = {
107
107
  regions: read_region_list,
108
- item_variation_data: read_item_variation_data
108
+ item_variation_data: read_item_variation_data,
109
109
  }
110
110
 
111
111
  @variable_store
@@ -140,7 +140,7 @@ module Fontisan
140
140
  axes << {
141
141
  start_coord: read_f2dot14,
142
142
  peak_coord: read_f2dot14,
143
- end_coord: read_f2dot14
143
+ end_coord: read_f2dot14,
144
144
  }
145
145
  end
146
146
 
@@ -158,13 +158,11 @@ module Fontisan
158
158
 
159
159
  item_variation_data = []
160
160
 
161
- data_count.times do |idx|
162
- begin
163
- item_data = read_single_item_variation_data
164
- item_variation_data << item_data
165
- rescue EOFError => e
166
- # break
167
- end
161
+ data_count.times do |_idx|
162
+ item_data = read_single_item_variation_data
163
+ item_variation_data << item_data
164
+ rescue EOFError
165
+ # break
168
166
  end
169
167
 
170
168
  item_variation_data
@@ -186,32 +184,32 @@ module Fontisan
186
184
 
187
185
  # Read delta sets
188
186
  delta_sets = []
189
- item_count.times do |item_idx|
190
- begin
191
- deltas = []
192
-
193
- # Short deltas (16-bit)
194
- short_delta_count.times do
195
- break if @io.eof?
196
- deltas << read_int16
197
- end
198
-
199
- # Long deltas (8-bit) for remaining regions
200
- (region_index_count - short_delta_count).times do
201
- break if @io.eof?
202
- deltas << read_int8
203
- end
204
-
205
- delta_sets << deltas
206
- rescue EOFError => e
207
- # break
187
+ item_count.times do |_item_idx|
188
+ deltas = []
189
+
190
+ # Short deltas (16-bit)
191
+ short_delta_count.times do
192
+ break if @io.eof?
193
+
194
+ deltas << read_int16
195
+ end
196
+
197
+ # Long deltas (8-bit) for remaining regions
198
+ (region_index_count - short_delta_count).times do
199
+ break if @io.eof?
200
+
201
+ deltas << read_int8
208
202
  end
203
+
204
+ delta_sets << deltas
205
+ rescue EOFError
206
+ # break
209
207
  end
210
208
 
211
209
  {
212
210
  item_count: item_count,
213
211
  region_indices: region_indices,
214
- delta_sets: delta_sets
212
+ delta_sets: delta_sets,
215
213
  }
216
214
  end
217
215
 
@@ -249,7 +247,10 @@ module Fontisan
249
247
  # @raise [EOFError] If not enough bytes available
250
248
  def read_safely(bytes, description)
251
249
  data = @io.read(bytes)
252
- raise EOFError, "Unexpected EOF while reading #{description}" if data.nil? || data.bytesize < bytes
250
+ if data.nil? || data.bytesize < bytes
251
+ raise EOFError,
252
+ "Unexpected EOF while reading #{description}"
253
+ end
253
254
 
254
255
  data
255
256
  end
@@ -269,7 +270,8 @@ module Fontisan
269
270
 
270
271
  if operator_byte?(byte)
271
272
  operator = read_dict_operator(io, byte)
272
- dict[operator] = operands.size == 1 ? operands.first : operands.dup
273
+ dict[operator] =
274
+ operands.size == 1 ? operands.first : operands.dup
273
275
  operands.clear
274
276
  else
275
277
  # Operand (number)
@@ -416,4 +418,4 @@ module Fontisan
416
418
  end
417
419
  end
418
420
  end
419
- end
421
+ end
@@ -148,7 +148,7 @@ module Fontisan
148
148
  {
149
149
  start: axis[:start_coord],
150
150
  peak: axis[:peak_coord],
151
- end: axis[:end_coord]
151
+ end: axis[:end_coord],
152
152
  }
153
153
  end
154
154
  end
@@ -209,4 +209,4 @@ module Fontisan
209
209
  end
210
210
  end
211
211
  end
212
- end
212
+ end
@@ -111,7 +111,7 @@ module Fontisan
111
111
  @num_axes,
112
112
  @global_subr_index,
113
113
  nil, # local subrs (CFF2 may not have them)
114
- 0 # vsindex
114
+ 0, # vsindex
115
115
  ).parse
116
116
  end
117
117
 
@@ -118,7 +118,8 @@ module Fontisan
118
118
  resolve(component_glyph, visited, depth + 1)
119
119
  else
120
120
  # Convert simple glyph to outline
121
- Models::Outline.from_truetype(component_glyph, component.glyph_index)
121
+ Models::Outline.from_truetype(component_glyph,
122
+ component.glyph_index)
122
123
  end
123
124
 
124
125
  # Apply transformation matrix
@@ -119,7 +119,10 @@ module Fontisan
119
119
  # @raise [ArgumentError] If parameters are invalid
120
120
  def self.calculate_error(cubic, quadratics)
121
121
  validate_cubic_curve!(cubic)
122
- raise ArgumentError, "quadratics must be Array" unless quadratics.is_a?(Array)
122
+ unless quadratics.is_a?(Array)
123
+ raise ArgumentError,
124
+ "quadratics must be Array"
125
+ end
123
126
  raise ArgumentError, "quadratics cannot be empty" if quadratics.empty?
124
127
 
125
128
  max_error = 0.0
@@ -305,7 +308,8 @@ module Fontisan
305
308
  required.each do |key|
306
309
  value = quad[key]
307
310
  unless value.is_a?(Numeric)
308
- raise ArgumentError, "quad[:#{key}] must be Numeric, got: #{value.class}"
311
+ raise ArgumentError,
312
+ "quad[:#{key}] must be Numeric, got: #{value.class}"
309
313
  end
310
314
  end
311
315
  end
@@ -324,14 +328,16 @@ module Fontisan
324
328
  required.each do |key|
325
329
  value = cubic[key]
326
330
  unless value.is_a?(Numeric)
327
- raise ArgumentError, "cubic[:#{key}] must be Numeric, got: #{value.class}"
331
+ raise ArgumentError,
332
+ "cubic[:#{key}] must be Numeric, got: #{value.class}"
328
333
  end
329
334
  end
330
335
  end
331
336
 
332
337
  private_class_method def self.validate_max_error!(max_error)
333
338
  unless max_error.is_a?(Numeric)
334
- raise ArgumentError, "max_error must be Numeric, got: #{max_error.class}"
339
+ raise ArgumentError,
340
+ "max_error must be Numeric, got: #{max_error.class}"
335
341
  end
336
342
 
337
343
  if max_error <= 0
@@ -106,7 +106,11 @@ module Fontisan
106
106
  # @raise [ArgumentError] If parameters are invalid
107
107
  def self.build_compound_glyph(components, bbox, instructions: "".b)
108
108
  raise ArgumentError, "components cannot be nil" if components.nil?
109
- raise ArgumentError, "components must be Array" unless components.is_a?(Array)
109
+
110
+ unless components.is_a?(Array)
111
+ raise ArgumentError,
112
+ "components must be Array"
113
+ end
110
114
  raise ArgumentError, "components cannot be empty" if components.empty?
111
115
 
112
116
  validate_bbox!(bbox)
@@ -114,7 +118,8 @@ module Fontisan
114
118
  build_compound_glyph_data(components, bbox, instructions)
115
119
  end
116
120
 
117
- private_class_method def self.build_simple_glyph_data(contours, bbox, instructions)
121
+ private_class_method def self.build_simple_glyph_data(contours, bbox,
122
+ instructions)
118
123
  num_contours = contours.length
119
124
 
120
125
  # Build endPtsOfContours array
@@ -136,7 +141,8 @@ module Fontisan
136
141
 
137
142
  # Header (10 bytes)
138
143
  data << [num_contours].pack("n") # numberOfContours
139
- data << [bbox[:x_min], bbox[:y_min], bbox[:x_max], bbox[:y_max]].pack("n4")
144
+ data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
145
+ bbox[:y_max]].pack("n4")
140
146
 
141
147
  # endPtsOfContours
142
148
  data << end_pts_of_contours.pack("n*")
@@ -155,18 +161,21 @@ module Fontisan
155
161
  data
156
162
  end
157
163
 
158
- private_class_method def self.build_compound_glyph_data(components, bbox, instructions)
164
+ private_class_method def self.build_compound_glyph_data(components, bbox,
165
+ instructions)
159
166
  data = (+"").force_encoding(Encoding::BINARY)
160
167
 
161
168
  # Header (10 bytes) - numberOfContours = -1 for compound
162
169
  data << [-1].pack("n") # Use signed pack, will convert to 0xFFFF
163
- data << [bbox[:x_min], bbox[:y_min], bbox[:x_max], bbox[:y_max]].pack("n4")
170
+ data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
171
+ bbox[:y_max]].pack("n4")
164
172
 
165
173
  # Encode components
166
174
  has_instructions = instructions.bytesize.positive?
167
175
  components.each_with_index do |component, index|
168
176
  is_last = (index == components.length - 1)
169
- component_data = encode_component(component, is_last, has_instructions)
177
+ component_data = encode_component(component, is_last,
178
+ has_instructions)
170
179
  data << component_data
171
180
  end
172
181
 
@@ -179,7 +188,8 @@ module Fontisan
179
188
  data
180
189
  end
181
190
 
182
- private_class_method def self.encode_component(component, is_last, has_instructions)
191
+ private_class_method def self.encode_component(component, is_last,
192
+ has_instructions)
183
193
  validate_component!(component)
184
194
 
185
195
  glyph_index = component[:glyph_index]
@@ -344,7 +354,8 @@ module Fontisan
344
354
  data
345
355
  end
346
356
 
347
- private_class_method def self.encode_coordinate_values(flags, deltas, axis)
357
+ private_class_method def self.encode_coordinate_values(flags, deltas,
358
+ axis)
348
359
  data = (+"").force_encoding(Encoding::BINARY)
349
360
  short_flag = axis == :x ? X_SHORT_VECTOR : Y_SHORT_VECTOR
350
361
  same_flag = axis == :x ? X_IS_SAME_OR_POSITIVE_X_SHORT : Y_IS_SAME_OR_POSITIVE_Y_SHORT
@@ -400,7 +411,10 @@ module Fontisan
400
411
  # Convert float to F2DOT14 fixed-point format
401
412
  # F2DOT14: 2 bits integer, 14 bits fractional
402
413
  # Range: -2.0 to ~1.99993896484375
403
- raise ArgumentError, "value out of F2DOT14 range" if value < -2.0 || value > 2.0
414
+ if value < -2.0 || value > 2.0
415
+ raise ArgumentError,
416
+ "value out of F2DOT14 range"
417
+ end
404
418
 
405
419
  fixed = (value * 16_384.0).round
406
420
  # Convert to unsigned 16-bit
@@ -434,7 +448,10 @@ module Fontisan
434
448
  end
435
449
 
436
450
  private_class_method def self.validate_component!(component)
437
- raise ArgumentError, "component must be Hash" unless component.is_a?(Hash)
451
+ unless component.is_a?(Hash)
452
+ raise ArgumentError,
453
+ "component must be Hash"
454
+ end
438
455
  unless component[:glyph_index]
439
456
  raise ArgumentError, "component must have :glyph_index"
440
457
  end
@@ -173,13 +173,13 @@ module Fontisan
173
173
  record = find_name_record(
174
174
  name_id,
175
175
  platform: PLATFORM_WINDOWS,
176
- language: WINDOWS_LANGUAGE_EN_US
176
+ language: WINDOWS_LANGUAGE_EN_US,
177
177
  )
178
178
 
179
179
  record ||= find_name_record(
180
180
  name_id,
181
181
  platform: PLATFORM_MACINTOSH,
182
- language: MAC_LANGUAGE_ENGLISH
182
+ language: MAC_LANGUAGE_ENGLISH,
183
183
  )
184
184
 
185
185
  return nil unless record
@@ -236,10 +236,10 @@ module Fontisan
236
236
  decoded = case record.platform_id
237
237
  when PLATFORM_WINDOWS, PLATFORM_UNICODE
238
238
  string_data.dup.force_encoding("UTF-16BE")
239
- .encode("UTF-8", invalid: :replace, undef: :replace)
239
+ .encode("UTF-8", invalid: :replace, undef: :replace)
240
240
  when PLATFORM_MACINTOSH
241
241
  string_data.dup.force_encoding("ASCII-8BIT")
242
- .encode("UTF-8", invalid: :replace, undef: :replace)
242
+ .encode("UTF-8", invalid: :replace, undef: :replace)
243
243
  else
244
244
  string_data.dup.force_encoding("UTF-8")
245
245
  end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bindata"
4
- require_relative "constants"
3
+ require_relative "base_collection"
5
4
 
6
5
  module Fontisan
7
- # TrueType Collection domain object using BinData
6
+ # TrueType Collection domain object
8
7
  #
9
- # Represents a complete TrueType Collection file using BinData's declarative
10
- # DSL for binary structure definition. The structure definition IS the
11
- # documentation, and BinData handles all low-level reading/writing.
8
+ # Represents a complete TrueType Collection file. Inherits all shared
9
+ # functionality from BaseCollection and implements TTC-specific behavior.
12
10
  #
13
11
  # @example Reading and extracting fonts
14
12
  # File.open("Helvetica.ttc", "rb") do |io|
@@ -16,51 +14,25 @@ module Fontisan
16
14
  # puts ttc.num_fonts # => 6
17
15
  # fonts = ttc.extract_fonts(io) # => [TrueTypeFont, TrueTypeFont, ...]
18
16
  # end
19
- class TrueTypeCollection < BinData::Record
20
- endian :big
21
-
22
- string :tag, length: 4, assert: "ttcf"
23
- uint16 :major_version
24
- uint16 :minor_version
25
- uint32 :num_fonts
26
- array :font_offsets, type: :uint32, initial_length: :num_fonts
27
-
28
- # Read TrueType Collection from a file
17
+ class TrueTypeCollection < BaseCollection
18
+ # Get the font class for TrueType collections
29
19
  #
30
- # @param path [String] Path to the TTC file
31
- # @return [TrueTypeCollection] A new instance
32
- # @raise [ArgumentError] if path is nil or empty
33
- # @raise [Errno::ENOENT] if file does not exist
34
- # @raise [RuntimeError] if file format is invalid
35
- def self.from_file(path)
36
- if path.nil? || path.to_s.empty?
37
- raise ArgumentError,
38
- "path cannot be nil or empty"
39
- end
40
- raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
41
-
42
- File.open(path, "rb") { |io| read(io) }
43
- rescue BinData::ValidityError => e
44
- raise "Invalid TTC file: #{e.message}"
45
- rescue EOFError => e
46
- raise "Invalid TTC file: unexpected end of file - #{e.message}"
20
+ # @return [Class] TrueTypeFont class
21
+ def self.font_class
22
+ require_relative "true_type_font"
23
+ TrueTypeFont
47
24
  end
48
25
 
49
- # Extract fonts as TrueTypeFont objects
26
+ # Get the collection format identifier
50
27
  #
51
- # Reads each font from the TTC file and returns them as TrueTypeFont objects.
52
- #
53
- # @param io [IO] Open file handle to read fonts from
54
- # @return [Array<TrueTypeFont>] Array of font objects
55
- def extract_fonts(io)
56
- require_relative "true_type_font"
57
-
58
- font_offsets.map do |offset|
59
- TrueTypeFont.from_ttc(io, offset)
60
- end
28
+ # @return [String] "TTC" for TrueType Collection
29
+ def self.collection_format
30
+ "TTC"
61
31
  end
62
32
 
63
- # Get a single font from the collection (Fontisan extension)
33
+ # Get a single font from the collection
34
+ #
35
+ # Overrides BaseCollection to use TrueType-specific from_ttc method.
64
36
  #
65
37
  # @param index [Integer] Index of the font (0-based)
66
38
  # @param io [IO] Open file handle
@@ -73,43 +45,27 @@ module Fontisan
73
45
  TrueTypeFont.from_ttc(io, font_offsets[index], mode: mode)
74
46
  end
75
47
 
76
- # Get font count (Fontisan extension)
48
+ # Extract fonts as TrueTypeFont objects
77
49
  #
78
- # @return [Integer] Number of fonts in collection
79
- def font_count
80
- num_fonts
81
- end
82
-
83
- # Validate format correctness
50
+ # Reads each font from the TTC file and returns them as TrueTypeFont objects.
51
+ # This method uses the TTC-specific from_ttc method.
84
52
  #
85
- # @return [Boolean] true if the format is valid, false otherwise
86
- def valid?
87
- tag == Constants::TTC_TAG && num_fonts.positive? && font_offsets.length == num_fonts
88
- rescue StandardError
89
- false
90
- end
53
+ # @param io [IO] Open file handle to read fonts from
54
+ # @return [Array<TrueTypeFont>] Array of font objects
55
+ def extract_fonts(io)
56
+ require_relative "true_type_font"
91
57
 
92
- # Get the TTC version as a single integer
93
- #
94
- # @return [Integer] Version number (e.g., 0x00010000 for version 1.0)
95
- def version
96
- (major_version << 16) | minor_version
58
+ font_offsets.map do |offset|
59
+ TrueTypeFont.from_ttc(io, offset)
60
+ end
97
61
  end
98
62
 
99
63
  # List all fonts in the collection with basic metadata
100
64
  #
101
- # Returns a CollectionListInfo model containing summaries of all fonts.
102
- # This is the API method used by the `ls` command for collections.
65
+ # Overrides BaseCollection to use TrueType-specific from_ttc method.
103
66
  #
104
67
  # @param io [IO] Open file handle to read fonts from
105
68
  # @return [CollectionListInfo] List of fonts with metadata
106
- #
107
- # @example List fonts in collection
108
- # File.open("fonts.ttc", "rb") do |io|
109
- # ttc = TrueTypeCollection.read(io)
110
- # list = ttc.list_fonts(io)
111
- # list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
112
- # end
113
69
  def list_fonts(io)
114
70
  require_relative "models/collection_list_info"
115
71
  require_relative "models/collection_font_summary"
@@ -159,51 +115,11 @@ module Fontisan
159
115
  )
160
116
  end
161
117
 
162
- # Get comprehensive collection metadata
163
- #
164
- # Returns a CollectionInfo model with header information, offsets,
165
- # and table sharing statistics.
166
- # This is the API method used by the `info` command for collections.
167
- #
168
- # @param io [IO] Open file handle to read fonts from
169
- # @param path [String] Collection file path (for file size)
170
- # @return [CollectionInfo] Collection metadata
171
- #
172
- # @example Get collection info
173
- # File.open("fonts.ttc", "rb") do |io|
174
- # ttc = TrueTypeCollection.read(io)
175
- # info = ttc.collection_info(io, "fonts.ttc")
176
- # puts "Version: #{info.version_string}"
177
- # end
178
- def collection_info(io, path)
179
- require_relative "models/collection_info"
180
- require_relative "models/table_sharing_info"
181
-
182
- # Calculate table sharing statistics
183
- table_sharing = calculate_table_sharing(io)
184
-
185
- # Get file size
186
- file_size = path ? File.size(path) : 0
187
-
188
- Models::CollectionInfo.new(
189
- collection_path: path,
190
- collection_format: "TTC",
191
- ttc_tag: tag,
192
- major_version: major_version,
193
- minor_version: minor_version,
194
- num_fonts: num_fonts,
195
- font_offsets: font_offsets.to_a,
196
- file_size_bytes: file_size,
197
- table_sharing: table_sharing,
198
- )
199
- end
200
-
201
118
  private
202
119
 
203
120
  # Calculate table sharing statistics
204
121
  #
205
- # Analyzes which tables are shared between fonts and calculates
206
- # space savings from deduplication.
122
+ # Overrides BaseCollection to use TrueType-specific from_ttc method.
207
123
  #
208
124
  # @param io [IO] Open file handle
209
125
  # @return [TableSharingInfo] Sharing statistics
@@ -233,7 +233,8 @@ module Fontisan
233
233
  batch_entries.each do |entry|
234
234
  relative_offset = entry.offset - batch_offset
235
235
  tag_key = entry.tag.dup.force_encoding("UTF-8")
236
- @table_data[tag_key] = batch_data[relative_offset, entry.table_length]
236
+ @table_data[tag_key] =
237
+ batch_data[relative_offset, entry.table_length]
237
238
  end
238
239
  end
239
240
 
@@ -307,6 +308,7 @@ module Fontisan
307
308
  # @return [Boolean] true if table is available in current mode
308
309
  def table_available?(tag)
309
310
  return false unless has_table?(tag)
311
+
310
312
  LoadingModes.table_allowed?(@loading_mode, tag)
311
313
  end
312
314
 
@@ -114,7 +114,7 @@ module Fontisan
114
114
  skip_tables = @checksum_config["skip_tables"] || []
115
115
 
116
116
  font.tables.each do |table_entry|
117
- tag = table_entry.tag.to_s # Convert BinData field to string
117
+ tag = table_entry.tag.to_s # Convert BinData field to string
118
118
 
119
119
  # Skip tables that are exempt from checksum validation
120
120
  next if skip_tables.include?(tag)
@@ -125,7 +125,7 @@ module Fontisan
125
125
 
126
126
  # Calculate checksum for the table
127
127
  calculated_checksum = calculate_table_checksum(table_data)
128
- declared_checksum = table_entry.checksum.to_i # Convert BinData field to integer
128
+ declared_checksum = table_entry.checksum.to_i # Convert BinData field to integer
129
129
 
130
130
  # Special handling for head table (checksum adjustment field should be 0)
131
131
  if tag == Constants::HEAD_TAG
@@ -44,6 +44,7 @@ module Fontisan
44
44
  @ttl = ttl
45
45
  @cache = {}
46
46
  @access_times = {}
47
+ @access_counter = 0
47
48
  @stats = {
48
49
  hits: 0,
49
50
  misses: 0,
@@ -219,7 +220,8 @@ module Fontisan
219
220
  #
220
221
  # @param key [String] Cache key
221
222
  def touch(key)
222
- @access_times[key] = Time.now
223
+ @access_counter += 1
224
+ @access_times[key] = @access_counter
223
225
  end
224
226
 
225
227
  # Evict entries if cache is full
@@ -162,7 +162,8 @@ module Fontisan
162
162
  # Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
163
163
  # This is a simplified mapping - full implementation would track
164
164
  # which coordinates are being varied
165
- regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0, y: 0 }
165
+ regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0,
166
+ y: 0 }
166
167
  if idx.even?
167
168
  regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
168
169
  else
@@ -79,7 +79,8 @@ module Fontisan
79
79
  # Apply each active tuple's deltas
80
80
  adjusted_points = base_points.dup
81
81
  matches.each do |match|
82
- apply_tuple_deltas(adjusted_points, match, tuple_data, base_points.length)
82
+ apply_tuple_deltas(adjusted_points, match, tuple_data,
83
+ base_points.length)
83
84
  end
84
85
 
85
86
  adjusted_points
@@ -107,7 +107,8 @@ module Fontisan
107
107
  index: index,
108
108
  name: instance_name(instance[:subfamily_name_id]),
109
109
  postscript_name: instance_name(instance[:postscript_name_id]),
110
- coordinates: instance_coordinates(instance[:coordinates], @context.axes),
110
+ coordinates: instance_coordinates(instance[:coordinates],
111
+ @context.axes),
111
112
  }
112
113
  end
113
114
  end
@@ -248,7 +248,8 @@ module Fontisan
248
248
  # @param scalars [Array<Float>] Region scalars
249
249
  # @return [Hash] Interpolated point
250
250
  def interpolate_point(base_point, delta_points, scalars)
251
- @context.interpolator.interpolate_point(base_point, delta_points, scalars)
251
+ @context.interpolator.interpolate_point(base_point, delta_points,
252
+ scalars)
252
253
  end
253
254
 
254
255
  private
@@ -209,9 +209,12 @@ module Fontisan
209
209
  coords2 = r2.region_axes[i]
210
210
 
211
211
  # Compare start, peak, end coordinates
212
- return false unless coords_similar?(coords1.start_coord, coords2.start_coord)
213
- return false unless coords_similar?(coords1.peak_coord, coords2.peak_coord)
214
- return false unless coords_similar?(coords1.end_coord, coords2.end_coord)
212
+ return false unless coords_similar?(coords1.start_coord,
213
+ coords2.start_coord)
214
+ return false unless coords_similar?(coords1.peak_coord,
215
+ coords2.peak_coord)
216
+ return false unless coords_similar?(coords1.end_coord,
217
+ coords2.end_coord)
215
218
  end
216
219
 
217
220
  true