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
@@ -54,29 +54,12 @@ module Fontisan
54
54
 
55
55
  # Generate static font instance
56
56
  #
57
- # Applies variation deltas and returns static font tables.
57
+ # Applies gvar/HVAR deltas at the coordinates to the outlines and
58
+ # metrics and drops the variation tables. TrueType (gvar) fonts only.
58
59
  #
59
60
  # @return [Hash<String, String>] Map of table tags to binary data
60
61
  def generate
61
- # Start with base font tables
62
- tables = @font.table_data.dup
63
-
64
- # Determine variation type
65
- if has_variation_table?("gvar")
66
- # TrueType outlines with gvar
67
- apply_gvar_deltas(tables)
68
- elsif has_variation_table?("CFF2")
69
- # PostScript outlines with CFF2 blend
70
- apply_cff2_blend(tables)
71
- end
72
-
73
- # Apply metrics variations if present
74
- apply_metrics_deltas(tables) if @context.has_metrics_variations?
75
-
76
- # Remove variation-specific tables to create static font
77
- remove_variation_tables(tables)
78
-
79
- tables
62
+ Instancing::TrueTypeInstancer.new(@font.table_data, @coordinates).call
80
63
  end
81
64
 
82
65
  # Generate a named instance
@@ -96,135 +79,6 @@ module Fontisan
96
79
  generate
97
80
  end
98
81
 
99
- # Apply gvar deltas to TrueType outlines
100
- #
101
- # @param tables [Hash<String, String>] Font tables
102
- def apply_gvar_deltas(_tables)
103
- gvar = variation_table("gvar")
104
- glyf = @font.table("glyf")
105
- return unless gvar && glyf
106
-
107
- # Get glyph count
108
- maxp = @font.table("maxp")
109
- glyph_count = maxp ? maxp.num_glyphs : gvar.glyph_count
110
-
111
- # Process each glyph
112
- glyph_count.times do |glyph_id|
113
- apply_glyph_deltas(glyph_id, gvar, glyf)
114
- end
115
-
116
- # Rebuild glyf and loca tables with adjusted outlines
117
- # This is a placeholder - full implementation would reconstruct tables
118
- end
119
-
120
- # Apply deltas to a specific glyph
121
- #
122
- # @param glyph_id [Integer] Glyph ID
123
- # @param gvar [Gvar] Gvar table
124
- # @param glyf [Glyf] Glyf table
125
- def apply_glyph_deltas(glyph_id, gvar, _glyf)
126
- # Get tuple variations for this glyph
127
- tuple_data = gvar.glyph_tuple_variations(glyph_id)
128
- return unless tuple_data
129
-
130
- # Match tuples to current coordinates
131
- matches = @context.region_matcher.match_tuples(
132
- coordinates: @coordinates,
133
- tuples: tuple_data[:tuples],
134
- )
135
-
136
- nil if matches.empty?
137
-
138
- # Get base glyph outline
139
- # Apply matched deltas with their scalars
140
- # This is a placeholder - full implementation would:
141
- # 1. Parse glyph outline points
142
- # 2. Parse delta data for each tuple
143
- # 3. Apply: new_point = base_point + Σ(delta * scalar)
144
- # 4. Update glyph outline
145
- end
146
-
147
- # Apply CFF2 blend operators
148
- #
149
- # @param tables [Hash<String, String>] Font tables
150
- def apply_cff2_blend(_tables)
151
- cff2 = variation_table("CFF2")
152
- return unless cff2
153
-
154
- # Set number of axes for CFF2
155
- cff2.num_axes = @context.axis_count
156
-
157
- # Process each glyph's CharString
158
- glyph_count = cff2.glyph_count
159
- return if glyph_count.zero?
160
-
161
- # Calculate variation scalars once
162
- calculate_variation_scalars
163
-
164
- # Apply blend to each glyph
165
- # This is a placeholder - full implementation would:
166
- # 1. Parse CharString with blend operators
167
- # 2. Apply scalars to blend operands
168
- # 3. Rebuild CharStrings without blend operators
169
- # 4. Update CFF2 table
170
- end
171
-
172
- # Calculate variation scalars for current coordinates
173
- #
174
- # @return [Array<Float>] Scalars for each axis
175
- def calculate_variation_scalars
176
- @context.axes.map do |axis|
177
- coord = @coordinates[axis.axis_tag] || axis.default_value
178
- @context.interpolator.normalize_coordinate(coord, axis.axis_tag)
179
- end
180
- end
181
-
182
- # Apply metrics variations
183
- #
184
- # @param tables [Hash<String, String>] Font tables
185
- def apply_metrics_deltas(tables)
186
- # Apply HVAR (horizontal metrics)
187
- apply_hvar_deltas(tables) if has_variation_table?("HVAR")
188
-
189
- # Apply VVAR (vertical metrics)
190
- apply_vvar_deltas(tables) if has_variation_table?("VVAR")
191
-
192
- # Apply MVAR (font-wide metrics)
193
- apply_mvar_deltas(tables) if has_variation_table?("MVAR")
194
- end
195
-
196
- # Apply HVAR deltas to horizontal metrics
197
- #
198
- # @param tables [Hash<String, String>] Font tables
199
- def apply_hvar_deltas(_tables)
200
- adjuster = MetricsAdjuster.new(@font, @context.interpolator)
201
- adjuster.apply_hvar_deltas(@coordinates)
202
- end
203
-
204
- # Apply VVAR deltas to vertical metrics
205
- #
206
- # @param tables [Hash<String, String>] Font tables
207
- def apply_vvar_deltas(_tables)
208
- adjuster = MetricsAdjuster.new(@font, @context.interpolator)
209
- adjuster.apply_vvar_deltas(@coordinates)
210
- end
211
-
212
- # Apply MVAR deltas to font-wide metrics
213
- #
214
- # @param tables [Hash<String, String>] Font tables
215
- def apply_mvar_deltas(_tables)
216
- adjuster = MetricsAdjuster.new(@font, @context.interpolator)
217
- adjuster.apply_mvar_deltas(@coordinates)
218
- end
219
-
220
- # Remove variation tables from static font
221
- #
222
- # @param tables [Hash<String, String>] Font tables
223
- def remove_variation_tables(tables)
224
- variation_tables = %w[fvar gvar cvar HVAR VVAR MVAR avar STAT]
225
- variation_tables.each { |tag| tables.delete(tag) }
226
- end
227
-
228
82
  # Interpolate a single value
229
83
  #
230
84
  # @param base_value [Numeric] Base value
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Variation
5
+ module Instancing
6
+ # The design space of a variable font: fvar axes plus avar mapping.
7
+ # Validates user coordinates and normalizes them to -1..1.
8
+ class AxisSpace
9
+ Axis = Struct.new(:tag, :minimum, :default, :maximum, :name_id)
10
+
11
+ attr_reader :axes
12
+
13
+ # @param fvar [String] raw fvar table
14
+ # @param avar [String, nil] raw avar table
15
+ def initialize(fvar, avar = nil)
16
+ @axes = parse_fvar(fvar)
17
+ @segments = avar ? parse_avar(avar) : {}
18
+ end
19
+
20
+ def tags
21
+ @axes.map(&:tag)
22
+ end
23
+
24
+ # Named instances: [{ name_id:, coordinates: { tag => value } }]
25
+ def self.named_instances(fvar)
26
+ offset, axis_count, axis_size, inst_count, inst_size = header(fvar)
27
+ base = offset + (axis_count * axis_size)
28
+ Array.new(inst_count) do |i|
29
+ pos = base + (i * inst_size)
30
+ name_id = fvar[pos, 2].unpack1("n")
31
+ coords = Array.new(axis_count) do |a|
32
+ fixed(fvar, pos + 4 + (a * 4))
33
+ end
34
+ { name_id: name_id, values: coords }
35
+ end
36
+ end
37
+
38
+ def self.header(fvar)
39
+ offset, _res, axis_count, axis_size, inst_count, inst_size =
40
+ fvar[4, 12].unpack("nnnnnn")
41
+ [offset, axis_count, axis_size, inst_count, inst_size]
42
+ end
43
+
44
+ def self.fixed(data, pos)
45
+ data[pos, 4].unpack1("l>") / 65536.0
46
+ end
47
+
48
+ # @param requested [Hash{String=>Numeric}] user coordinates
49
+ # @return [Hash{String=>Float}] full user location (defaults filled)
50
+ # @raise [InvalidCoordinatesError, InvalidAxisError]
51
+ def validate(requested)
52
+ known = tags
53
+ requested.each_key do |tag|
54
+ next if known.include?(tag.to_s)
55
+
56
+ raise InvalidAxisError.new(
57
+ axis: tag.to_s,
58
+ reason: "font has axes #{known.join(', ')}",
59
+ )
60
+ end
61
+ @axes.to_h do |axis|
62
+ value = requested.transform_keys(&:to_s)[axis.tag]
63
+ value = axis.default if value.nil?
64
+ value = Float(value)
65
+ unless value.between?(axis.minimum, axis.maximum)
66
+ raise InvalidCoordinatesError.new(
67
+ axis: axis.tag, value: value, range: [axis.minimum, axis.maximum],
68
+ )
69
+ end
70
+ [axis.tag, value]
71
+ end
72
+ end
73
+
74
+ # @return [Array<Float>] normalized (avar-mapped) coordinates, fvar order
75
+ def normalize(location)
76
+ @axes.map do |axis|
77
+ value = location.fetch(axis.tag)
78
+ n = if value < axis.default
79
+ (value - axis.default) / (axis.default - axis.minimum)
80
+ elsif value > axis.default
81
+ (value - axis.default) / (axis.maximum - axis.default)
82
+ else
83
+ 0.0
84
+ end
85
+ map_segment(axis.tag, n.clamp(-1.0, 1.0))
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def parse_fvar(fvar)
92
+ offset, axis_count, axis_size, = self.class.header(fvar)
93
+ Array.new(axis_count) do |i|
94
+ pos = offset + (i * axis_size)
95
+ Axis.new(fvar[pos, 4], self.class.fixed(fvar, pos + 4),
96
+ self.class.fixed(fvar, pos + 8),
97
+ self.class.fixed(fvar, pos + 12),
98
+ fvar[pos + 18, 2].unpack1("n"))
99
+ end
100
+ end
101
+
102
+ # avar v1: per axis, array of [from, to] pairs (F2Dot14)
103
+ def parse_avar(avar)
104
+ axis_count = avar[6, 2].unpack1("n")
105
+ pos = 8
106
+ segments = {}
107
+ axis_count.times do |i|
108
+ count = avar[pos, 2].unpack1("n")
109
+ pos += 2
110
+ pairs = Array.new(count) do |j|
111
+ from, to = avar[pos + (j * 4), 4].unpack("s>s>")
112
+ [from / 16384.0, to / 16384.0]
113
+ end
114
+ pos += count * 4
115
+ segments[@axes[i].tag] = pairs if @axes[i]
116
+ end
117
+ segments
118
+ end
119
+
120
+ def map_segment(tag, value)
121
+ pairs = @segments[tag]
122
+ return value if pairs.nil? || pairs.empty?
123
+
124
+ exact = pairs.find { |from, _| from == value }
125
+ return exact[1] if exact
126
+
127
+ idx = pairs.index { |from, _| from > value }
128
+ return value if idx.nil? || idx.zero?
129
+
130
+ (f0, t0), (f1, t1) = pairs[idx - 1], pairs[idx]
131
+ t0 + ((t1 - t0) * (value - f0) / (f1 - f0))
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Variation
5
+ module Instancing
6
+ # A parsed glyf entry that can be moved by deltas and re-serialized.
7
+ # Points are the simple-glyph points, or for composites one point per
8
+ # component (its x/y offset), matching gvar's point numbering.
9
+ class GlyphOutline
10
+ ON_CURVE = 0x01
11
+ X_SHORT = 0x02
12
+ Y_SHORT = 0x04
13
+ REPEAT = 0x08
14
+ X_SAME = 0x10
15
+ Y_SAME = 0x20
16
+
17
+ ARGS_ARE_WORDS = 0x0001
18
+ ARGS_ARE_XY = 0x0002
19
+ HAVE_SCALE = 0x0008
20
+ MORE_COMPONENTS = 0x0020
21
+ HAVE_XY_SCALE = 0x0040
22
+ HAVE_TWO_BY_TWO = 0x0080
23
+
24
+ Component = Struct.new(:flags, :gid, :arg1, :arg2, :transform)
25
+
26
+ attr_reader :end_pts, :on_curve, :overlap, :instructions, :components,
27
+ :trailer, :header_contours
28
+
29
+ def self.parse(bytes)
30
+ new(bytes)
31
+ end
32
+
33
+ def initialize(bytes)
34
+ @header_contours = 0
35
+ @end_pts = []
36
+ @on_curve = []
37
+ @components = []
38
+ @points = []
39
+ return if bytes.nil? || bytes.empty?
40
+
41
+ @header_contours = bytes[0, 2].unpack1("s>")
42
+ if @header_contours >= 0
43
+ parse_simple(bytes)
44
+ else
45
+ parse_composite(bytes)
46
+ end
47
+ end
48
+
49
+ def empty?
50
+ @header_contours.zero? && @points.empty?
51
+ end
52
+
53
+ def composite?
54
+ @header_contours.negative?
55
+ end
56
+
57
+ # @return [Array<Array<Float>>] [[x, y], ...]
58
+ def points
59
+ @points.map(&:dup)
60
+ end
61
+
62
+ def point_count
63
+ @points.size
64
+ end
65
+
66
+ def bounds_of(points)
67
+ return nil if points.empty?
68
+
69
+ xs = points.map(&:first)
70
+ ys = points.map(&:last)
71
+ [xs.min, ys.min, xs.max, ys.max]
72
+ end
73
+
74
+ # Serialize with the given (already rounded) points. +bbox+ is the
75
+ # [xMin, yMin, xMax, yMax] to record in the header.
76
+ def serialize(new_points, bbox)
77
+ return "".b if empty?
78
+
79
+ composite? ? serialize_composite(new_points, bbox) : serialize_simple(new_points, bbox)
80
+ end
81
+
82
+ private
83
+
84
+ def parse_simple(bytes)
85
+ n = @header_contours
86
+ @end_pts = bytes[10, n * 2].unpack("n*")
87
+ pos = 10 + (n * 2)
88
+ ilen = bytes[pos, 2].unpack1("n")
89
+ @instructions = bytes[pos + 2, ilen]
90
+ pos += 2 + ilen
91
+ count = n.zero? ? 0 : @end_pts.last + 1
92
+ flags = []
93
+ while flags.size < count
94
+ f = bytes.getbyte(pos)
95
+ pos += 1
96
+ flags << f
97
+ if f.anybits?(REPEAT)
98
+ bytes.getbyte(pos).times { flags << f }
99
+ pos += 1
100
+ end
101
+ end
102
+ xs = read_coords(bytes, pos, flags, X_SHORT, X_SAME)
103
+ pos = xs.pop
104
+ ys = read_coords(bytes, pos, flags, Y_SHORT, Y_SAME)
105
+ ys.pop
106
+ @on_curve = flags.map { |f| f.anybits?(ON_CURVE) }
107
+ @overlap = flags.first&.anybits?(0x40) || false
108
+ @points = xs.zip(ys)
109
+ end
110
+
111
+ def read_coords(bytes, pos, flags, short, same)
112
+ val = 0
113
+ out = flags.map do |f|
114
+ if f.anybits?(short)
115
+ d = bytes.getbyte(pos)
116
+ pos += 1
117
+ val += f.anybits?(same) ? d : -d
118
+ elsif !f.anybits?(same)
119
+ val += bytes[pos, 2].unpack1("s>")
120
+ pos += 2
121
+ end
122
+ val
123
+ end
124
+ out << pos
125
+ end
126
+
127
+ def parse_composite(bytes)
128
+ pos = 10
129
+ loop do
130
+ flags, gid = bytes[pos, 4].unpack("nn")
131
+ pos += 4
132
+ if flags.anybits?(ARGS_ARE_WORDS)
133
+ fmt = flags.anybits?(ARGS_ARE_XY) ? "s>s>" : "nn"
134
+ a1, a2 = bytes[pos, 4].unpack(fmt)
135
+ pos += 4
136
+ else
137
+ fmt = flags.anybits?(ARGS_ARE_XY) ? "cc" : "CC"
138
+ a1, a2 = bytes[pos, 2].unpack(fmt)
139
+ pos += 2
140
+ end
141
+ tlen = if flags.anybits?(HAVE_SCALE) then 2
142
+ elsif flags.anybits?(HAVE_XY_SCALE) then 4
143
+ elsif flags.anybits?(HAVE_TWO_BY_TWO) then 8
144
+ else 0
145
+ end
146
+ @components << Component.new(flags, gid, a1, a2, bytes[pos, tlen])
147
+ pos += tlen
148
+ break unless flags.anybits?(MORE_COMPONENTS)
149
+ end
150
+ @trailer = bytes[pos..] || "".b
151
+ @points = @components.map do |c|
152
+ c.flags.anybits?(ARGS_ARE_XY) ? [c.arg1.to_f, c.arg2.to_f] : [0.0, 0.0]
153
+ end
154
+ end
155
+
156
+ def serialize_simple(pts, bbox)
157
+ out = [@header_contours, *bbox].pack("s>5")
158
+ out << @end_pts.pack("n*")
159
+ out << [@instructions.bytesize].pack("n") << @instructions
160
+ flags = +"".b
161
+ xs = +"".b
162
+ ys = +"".b
163
+ px = py = 0
164
+ pts.each_with_index do |(x, y), i|
165
+ f = @on_curve[i] ? ON_CURVE : 0
166
+ f |= 0x40 if i.zero? && @overlap
167
+ dx = x - px
168
+ dy = y - py
169
+ if dx.zero? then f |= X_SAME
170
+ else xs << [dx].pack("s>")
171
+ end
172
+ if dy.zero? then f |= Y_SAME
173
+ else ys << [dy].pack("s>")
174
+ end
175
+ flags << f.chr
176
+ px = x
177
+ py = y
178
+ end
179
+ out << flags << xs << ys
180
+ end
181
+
182
+ def serialize_composite(pts, bbox)
183
+ out = [@header_contours, *bbox].pack("s>5")
184
+ @components.each_with_index do |c, i|
185
+ flags = c.flags
186
+ a1 = c.arg1
187
+ a2 = c.arg2
188
+ if flags.anybits?(ARGS_ARE_XY)
189
+ a1, a2 = pts[i]
190
+ flags |= ARGS_ARE_WORDS
191
+ out << [flags, c.gid, a1, a2].pack("nns>s>")
192
+ elsif flags.anybits?(ARGS_ARE_WORDS)
193
+ out << [flags, c.gid, a1, a2].pack("nnnn")
194
+ else
195
+ out << [flags, c.gid, a1, a2].pack("nnCC")
196
+ end
197
+ out << c.transform
198
+ end
199
+ out << @trailer
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Variation
5
+ module Instancing
6
+ # Reads per-glyph tuple variations from a gvar table.
7
+ class GvarReader
8
+ SHARED_POINTS = 0x8000
9
+ EMBEDDED_PEAK = 0x8000
10
+ INTERMEDIATE = 0x4000
11
+ PRIVATE_POINTS = 0x2000
12
+ INDEX_MASK = 0x0FFF
13
+
14
+ # points == nil means "every point of the glyph, phantoms included"
15
+ Tuple = Struct.new(:peak, :start, :end, :points, :dx, :dy) do
16
+ def intermediate?
17
+ !start.nil?
18
+ end
19
+ end
20
+
21
+ def initialize(data)
22
+ @data = data
23
+ _major, _minor, @axis_count, shared_count, shared_offset,
24
+ @glyph_count, flags, @array_offset = data[0, 20].unpack("nnnnNnnN")
25
+ @long_offsets = flags.anybits?(1)
26
+ @shared = Array.new(shared_count) do |i|
27
+ read_tuple(shared_offset + (i * @axis_count * 2))
28
+ end
29
+ end
30
+
31
+ # @param gid [Integer]
32
+ # @param point_count [Integer] glyph points including 4 phantoms
33
+ # @return [Array<Tuple>]
34
+ def tuples_for(gid, point_count)
35
+ return [] if gid >= @glyph_count
36
+
37
+ first, last = glyph_range(gid)
38
+ return [] if last <= first
39
+
40
+ parse_glyph(@data[@array_offset + first, last - first], point_count)
41
+ end
42
+
43
+ private
44
+
45
+ def read_tuple(pos)
46
+ @data[pos, @axis_count * 2].unpack("s>*").map { |v| v / 16384.0 }
47
+ end
48
+
49
+ def glyph_range(gid)
50
+ if @long_offsets
51
+ @data[20 + (gid * 4), 8].unpack("NN")
52
+ else
53
+ @data[20 + (gid * 2), 4].unpack("nn").map { |v| v * 2 }
54
+ end
55
+ end
56
+
57
+ def parse_glyph(glyph_data, point_count)
58
+ count_field, data_offset = glyph_data[0, 4].unpack("nn")
59
+ count = count_field & INDEX_MASK
60
+ headers = []
61
+ pos = 4
62
+ count.times do
63
+ size, index = glyph_data[pos, 4].unpack("nn")
64
+ pos += 4
65
+ peak = start = finish = nil
66
+ if index.anybits?(EMBEDDED_PEAK)
67
+ peak = glyph_data[pos, @axis_count * 2].unpack("s>*").map { |v| v / 16384.0 }
68
+ pos += @axis_count * 2
69
+ else
70
+ peak = @shared.fetch(index & INDEX_MASK)
71
+ end
72
+ if index.anybits?(INTERMEDIATE)
73
+ start = glyph_data[pos, @axis_count * 2].unpack("s>*").map { |v| v / 16384.0 }
74
+ finish = glyph_data[pos + (@axis_count * 2), @axis_count * 2]
75
+ .unpack("s>*").map { |v| v / 16384.0 }
76
+ pos += @axis_count * 4
77
+ end
78
+ headers << [size, index, peak, start, finish]
79
+ end
80
+
81
+ dpos = data_offset
82
+ shared_points = nil
83
+ if count_field.anybits?(SHARED_POINTS)
84
+ shared_points, dpos = read_points(glyph_data, dpos)
85
+ end
86
+ headers.map do |size, index, peak, start, finish|
87
+ tuple_end = dpos + size
88
+ points = shared_points
89
+ points, dpos = read_points(glyph_data, dpos) if index.anybits?(PRIVATE_POINTS)
90
+ n = points ? points.size : point_count
91
+ dx, dpos = read_deltas(glyph_data, dpos, n)
92
+ dy, = read_deltas(glyph_data, dpos, n)
93
+ dpos = tuple_end
94
+ Tuple.new(peak, start, finish, points, dx, dy)
95
+ end
96
+ end
97
+
98
+ def read_points(data, pos)
99
+ count = data.getbyte(pos)
100
+ pos += 1
101
+ if count.anybits?(0x80)
102
+ count = ((count & 0x7F) << 8) | data.getbyte(pos)
103
+ pos += 1
104
+ end
105
+ return [nil, pos] if count.zero?
106
+
107
+ points = []
108
+ last = 0
109
+ while points.size < count
110
+ ctl = data.getbyte(pos)
111
+ pos += 1
112
+ run = (ctl & 0x7F) + 1
113
+ if ctl.anybits?(0x80)
114
+ vals = data[pos, run * 2].unpack("n*")
115
+ pos += run * 2
116
+ else
117
+ vals = data[pos, run].unpack("C*")
118
+ pos += run
119
+ end
120
+ vals.each { |v| points << (last += v) }
121
+ end
122
+ [points, pos]
123
+ end
124
+
125
+ def read_deltas(data, pos, count)
126
+ out = []
127
+ while out.size < count
128
+ ctl = data.getbyte(pos)
129
+ pos += 1
130
+ run = (ctl & 0x3F) + 1
131
+ if ctl.anybits?(0x80)
132
+ out.concat(Array.new(run, 0))
133
+ elsif ctl.anybits?(0x40)
134
+ out.concat(data[pos, run * 2].unpack("s>*"))
135
+ pos += run * 2
136
+ else
137
+ out.concat(data[pos, run].unpack("c*"))
138
+ pos += run
139
+ end
140
+ end
141
+ [out.first(count), pos]
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end