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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "lutaml/model"
4
4
  require_relative "table_sharing_info"
5
+ require_relative "font_info"
5
6
 
6
7
  module Fontisan
7
8
  module Models
@@ -20,7 +21,8 @@ module Fontisan
20
21
  # num_fonts: 6,
21
22
  # font_offsets: [48, 380, 712, 1044, 1376, 1676],
22
23
  # file_size_bytes: 2240000,
23
- # table_sharing: table_sharing_obj
24
+ # table_sharing: table_sharing_obj,
25
+ # fonts: [font_info1, font_info2, ...]
24
26
  # )
25
27
  class CollectionInfo < Lutaml::Model::Serializable
26
28
  attribute :collection_path, :string
@@ -32,6 +34,7 @@ module Fontisan
32
34
  attribute :font_offsets, :integer, collection: true
33
35
  attribute :file_size_bytes, :integer
34
36
  attribute :table_sharing, TableSharingInfo
37
+ attribute :fonts, FontInfo, collection: true
35
38
 
36
39
  yaml do
37
40
  map "collection_path", to: :collection_path
@@ -43,6 +46,7 @@ module Fontisan
43
46
  map "font_offsets", to: :font_offsets
44
47
  map "file_size_bytes", to: :file_size_bytes
45
48
  map "table_sharing", to: :table_sharing
49
+ map "fonts", to: :fonts
46
50
  end
47
51
 
48
52
  json do
@@ -55,6 +59,7 @@ module Fontisan
55
59
  map "font_offsets", to: :font_offsets
56
60
  map "file_size_bytes", to: :file_size_bytes
57
61
  map "table_sharing", to: :table_sharing
62
+ map "fonts", to: :fonts
58
63
  end
59
64
 
60
65
  # Get version as a formatted string
@@ -71,8 +71,8 @@ module Fontisan
71
71
  attribute :tag, :string
72
72
  attribute :checksum, :string
73
73
  attribute :parsed, :boolean, default: -> { false }
74
- attribute :data, :string, default: -> { nil }
75
- attribute :fields, :string, default: -> { nil }
74
+ attribute :data, :string, default: -> {}
75
+ attribute :fields, :string, default: -> {}
76
76
 
77
77
  yaml do
78
78
  map "tag", to: :tag
@@ -36,37 +36,9 @@ module Fontisan
36
36
  attribute :font_revision, :float
37
37
  attribute :permissions, :string
38
38
  attribute :units_per_em, :integer
39
+ attribute :collection_offset, :integer
39
40
 
40
- json do
41
- map "font_format", to: :font_format
42
- map "is_variable", to: :is_variable
43
- map "family_name", to: :family_name
44
- map "subfamily_name", to: :subfamily_name
45
- map "full_name", to: :full_name
46
- map "postscript_name", to: :postscript_name
47
- map "postscript_cid_name", to: :postscript_cid_name
48
- map "preferred_family", to: :preferred_family
49
- map "preferred_subfamily", to: :preferred_subfamily
50
- map "mac_font_menu_name", to: :mac_font_menu_name
51
- map "version", to: :version
52
- map "unique_id", to: :unique_id
53
- map "description", to: :description
54
- map "designer", to: :designer
55
- map "designer_url", to: :designer_url
56
- map "manufacturer", to: :manufacturer
57
- map "vendor_url", to: :vendor_url
58
- map "vendor_id", to: :vendor_id
59
- map "trademark", to: :trademark
60
- map "copyright", to: :copyright
61
- map "license_description", to: :license_description
62
- map "license_url", to: :license_url
63
- map "sample_text", to: :sample_text
64
- map "font_revision", to: :font_revision
65
- map "permissions", to: :permissions
66
- map "units_per_em", to: :units_per_em
67
- end
68
-
69
- yaml do
41
+ key_value do
70
42
  map "font_format", to: :font_format
71
43
  map "is_variable", to: :is_variable
72
44
  map "family_name", to: :family_name
@@ -93,6 +65,7 @@ module Fontisan
93
65
  map "font_revision", to: :font_revision
94
66
  map "permissions", to: :permissions
95
67
  map "units_per_em", to: :units_per_em
68
+ map "collection_offset", to: :collection_offset
96
69
  end
97
70
  end
98
71
  end
@@ -76,7 +76,7 @@ module Fontisan
76
76
  {
77
77
  type: h.type,
78
78
  data: h.data,
79
- source_format: h.source_format
79
+ source_format: h.source_format,
80
80
  }
81
81
  end
82
82
  glyph_hints_hash[glyph_id.to_s] = hints_data
@@ -124,6 +124,7 @@ module Fontisan
124
124
  # Parse glyph hints JSON
125
125
  def parse_glyph_hints
126
126
  return {} if @glyph_hints.nil? || @glyph_hints.empty? || @glyph_hints == "{}"
127
+
127
128
  JSON.parse(@glyph_hints)
128
129
  rescue JSON::ParserError
129
130
  {}
@@ -202,7 +203,7 @@ module Fontisan
202
203
  when :interpolate
203
204
  # IUP instruction
204
205
  axis = data[:axis] || :y
205
- axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
206
+ axis == :x ? [0x31] : [0x30] # IUP[x] or IUP[y]
206
207
  when :shift
207
208
  # SHP instruction
208
209
  data[:instructions] || []
@@ -268,23 +269,22 @@ module Fontisan
268
269
 
269
270
  # Convert stem hint to TrueType instructions
270
271
  def convert_stem_to_truetype
271
- position = data[:position] || 0
272
- width = data[:width] || 0
272
+ data[:position] || 0
273
+ data[:width] || 0
273
274
  orientation = data[:orientation] || :vertical
274
275
 
275
276
  # TrueType uses MDAP (Move Direct Absolute Point) and MDRP (Move Direct Relative Point)
276
277
  # to control stem positioning
277
278
  instructions = []
278
279
 
279
- if orientation == :vertical
280
- # Vertical stem: use Y-axis instructions
281
- instructions << 0x2E # MDAP[rnd] - mark reference point
282
- instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
283
- else
284
- # Horizontal stem: use X-axis instructions
285
- instructions << 0x2F # MDAP[rnd]
286
- instructions << 0xC0 # MDRP[min,rnd,black]
287
- end
280
+ instructions << if orientation == :vertical
281
+ # Vertical stem: use Y-axis instructions
282
+ 0x2E # MDAP[rnd] - mark reference point
283
+ else
284
+ # Horizontal stem: use X-axis instructions
285
+ 0x2F # MDAP[rnd]
286
+ end
287
+ instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
288
288
 
289
289
  instructions
290
290
  end
@@ -313,16 +313,15 @@ module Fontisan
313
313
  # Generate MDAP/MDRP pairs for each stem
314
314
  instructions = []
315
315
 
316
- stems.each do |stem|
317
- if orientation == :vertical
318
- # Vertical stem: use Y-axis instructions
319
- instructions << 0x2E # MDAP[rnd] - mark reference point
320
- instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
321
- else
322
- # Horizontal stem: use X-axis instructions
323
- instructions << 0x2F # MDAP[rnd]
324
- instructions << 0xC0 # MDRP[min,rnd,black]
325
- end
316
+ stems.each do |_stem|
317
+ instructions << if orientation == :vertical
318
+ # Vertical stem: use Y-axis instructions
319
+ 0x2E # MDAP[rnd] - mark reference point
320
+ else
321
+ # Horizontal stem: use X-axis instructions
322
+ 0x2F # MDAP[rnd]
323
+ end
324
+ instructions << 0xC0 # MDRP[min,rnd,black] - move relative point
326
325
  end
327
326
 
328
327
  instructions
@@ -121,7 +121,10 @@ module Fontisan
121
121
 
122
122
  # Get path from CharString
123
123
  path = charstring.path
124
- raise ArgumentError, "CharString has no path data" if path.nil? || path.empty?
124
+ if path.nil? || path.empty?
125
+ raise ArgumentError,
126
+ "CharString has no path data"
127
+ end
125
128
 
126
129
  commands = convert_cff_path_to_commands(path)
127
130
 
@@ -29,7 +29,7 @@ module Fontisan
29
29
  attribute :severity, :string
30
30
  attribute :category, :string
31
31
  attribute :message, :string
32
- attribute :location, :string, default: -> { nil }
32
+ attribute :location, :string, default: -> {}
33
33
 
34
34
  yaml do
35
35
  map "severity", to: :severity
@@ -1,13 +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
- # OpenType Collection domain object using BinData
6
+ # OpenType Collection domain object
8
7
  #
9
- # Represents a complete OpenType Collection file (OTC) using BinData's declarative
10
- # DSL for binary structure definition. Parallel to TrueTypeCollection but for OpenType fonts.
8
+ # Represents a complete OpenType Collection file (OTC). Inherits all shared
9
+ # functionality from BaseCollection and implements OTC-specific behavior.
11
10
  #
12
11
  # @example Reading and extracting fonts
13
12
  # File.open("fonts.otc", "rb") do |io|
@@ -15,39 +14,26 @@ module Fontisan
15
14
  # puts otc.num_fonts # => 4
16
15
  # fonts = otc.extract_fonts(io) # => [OpenTypeFont, OpenTypeFont, ...]
17
16
  # end
18
- class OpenTypeCollection < BinData::Record
19
- endian :big
20
-
21
- string :tag, length: 4, assert: "ttcf"
22
- uint16 :major_version
23
- uint16 :minor_version
24
- uint32 :num_fonts
25
- array :font_offsets, type: :uint32, initial_length: :num_fonts
26
-
27
- # Read OpenType Collection from a file
17
+ class OpenTypeCollection < BaseCollection
18
+ # Get the font class for OpenType collections
28
19
  #
29
- # @param path [String] Path to the OTC file
30
- # @return [OpenTypeCollection] A new instance
31
- # @raise [ArgumentError] if path is nil or empty
32
- # @raise [Errno::ENOENT] if file does not exist
33
- # @raise [RuntimeError] if file format is invalid
34
- def self.from_file(path)
35
- if path.nil? || path.to_s.empty?
36
- raise ArgumentError,
37
- "path cannot be nil or empty"
38
- end
39
- raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
20
+ # @return [Class] OpenTypeFont class
21
+ def self.font_class
22
+ require_relative "open_type_font"
23
+ OpenTypeFont
24
+ end
40
25
 
41
- File.open(path, "rb") { |io| read(io) }
42
- rescue BinData::ValidityError => e
43
- raise "Invalid OTC file: #{e.message}"
44
- rescue EOFError => e
45
- raise "Invalid OTC file: unexpected end of file - #{e.message}"
26
+ # Get the collection format identifier
27
+ #
28
+ # @return [String] "OTC" for OpenType Collection
29
+ def self.collection_format
30
+ "OTC"
46
31
  end
47
32
 
48
33
  # Extract fonts as OpenTypeFont objects
49
34
  #
50
35
  # Reads each font from the OTC file and returns them as OpenTypeFont objects.
36
+ # This method uses the from_collection method.
51
37
  #
52
38
  # @param io [IO] Open file handle to read fonts from
53
39
  # @return [Array<OpenTypeFont>] Array of font objects
@@ -58,194 +44,5 @@ module Fontisan
58
44
  OpenTypeFont.from_collection(io, offset)
59
45
  end
60
46
  end
61
-
62
- # Get a single font from the collection
63
- #
64
- # @param index [Integer] Index of the font (0-based)
65
- # @param io [IO] Open file handle
66
- # @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
67
- # @return [OpenTypeFont, nil] Font object or nil if index out of range
68
- def font(index, io, mode: LoadingModes::FULL)
69
- return nil if index >= num_fonts
70
-
71
- require_relative "open_type_font"
72
- OpenTypeFont.from_collection(io, font_offsets[index], mode: mode)
73
- end
74
-
75
- # Get font count
76
- #
77
- # @return [Integer] Number of fonts in collection
78
- def font_count
79
- num_fonts
80
- end
81
-
82
- # Validate format correctness
83
- #
84
- # @return [Boolean] true if the format is valid, false otherwise
85
- def valid?
86
- tag == Constants::TTC_TAG && num_fonts.positive? && font_offsets.length == num_fonts
87
- rescue StandardError
88
- false
89
- end
90
-
91
- # Get the OTC version as a single integer
92
- #
93
- # @return [Integer] Version number (e.g., 0x00010000 for version 1.0)
94
- def version
95
- (major_version << 16) | minor_version
96
- end
97
-
98
- # List all fonts in the collection with basic metadata
99
- #
100
- # Returns a CollectionListInfo model containing summaries of all fonts.
101
- # This is the API method used by the `ls` command for collections.
102
- #
103
- # @param io [IO] Open file handle to read fonts from
104
- # @return [CollectionListInfo] List of fonts with metadata
105
- #
106
- # @example List fonts in collection
107
- # File.open("fonts.otc", "rb") do |io|
108
- # otc = OpenTypeCollection.read(io)
109
- # list = otc.list_fonts(io)
110
- # list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
111
- # end
112
- def list_fonts(io)
113
- require_relative "models/collection_list_info"
114
- require_relative "models/collection_font_summary"
115
- require_relative "open_type_font"
116
- require_relative "tables/name"
117
-
118
- fonts = font_offsets.map.with_index do |offset, index|
119
- font = OpenTypeFont.from_collection(io, offset)
120
-
121
- # Extract basic font info
122
- name_table = font.table("name")
123
- post_table = font.table("post")
124
-
125
- family_name = name_table&.english_name(Tables::Name::FAMILY) || "Unknown"
126
- subfamily_name = name_table&.english_name(Tables::Name::SUBFAMILY) || "Regular"
127
- postscript_name = name_table&.english_name(Tables::Name::POSTSCRIPT_NAME) || "Unknown"
128
-
129
- # Determine font format
130
- sfnt = font.header.sfnt_version
131
- font_format = case sfnt
132
- when 0x00010000, 0x74727565 # 0x74727565 = 'true'
133
- "TrueType"
134
- when 0x4F54544F # 'OTTO'
135
- "OpenType"
136
- else
137
- "Unknown"
138
- end
139
-
140
- num_glyphs = post_table&.glyph_names&.length || 0
141
- num_tables = font.table_names.length
142
-
143
- Models::CollectionFontSummary.new(
144
- index: index,
145
- family_name: family_name,
146
- subfamily_name: subfamily_name,
147
- postscript_name: postscript_name,
148
- font_format: font_format,
149
- num_glyphs: num_glyphs,
150
- num_tables: num_tables,
151
- )
152
- end
153
-
154
- Models::CollectionListInfo.new(
155
- collection_path: nil, # Will be set by command
156
- num_fonts: num_fonts,
157
- fonts: fonts,
158
- )
159
- end
160
-
161
- # Get comprehensive collection metadata
162
- #
163
- # Returns a CollectionInfo model with header information, offsets,
164
- # and table sharing statistics.
165
- # This is the API method used by the `info` command for collections.
166
- #
167
- # @param io [IO] Open file handle to read fonts from
168
- # @param path [String] Collection file path (for file size)
169
- # @return [CollectionInfo] Collection metadata
170
- #
171
- # @example Get collection info
172
- # File.open("fonts.otc", "rb") do |io|
173
- # otc = OpenTypeCollection.read(io)
174
- # info = otc.collection_info(io, "fonts.otc")
175
- # puts "Version: #{info.version_string}"
176
- # end
177
- def collection_info(io, path)
178
- require_relative "models/collection_info"
179
- require_relative "models/table_sharing_info"
180
-
181
- # Calculate table sharing statistics
182
- table_sharing = calculate_table_sharing(io)
183
-
184
- # Get file size
185
- file_size = path ? File.size(path) : 0
186
-
187
- Models::CollectionInfo.new(
188
- collection_path: path,
189
- collection_format: "OTC",
190
- ttc_tag: tag,
191
- major_version: major_version,
192
- minor_version: minor_version,
193
- num_fonts: num_fonts,
194
- font_offsets: font_offsets.to_a,
195
- file_size_bytes: file_size,
196
- table_sharing: table_sharing,
197
- )
198
- end
199
-
200
- private
201
-
202
- # Calculate table sharing statistics
203
- #
204
- # Analyzes which tables are shared between fonts and calculates
205
- # space savings from deduplication.
206
- #
207
- # @param io [IO] Open file handle
208
- # @return [TableSharingInfo] Sharing statistics
209
- def calculate_table_sharing(io)
210
- require_relative "models/table_sharing_info"
211
- require_relative "open_type_font"
212
-
213
- # Extract all fonts
214
- fonts = font_offsets.map do |offset|
215
- OpenTypeFont.from_collection(io, offset)
216
- end
217
-
218
- # Build table hash map (checksum -> size)
219
- table_map = {}
220
- total_table_size = 0
221
-
222
- fonts.each do |font|
223
- font.tables.each do |entry|
224
- key = entry.checksum
225
- size = entry.table_length
226
- table_map[key] ||= size
227
- total_table_size += size
228
- end
229
- end
230
-
231
- # Count unique vs shared
232
- unique_tables = table_map.size
233
- total_tables = fonts.sum { |f| f.tables.length }
234
- shared_tables = total_tables - unique_tables
235
-
236
- # Calculate space saved
237
- unique_size = table_map.values.sum
238
- space_saved = total_table_size - unique_size
239
-
240
- # Calculate sharing percentage
241
- sharing_pct = total_tables.positive? ? (shared_tables.to_f / total_tables * 100).round(2) : 0.0
242
-
243
- Models::TableSharingInfo.new(
244
- shared_tables: shared_tables,
245
- unique_tables: unique_tables,
246
- sharing_percentage: sharing_pct,
247
- space_saved_bytes: space_saved,
248
- )
249
- end
250
47
  end
251
48
  end
@@ -211,7 +211,8 @@ module Fontisan
211
211
  batch_entries.each do |entry|
212
212
  relative_offset = entry.offset - batch_offset
213
213
  tag_key = entry.tag.dup.force_encoding("UTF-8")
214
- @table_data[tag_key] = batch_data[relative_offset, entry.table_length]
214
+ @table_data[tag_key] =
215
+ batch_data[relative_offset, entry.table_length]
215
216
  end
216
217
  end
217
218
 
@@ -286,6 +287,7 @@ module Fontisan
286
287
  # @return [Boolean] true if table is available in current mode
287
288
  def table_available?(tag)
288
289
  return false unless has_table?(tag)
290
+
289
291
  LoadingModes.table_allowed?(@loading_mode, tag)
290
292
  end
291
293
 
@@ -222,7 +222,8 @@ module Fontisan
222
222
  if @stack_aware
223
223
  tracker = @stack_trackers[glyph_id]
224
224
  next unless tracker
225
- next unless tracker.stack_neutral?(start_pos, start_pos + length)
225
+ next unless tracker.stack_neutral?(start_pos,
226
+ start_pos + length)
226
227
  end
227
228
 
228
229
  pattern_bytes = charstring[start_pos, length]
@@ -74,7 +74,7 @@ module Fontisan
74
74
  # 2. Analyze patterns
75
75
  analyzer = PatternAnalyzer.new(
76
76
  min_length: @min_pattern_length,
77
- stack_aware: true
77
+ stack_aware: true,
78
78
  )
79
79
  patterns = analyzer.analyze(charstrings)
80
80
 
@@ -69,7 +69,10 @@ module Fontisan
69
69
  # @return [Integer] Number of bytes written
70
70
  def write_svg(result)
71
71
  svg_xml = result[:svg_xml] || result["svg_xml"]
72
- raise ArgumentError, "SVG result must contain :svg_xml key" unless svg_xml
72
+ unless svg_xml
73
+ raise ArgumentError,
74
+ "SVG result must contain :svg_xml key"
75
+ end
73
76
 
74
77
  File.write(@output_path, svg_xml)
75
78
  end
@@ -80,7 +83,8 @@ module Fontisan
80
83
  # @return [Integer] Number of bytes written
81
84
  def write_sfnt(tables)
82
85
  sfnt_version = determine_sfnt_version
83
- FontWriter.write_to_file(tables, @output_path, sfnt_version: sfnt_version)
86
+ FontWriter.write_to_file(tables, @output_path,
87
+ sfnt_version: sfnt_version)
84
88
  end
85
89
 
86
90
  # Write WOFF format
@@ -145,7 +149,8 @@ module Fontisan
145
149
  when :otf
146
150
  OpenTypeFont.from_tables(tables)
147
151
  else
148
- raise ArgumentError, "Cannot determine font type for format: #{@format}"
152
+ raise ArgumentError,
153
+ "Cannot determine font type for format: #{@format}"
149
154
  end
150
155
  end
151
156
  end
@@ -294,7 +294,8 @@ module Fontisan
294
294
  when ".woff" then :woff
295
295
  when ".woff2" then :woff2
296
296
  else
297
- raise ArgumentError, "Cannot determine target format from extension: #{ext}"
297
+ raise ArgumentError,
298
+ "Cannot determine target format from extension: #{ext}"
298
299
  end
299
300
  end
300
301
 
@@ -304,7 +305,10 @@ module Fontisan
304
305
  def variation_options
305
306
  opts = {}
306
307
  opts[:coordinates] = @options[:coordinates] if @options[:coordinates]
307
- opts[:instance_index] = @options[:instance_index] if @options[:instance_index]
308
+ if @options[:instance_index]
309
+ opts[:instance_index] =
310
+ @options[:instance_index]
311
+ end
308
312
  opts
309
313
  end
310
314
 
@@ -344,7 +348,8 @@ module Fontisan
344
348
  when :otf
345
349
  OpenTypeFont.from_tables(tables)
346
350
  else
347
- raise ArgumentError, "Cannot determine font type: format=#{format}, has_cff=#{has_cff}, has_glyf=#{has_glyf}"
351
+ raise ArgumentError,
352
+ "Cannot determine font type: format=#{format}, has_cff=#{has_cff}, has_glyf=#{has_glyf}"
348
353
  end
349
354
  end
350
355
  end
@@ -122,11 +122,11 @@ module Fontisan
122
122
  data = table.to_binary_s.dup
123
123
 
124
124
  # Calculate new numberOfHMetrics
125
- new_num_h_metrics = if hmtx && hmtx.h_metrics
126
- hmtx.h_metrics.size
127
- else
128
- calculate_number_of_h_metrics
129
- end
125
+ new_num_h_metrics = if hmtx&.h_metrics
126
+ hmtx.h_metrics.size
127
+ else
128
+ calculate_number_of_h_metrics
129
+ end
130
130
 
131
131
  # Update numberOfHMetrics field (at offset 34, uint16)
132
132
  data[34, 2] = [new_num_h_metrics].pack("n")