fontisan 0.4.45 → 0.4.47

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -2
  3. data/lib/fontisan/cli.rb +1 -1
  4. data/lib/fontisan/commands/export_command.rb +3 -3
  5. data/lib/fontisan/export/exporter.rb +1 -1
  6. data/lib/fontisan/export/table_serializer.rb +36 -38
  7. data/lib/fontisan/pipeline/transformation_pipeline.rb +10 -8
  8. data/lib/fontisan/pipeline.rb +1 -1
  9. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +1 -2
  10. data/lib/fontisan/tables/cff/standard_strings_data.rb +1 -2
  11. data/lib/fontisan/tables/glyf.rb +4 -6
  12. data/lib/fontisan/tables/gpos/glyph_filter.rb +87 -0
  13. data/lib/fontisan/tables/gpos/kerning_pairs.rb +215 -0
  14. data/lib/fontisan/tables/gpos/pair_position.rb +274 -0
  15. data/lib/fontisan/tables/gpos.rb +23 -0
  16. data/lib/fontisan/tables/post.rb +2 -4
  17. data/lib/fontisan/validators/opentype_validator.rb +13 -3
  18. data/lib/fontisan/variable/instancer.rb +22 -192
  19. data/lib/fontisan/variation/instance_generator.rb +3 -149
  20. data/lib/fontisan/variation/instancing/axis_space.rb +136 -0
  21. data/lib/fontisan/variation/instancing/glyph_outline.rb +204 -0
  22. data/lib/fontisan/variation/instancing/gvar_reader.rb +146 -0
  23. data/lib/fontisan/variation/instancing/item_variation_store.rb +92 -0
  24. data/lib/fontisan/variation/instancing/name_rewriter.rb +90 -0
  25. data/lib/fontisan/variation/instancing/true_type_instancer.rb +318 -0
  26. data/lib/fontisan/variation/instancing.rb +20 -0
  27. data/lib/fontisan/variation/variable_svg_generator.rb +19 -1
  28. data/lib/fontisan/variation.rb +1 -0
  29. data/lib/fontisan/version.rb +1 -1
  30. metadata +12 -3
  31. data/fontisan.gemspec +0 -48
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fontisan"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ class Gpos
8
+ # Checked views share physical identity without copying table suffixes.
9
+ class BinaryView
10
+ def initialize(data, offset: 0, cache: {})
11
+ @data = data
12
+ @offset = offset
13
+ @cache = cache
14
+ end
15
+
16
+ def bytes(offset, length)
17
+ if offset.negative? || length.negative? || @offset + offset + length > @data.bytesize
18
+ raise ParseError, "Truncated GPOS data at byte #{offset}"
19
+ end
20
+
21
+ @data.byteslice(@offset + offset, length)
22
+ end
23
+
24
+ def uint16(offset)
25
+ bytes(offset, 2).unpack1("n")
26
+ end
27
+
28
+ def uint32(offset)
29
+ bytes(offset, 4).unpack1("N")
30
+ end
31
+
32
+ def array(offset, count)
33
+ bytes(offset, count * 2).unpack("n*")
34
+ end
35
+
36
+ def child(offset, minimum:)
37
+ raise ParseError, "Invalid GPOS offset #{offset}" if offset < minimum
38
+
39
+ bytes(offset, 1)
40
+ self.class.new(@data, offset: @offset + offset, cache: @cache)
41
+ end
42
+
43
+ def record(type, header:, count_offset:, item_size:)
44
+ bytes(0, header + uint16(count_offset) * item_size)
45
+ type.read(@data.byteslice(@offset..))
46
+ end
47
+
48
+ def cached(context)
49
+ key = [@offset, context]
50
+ @cache.fetch(key) { @cache[key] = yield }
51
+ end
52
+
53
+ def freeze
54
+ @cache.freeze
55
+ super
56
+ end
57
+ end
58
+
59
+ # Coverage ranges remain compact, including the full glyph-ID domain.
60
+ class Coverage
61
+ include Enumerable
62
+
63
+ attr_reader :size
64
+
65
+ def initialize(view)
66
+ format = view.uint16(0)
67
+ count = view.uint16(2)
68
+ case format
69
+ when 1
70
+ @glyphs = view.array(4, count).freeze
71
+ unless @glyphs.each_cons(2).all? { |first, second| first < second }
72
+ raise ParseError, "Unsorted or duplicate GPOS glyph IDs"
73
+ end
74
+
75
+ @size = count
76
+ when 2
77
+ @ranges = view.array(4, count * 3).each_slice(3).map(&:freeze).freeze
78
+ @size = 0
79
+ previous = -1
80
+ @ranges.each do |first, last, index|
81
+ if first > last || index != @size || first <= previous
82
+ raise ParseError, "Invalid GPOS coverage range"
83
+ end
84
+
85
+ @size += last - first + 1
86
+ previous = last
87
+ end
88
+ else
89
+ raise ParseError, "Unsupported Coverage format #{format}"
90
+ end
91
+ freeze
92
+ end
93
+
94
+ def include?(glyph)
95
+ return @glyphs.bsearch { |candidate| candidate >= glyph } == glyph if @glyphs
96
+
97
+ record = @ranges.bsearch { |_first, last, _index| last >= glyph }
98
+ record && record[0] <= glyph
99
+ end
100
+
101
+ def each(&block)
102
+ return enum_for(:each) unless block
103
+
104
+ if @glyphs
105
+ @glyphs.each(&block)
106
+ else
107
+ @ranges.each do |first, last, _index|
108
+ (first..last).each(&block)
109
+ end
110
+ end
111
+ self
112
+ end
113
+ end
114
+
115
+ # Class arrays support direct glyph indexing without singleton ranges.
116
+ class ClassDefinition
117
+ attr_reader :maximum
118
+
119
+ def initialize(view)
120
+ @view = view
121
+ case view.uint16(0)
122
+ when 1
123
+ @first = view.uint16(2)
124
+ @count = view.uint16(4)
125
+ raise ParseError, "Invalid GPOS class definition" if @first + @count > 65536
126
+
127
+ @maximum = view.array(6, @count).max || 0
128
+ when 2
129
+ @ranges = view.array(4, view.uint16(2) * 3).each_slice(3).map(&:freeze).freeze
130
+ previous = -1
131
+ @maximum = 0
132
+ @ranges.each do |first, last, value|
133
+ if first <= previous || first > last
134
+ raise ParseError, "Invalid GPOS class definition"
135
+ end
136
+
137
+ @maximum = [@maximum, value].max
138
+ previous = last
139
+ end
140
+ else
141
+ raise ParseError, "Unsupported ClassDef format #{view.uint16(0)}"
142
+ end
143
+ freeze
144
+ end
145
+
146
+ def [](glyph)
147
+ if @ranges
148
+ record = @ranges.bsearch { |_first, last, _value| last >= glyph }
149
+ record && record[0] <= glyph ? record[2] : 0
150
+ else
151
+ index = glyph - @first
152
+ index.between?(0, @count - 1) ? @view.uint16(6 + index * 2) : 0
153
+ end
154
+ end
155
+ end
156
+
157
+ # Compact PairPos data; class matrices stay in their binary representation.
158
+ class PairPosition
159
+ attr_reader :left_glyphs
160
+
161
+ def initialize(view)
162
+ @view = view
163
+ @format = view.uint16(0)
164
+ raise ParseError, "Unsupported PairPos format #{@format}" unless [1, 2].include?(@format)
165
+
166
+ @first_format = view.uint16(4)
167
+ @second_format = view.uint16(6)
168
+ @first_size = value_size(@first_format)
169
+ @record_size = @first_size + value_size(@second_format)
170
+ @advance_offset = value_size(@first_format & 3)
171
+ @coverage = coverage
172
+ @left_glyphs = @coverage
173
+ @format == 1 ? parse_pairs : parse_classes
174
+ freeze
175
+ end
176
+
177
+ def class_based?
178
+ @format == 2
179
+ end
180
+
181
+ def covers?(glyph)
182
+ @coverage.include?(glyph)
183
+ end
184
+
185
+ def adjustment(left, right)
186
+ return unless covers?(left)
187
+ return @pairs.fetch(left)[right] unless class_based?
188
+
189
+ row = @first_classes[left]
190
+ column = @second_classes[right]
191
+ advance(@matrix, (row * @second_count + column) * @record_size)
192
+ end
193
+
194
+ def right_glyphs(left)
195
+ @pairs.fetch(left).each_key
196
+ end
197
+
198
+ private
199
+
200
+ def value_size(format)
201
+ raise ParseError, "Reserved GPOS ValueFormat bits" unless (format & 0xff00).zero?
202
+
203
+ format.digits(2).sum * 2
204
+ end
205
+
206
+ def advance(view, offset)
207
+ return 0 if (@first_format & 4).zero?
208
+
209
+ value = view.uint16(offset + @advance_offset)
210
+ value >= 0x8000 ? value - 0x10000 : value
211
+ end
212
+
213
+ def coverage
214
+ minimum = @format == 1 ? 10 + @view.uint16(8) * 2 : 16 + matrix_size
215
+ view = @view.child(@view.uint16(2), minimum: minimum)
216
+ view.cached(:coverage) { Coverage.new(view) }
217
+ end
218
+
219
+ def parse_pairs
220
+ count = @view.uint16(8)
221
+ raise ParseError, "GPOS PairSet count differs from coverage" unless count == left_glyphs.size
222
+
223
+ offsets = @view.array(10, count)
224
+ @pairs = {}
225
+ left_glyphs.each_with_index do |left, index|
226
+ view = @view.child(offsets[index], minimum: 10 + count * 2)
227
+ @pairs[left] = view.cached([:pair_set, @first_format, @second_format]) { pair_set(view) }
228
+ end
229
+ @pairs.freeze
230
+ end
231
+
232
+ def pair_set(view)
233
+ count = view.uint16(0)
234
+ view.bytes(2, count * (2 + @record_size))
235
+ previous = -1
236
+ pairs = {}
237
+ count.times do |index|
238
+ start = 2 + index * (2 + @record_size)
239
+ glyph = view.uint16(start)
240
+ raise ParseError, "Unsorted or duplicate GPOS glyph IDs" if glyph <= previous
241
+
242
+ pairs[glyph] = advance(view, start + 2)
243
+ previous = glyph
244
+ end
245
+ pairs.freeze
246
+ end
247
+
248
+ def matrix_size
249
+ @view.uint16(12) * @view.uint16(14) * @record_size
250
+ end
251
+
252
+ def parse_classes
253
+ first_count = @view.uint16(12)
254
+ @second_count = @view.uint16(14)
255
+ raise ParseError, "Empty GPOS class matrix" if first_count.zero? || @second_count.zero?
256
+
257
+ @matrix = BinaryView.new(@view.bytes(16, matrix_size).freeze).freeze
258
+ @first_classes = class_definition(@view.uint16(8), first_count)
259
+ @second_classes = class_definition(@view.uint16(10), @second_count)
260
+ end
261
+
262
+ def class_definition(offset, class_count)
263
+ view = @view.child(offset, minimum: 16 + matrix_size)
264
+ definition = view.cached(:class_definition) { ClassDefinition.new(view) }
265
+ raise ParseError, "Invalid GPOS class definition" if definition.maximum >= class_count
266
+
267
+ definition
268
+ end
269
+ end
270
+
271
+ private_constant :BinaryView, :Coverage, :ClassDefinition, :PairPosition
272
+ end
273
+ end
274
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fontisan"
4
+
3
5
  module Fontisan
4
6
  module Tables
5
7
  # GPOS (Glyph Positioning) table parser
@@ -55,6 +57,25 @@ module Fontisan
55
57
  []
56
58
  end
57
59
 
60
+ # Read PairPos value1.xAdvance in font units, directly or via extensions.
61
+ # The returned Enumerable supports pairs[[left_glyph_id, right_glyph_id]];
62
+ # unmatched pairs return zero. Use glyph_count: font.table("maxp").num_glyphs.to_i
63
+ # to enumerate class-based pairs, including implicit class zero, with to_h.
64
+ # Selects the script's default language system, with no script fallback.
65
+ # First matching subtables take precedence; distinct lookups are summed.
66
+ # Supports PairPos formats 1 and 2 in lookup types 2 and 9. Pass raw GDEF
67
+ # bytes with gdef: font.table_data["GDEF"] for glyph-filtering lookups.
68
+ # Lazy SFNT fonts can use font.sfnt_table("GDEF").load_data!.data instead.
69
+ # Filters apply to supplied pair endpoints; this does not shape a stream.
70
+ # Other lookup types, device corrections and variations are not applied.
71
+ # @return [Enumerable] nonzero glyph-ID pairs and signed advances
72
+ # @raise [ParseError] malformed selected positioning data
73
+ # @raise [UnsupportedFormatError] selected filtering requires missing GDEF data
74
+ def kern_pairs(script_tag: "latn", feature_tag: "kern", glyph_count: nil, gdef: nil)
75
+ KerningPairs.new(self, script_tag: script_tag, feature_tag: feature_tag,
76
+ glyph_count: glyph_count, gdef: gdef)
77
+ end
78
+
58
79
  private
59
80
 
60
81
  # Get feature indices for a given script
@@ -107,3 +128,5 @@ module Fontisan
107
128
  end
108
129
  end
109
130
  end
131
+
132
+ require "fontisan/tables/gpos/kerning_pairs"
@@ -14,7 +14,7 @@ module Fontisan
14
14
 
15
15
  register_tag "post"
16
16
  # Standard Mac glyph names for version 1.0 (258 glyphs)
17
- # rubocop:disable Metrics/CollectionLiteralLength
17
+ # rubocop:disable-next Metrics/CollectionLiteralLength
18
18
  STANDARD_NAMES = %w[
19
19
  .notdef .null nonmarkingreturn space exclam quotedbl numbersign
20
20
  dollar percent ampersand quotesingle parenleft parenright asterisk
@@ -48,7 +48,6 @@ module Fontisan
48
48
  Gbreve gbreve Idotaccent Scedilla scedilla Cacute cacute Ccaron
49
49
  ccaron dcroat
50
50
  ].freeze
51
- # rubocop:enable Metrics/CollectionLiteralLength
52
51
 
53
52
  # Version 2.0 as Fixed 16.16 constant
54
53
  VERSION_2_0_RAW = 131_072 # 2.0 * 65536
@@ -99,7 +98,7 @@ module Fontisan
99
98
  #
100
99
  # Version 2.0 uses a combination of standard Mac names (indices 0-257)
101
100
  # and custom names (indices >= 258) stored as Pascal strings.
102
- # rubocop:disable Metrics/PerceivedComplexity
101
+ # rubocop:disable-next Metrics/PerceivedComplexity
103
102
  def parse_version_2_names
104
103
  return [] unless version_raw == VERSION_2_0_RAW
105
104
  return [] if remaining_data.empty?
@@ -145,7 +144,6 @@ module Fontisan
145
144
  end
146
145
  end
147
146
  end
148
- # rubocop:enable Metrics/PerceivedComplexity
149
147
 
150
148
  public
151
149
 
@@ -44,7 +44,7 @@ module Fontisan
44
44
  glyf = font.table("glyf")
45
45
  next true unless glyf # Skip if CFF font
46
46
 
47
- loca = font.table("loca")
47
+ loca = parsed_loca(font)
48
48
  head = font.table("head")
49
49
  maxp = font.table("maxp")
50
50
  glyf.all_glyphs_accessible?(loca, head, maxp.num_glyphs)
@@ -55,7 +55,7 @@ module Fontisan
55
55
  glyf = font.table("glyf")
56
56
  next true unless glyf
57
57
 
58
- loca = font.table("loca")
58
+ loca = parsed_loca(font)
59
59
  head = font.table("head")
60
60
  maxp = font.table("maxp")
61
61
  glyf.no_clipped_glyphs?(loca, head, maxp.num_glyphs)
@@ -66,7 +66,7 @@ module Fontisan
66
66
  glyf = font.table("glyf")
67
67
  next true unless glyf
68
68
 
69
- loca = font.table("loca")
69
+ loca = parsed_loca(font)
70
70
  head = font.table("head")
71
71
  maxp = font.table("maxp")
72
72
 
@@ -101,6 +101,16 @@ module Fontisan
101
101
  true # Placeholder - actual checksum validation if desired
102
102
  end
103
103
  end
104
+
105
+ # loca must be parsed with head/maxp context before glyf can read it.
106
+ def parsed_loca(font)
107
+ loca = font.table("loca")
108
+ return loca if loca.parsed?
109
+
110
+ loca.parse_with_context(font.table("head").index_to_loc_format,
111
+ font.table("maxp").num_glyphs)
112
+ loca
113
+ end
104
114
  end
105
115
  end
106
116
  end
@@ -25,12 +25,6 @@ module Fontisan
25
25
  # @return [Object] The variable font object
26
26
  attr_reader :font
27
27
 
28
- # @return [DeltaApplicator] Delta applicator
29
- attr_reader :delta_applicator
30
-
31
- # @return [StaticFontBuilder] Static font builder
32
- attr_reader :static_font_builder
33
-
34
28
  # Initialize the instancer
35
29
  #
36
30
  # @param font [TrueTypeFont, OpenTypeFont] Variable font object
@@ -38,9 +32,6 @@ module Fontisan
38
32
  def initialize(font)
39
33
  @font = font
40
34
  validate_variable_font!
41
-
42
- @delta_applicator = DeltaApplicator.new(font)
43
- @static_font_builder = StaticFontBuilder.new(font)
44
35
  end
45
36
 
46
37
  # Generate static font instance at specified coordinates
@@ -53,21 +44,10 @@ module Fontisan
53
44
  #
54
45
  # @example
55
46
  # binary = instancer.instance({ "wght" => 700, "wdth" => 100 })
56
- def instance(user_coords, options = {})
57
- # Apply deltas to get all varied data
58
- delta_result = @delta_applicator.apply(user_coords)
59
-
60
- # Collect varied metrics for all glyphs
61
- varied_metrics = collect_varied_glyph_metrics(
62
- delta_result[:normalized_coords],
63
- delta_result[:region_scalars],
64
- )
65
-
66
- # Extract font-level metrics
67
- font_metrics = delta_result[:font_metrics]
68
-
69
- # Build static font
70
- @static_font_builder.build(varied_metrics, font_metrics, options)
47
+ def instance(user_coords, _options = {})
48
+ tables = Variation::Instancing::TrueTypeInstancer
49
+ .new(@font.table_data, user_coords).call
50
+ FontWriter.write_font(tables)
71
51
  end
72
52
 
73
53
  # Generate static font instance and write to file
@@ -117,23 +97,16 @@ module Fontisan
117
97
  # @return [Array<Hash>] Array of named instance information
118
98
  # [{ name: "Bold", coords: { "wght" => 700 } }, ...]
119
99
  def named_instances
120
- fvar = load_fvar_table
121
- return [] unless fvar
122
-
123
- # Get name table for instance names
124
- name_table = load_name_table
125
-
126
- fvar.instances.map do |instance|
127
- coords = extract_instance_coords(instance, fvar)
128
- name = if name_table
129
- get_instance_name(instance[:name_id],
130
- name_table)
131
- end
132
-
100
+ fvar = @font.table_data["fvar"]
101
+ names = if @font.table_data["name"]
102
+ Variation::Instancing::NameRewriter.new(@font.table_data["name"])
103
+ end
104
+ tags = axis_tags
105
+ Variation::Instancing::AxisSpace.named_instances(fvar).map do |inst|
133
106
  {
134
- name_id: instance[:name_id],
135
- name: name || "Instance #{instance[:name_id]}",
136
- coordinates: coords,
107
+ name_id: inst[:name_id],
108
+ name: names&.lookup(inst[:name_id]) || "Instance #{inst[:name_id]}",
109
+ coordinates: tags.zip(inst[:values]).to_h,
137
110
  }
138
111
  end
139
112
  end
@@ -142,21 +115,21 @@ module Fontisan
142
115
  #
143
116
  # @return [Boolean] True if variable font
144
117
  def variable_font?
145
- @delta_applicator.variable_font?
118
+ true
146
119
  end
147
120
 
148
121
  # Get available axis information
149
122
  #
150
- # @return [Hash] Axis information
123
+ # @return [Array<Variation::Instancing::AxisSpace::Axis>]
151
124
  def axes
152
- @delta_applicator.axes
125
+ axis_space.axes
153
126
  end
154
127
 
155
128
  # Get available axis tags
156
129
  #
157
130
  # @return [Array<String>] Array of axis tags
158
131
  def axis_tags
159
- @delta_applicator.axis_tags
132
+ axis_space.tags
160
133
  end
161
134
 
162
135
  private
@@ -165,60 +138,16 @@ module Fontisan
165
138
  #
166
139
  # @raise [ArgumentError] If not a variable font
167
140
  def validate_variable_font!
168
- fvar_data = @font.table_data("fvar")
141
+ fvar_data = @font.table_data["fvar"]
169
142
  return unless fvar_data.nil? || fvar_data.empty?
170
143
 
171
144
  raise ArgumentError, "Font is not a variable font (missing fvar table)"
172
145
  end
173
146
 
174
- # Collect varied metrics for all glyphs
175
- #
176
- # @param normalized_coords [Hash] Normalized coordinates
177
- # @param region_scalars [Array<Float>] Region scalars
178
- # @return [Hash<Integer, Hash>] Varied metrics by glyph ID
179
- def collect_varied_glyph_metrics(normalized_coords, _region_scalars)
180
- varied_metrics = {}
181
-
182
- # Get number of glyphs
183
- maxp = load_maxp_table
184
- return varied_metrics unless maxp
185
-
186
- num_glyphs = maxp.num_glyphs
187
-
188
- # Get original hmtx metrics
189
- hmtx = load_hmtx_table
190
- return varied_metrics unless hmtx
191
-
192
- # For each glyph, calculate varied metrics
193
- num_glyphs.times do |glyph_id|
194
- original_metric = hmtx.metric_for(glyph_id)
195
- next unless original_metric
196
-
197
- # Get deltas from delta applicator
198
- deltas = @delta_applicator.apply_glyph(glyph_id, normalized_coords)
199
- metric_deltas = deltas[:metric_deltas]
200
-
201
- # Calculate varied values
202
- varied_metric = {
203
- advance_width: original_metric[:advance_width],
204
- lsb: original_metric[:lsb],
205
- }
206
-
207
- # Apply horizontal deltas if present
208
- if metric_deltas[:horizontal]
209
- if metric_deltas[:horizontal][:advance_width]
210
- varied_metric[:advance_width] += metric_deltas[:horizontal][:advance_width]
211
- end
212
-
213
- if metric_deltas[:horizontal][:lsb]
214
- varied_metric[:lsb] += metric_deltas[:horizontal][:lsb]
215
- end
216
- end
217
-
218
- varied_metrics[glyph_id] = varied_metric
219
- end
220
-
221
- varied_metrics
147
+ def axis_space
148
+ @axis_space ||= Variation::Instancing::AxisSpace.new(
149
+ @font.table_data["fvar"], @font.table_data["avar"]
150
+ )
222
151
  end
223
152
 
224
153
  # Find coordinates for a named instance
@@ -237,105 +166,6 @@ module Fontisan
237
166
 
238
167
  instance[:coordinates]
239
168
  end
240
-
241
- # Extract coordinates from instance record
242
- #
243
- # @param instance [Hash] Instance record
244
- # @param fvar [Fvar] fvar table
245
- # @return [Hash<String, Float>] Coordinates by axis tag
246
- def extract_instance_coords(instance, fvar)
247
- coords = {}
248
- instance[:coordinates].each_with_index do |value, index|
249
- axis = fvar.axes[index]
250
- coords[axis.axis_tag.to_s] = value if axis
251
- end
252
- coords
253
- end
254
-
255
- # Get instance name from name table
256
- #
257
- # @param name_id [Integer] Name table ID
258
- # @param name_table [Name] name table
259
- # @return [String, nil] Instance name
260
- def get_instance_name(name_id, name_table)
261
- # Try to get English name
262
- record = name_table.records.find do |r|
263
- r.name_id == name_id && r.language_id == 0x0409 # English (US)
264
- end
265
-
266
- record ||= name_table.records.find { |r| r.name_id == name_id }
267
- record&.value
268
- rescue StandardError
269
- nil
270
- end
271
-
272
- # Load fvar table
273
- #
274
- # @return [Fvar, nil] fvar table or nil
275
- def load_fvar_table
276
- data = @font.table_data("fvar")
277
- return nil if data.nil? || data.empty?
278
-
279
- Tables::Fvar.read(data)
280
- rescue StandardError
281
- nil
282
- end
283
-
284
- # Load name table
285
- #
286
- # @return [Name, nil] name table or nil
287
- def load_name_table
288
- data = @font.table_data("name")
289
- return nil if data.nil? || data.empty?
290
-
291
- Tables::Name.read(data)
292
- rescue StandardError
293
- nil
294
- end
295
-
296
- # Load maxp table
297
- #
298
- # @return [Maxp, nil] maxp table or nil
299
- def load_maxp_table
300
- data = @font.table_data("maxp")
301
- return nil if data.nil? || data.empty?
302
-
303
- Tables::Maxp.read(data)
304
- rescue StandardError
305
- nil
306
- end
307
-
308
- # Load hmtx table
309
- #
310
- # @return [Hmtx, nil] hmtx table or nil
311
- def load_hmtx_table
312
- data = @font.table_data("hmtx")
313
- return nil if data.nil? || data.empty?
314
-
315
- hmtx = Tables::Hmtx.read(data)
316
-
317
- # Parse with context
318
- hhea = load_hhea_table
319
- maxp = load_maxp_table
320
- return nil unless hhea && maxp
321
-
322
- hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
323
- hmtx
324
- rescue StandardError
325
- nil
326
- end
327
-
328
- # Load hhea table
329
- #
330
- # @return [Hhea, nil] hhea table or nil
331
- def load_hhea_table
332
- data = @font.table_data("hhea")
333
- return nil if data.nil? || data.empty?
334
-
335
- Tables::Hhea.read(data)
336
- rescue StandardError
337
- nil
338
- end
339
169
  end
340
170
  end
341
171
  end