fontisan 0.4.46 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1866983941321f8342976513ef0fb988454641cd88e677c9f8ea5c5811fe4368
4
- data.tar.gz: 959ca2b7cec09514b960b1f9c649ca9b6f3d9716dfd87c31dfa8155ae54d6ac2
3
+ metadata.gz: 999790a5a5e57d822deee3a86f540b3dac9b8bef06c234c883cfaacb87e9f0b1
4
+ data.tar.gz: c376a3129b02bec87f5754656a66bef27695e0f445ef5740a60dc8a60013b71e
5
5
  SHA512:
6
- metadata.gz: 8515b8d576d88acd19737ccfb14fc5aea468c7ac94b1242beb09757a6a45c11f9846caf8e64df1558ccd472f31a74ceee3fae415471861c8fe0b0520802d86fd
7
- data.tar.gz: b025f9cb74a3a4059367d33453113838fbde355ff434f51879a97d19a134e5753d640b6b2460741d3999a69b79ce16e5bc78db3e5e6e929e85974a77e5cebca2
6
+ metadata.gz: 481c4a053c4aece81d321e5bc6f76abb7633f6f2d987c901bcba4587dbed9a40462dd192a9f9cd71166114b0a5c0fc6bbc628e7cb71b1c043423cf321bca19ec
7
+ data.tar.gz: b367817e918c9ec6de0e346180c9e45f2a2afa73d4c223997357e7b2d38ed445ce04aff98b05da36e1d297d63a71c29728c0e8e9807a79c2567a2c47b9544645
@@ -216,7 +216,7 @@ module Fontisan
216
216
  return tables if source_format == target
217
217
 
218
218
  # Use FormatConverter for outline conversion
219
- if needs_outline_conversion?(source_format, target) || target == :svg
219
+ if needs_outline_conversion?(tables, target) || target == :svg
220
220
  converter = Converters::FormatConverter.new
221
221
  # Create temporary font object from tables
222
222
  font = build_font_from_tables(tables, source_format)
@@ -229,16 +229,18 @@ module Fontisan
229
229
 
230
230
  # Check if outline conversion is needed
231
231
  #
232
- # @param source [Symbol] Source format
232
+ # TTF <-> OTF changes the outline format. WOFF/WOFF2 targets only
233
+ # repackage whichever outlines the font already has.
234
+ #
235
+ # @param tables [Hash] Font tables (their outlines decide the source)
233
236
  # @param target [Symbol] Target format
234
237
  # @return [Boolean] True if outline conversion needed
235
- def needs_outline_conversion?(source, target)
236
- # TTF ↔ OTF requires outline conversion
237
- ttf_formats = %i[ttf ttc woff woff2]
238
- otf_formats = %i[otf otc]
238
+ def needs_outline_conversion?(tables, target)
239
+ target_outline = { ttf: :glyf, ttc: :glyf, otf: :cff, otc: :cff }[target]
240
+ return false unless target_outline
239
241
 
240
- (ttf_formats.include?(source) && otf_formats.include?(target)) ||
241
- (otf_formats.include?(source) && ttf_formats.include?(target))
242
+ source_outline = tables.key?("glyf") ? :glyf : :cff
243
+ source_outline != target_outline
242
244
  end
243
245
 
244
246
  # Write output font file
@@ -6,7 +6,7 @@ module Fontisan
6
6
  module Pipeline
7
7
  autoload :FormatDetector, "fontisan/pipeline/format_detector"
8
8
  autoload :OutputWriter, "fontisan/pipeline/output_writer"
9
- autoload :Strategies, "fontisan/pipeline/strategies/base_strategy"
9
+ autoload :Strategies, "fontisan/pipeline/strategies"
10
10
  autoload :TransformationPipeline,
11
11
  "fontisan/pipeline/transformation_pipeline"
12
12
  autoload :VariationResolver, "fontisan/pipeline/variation_resolver"
@@ -241,13 +241,11 @@ module Fontisan
241
241
  glyph = glyph_for(glyph_id, loca, head)
242
242
  return true if glyph.nil? # Empty glyphs are OK
243
243
 
244
- # Simple glyphs: contours should be >= 0
245
- # Compound glyphs: numberOfContours = -1
244
+ # Simple glyphs: contours must be >= 0. Compound glyphs always have
245
+ # numberOfContours = -1; CompoundGlyph.parse rejects anything else.
246
246
  case glyph
247
- when SimpleGlyph, CompoundGlyph
248
- glyph.num_contours >= -1
249
- else
250
- true
247
+ when SimpleGlyph then glyph.num_contours >= 0
248
+ else true
251
249
  end
252
250
  rescue StandardError
253
251
  false
@@ -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
@@ -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