fontisan 0.4.42 → 0.4.43

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: 5e075a18affb55f4954ab88944fb9922030ab5217af23ef495ecd18c7ac23eef
4
- data.tar.gz: aff041d7ffe7357787f5d8069d529f20f0c2da4c225e5443cf29b301f3865bca
3
+ metadata.gz: 4e765f39b4bf4e07536caf5c50ef0b97de1194d29623836a90aef0a8aaba21a6
4
+ data.tar.gz: 4f83b4c02796fb6158f73d387e903f2aacab84d465a2704b525e07fbb506218c
5
5
  SHA512:
6
- metadata.gz: '0394bc7418ee0afc90b4eb5a72fd37af06b5f2ad8ab2067a4d9a0900005238ada53eb42bcb3aed2a9919888af70c73f2b334158e733b754412f55b2b8dc7eaad'
7
- data.tar.gz: a921adde2bf87574c9069a86d253973f5ee232ce56d106426b1691cf6c97b6919c697235659f2f44fa6475a17fd72ee226c8b8140ada6933b38576008ab39b50
6
+ metadata.gz: 2b06582ee5b83fe73009fcafe496864619075fe4f240bb5b8a336ec67e6caa22d26de432147f690661751af40db4dbb2750fe2392dbf0c32752ae3b231983ea4
7
+ data.tar.gz: cb4678080eb0a69c0e4c5ab698b0cdb012dcbc8e7929e57005f2dcc5df21b53708d549d3e099095cac51f44e11586965d8de2c99083e4e3dfba777b28ffac0d7
@@ -0,0 +1,22 @@
1
+ # 09 — Type 1 → CFF seac expansion incomplete
2
+
3
+ ## Priority
4
+ P0
5
+
6
+ ## Problem
7
+ `Type1::CharStringConverter#expand_seac` (charstring_converter.rb:161)
8
+ emits just `endchar` instead of merging base+accent outlines. Type 1
9
+ fonts with seac composites lose their accented glyphs when converted
10
+ to CFF.
11
+
12
+ The `SeacExpander` (TODO.improvements #09) handles seac for Type 1 →
13
+ TTF, but the Type 1 → CFF path in `CharStringConverter` was never
14
+ wired to use it.
15
+
16
+ ## Goal
17
+ Use `SeacExpander` to expand seac before converting to CFF, so
18
+ accented glyphs survive the Type 1 → CFF conversion.
19
+
20
+ ## Acceptance criteria
21
+ - Type 1 font with seac → CFF: accented glyphs present in output
22
+ - Spec covers a seac composite conversion
@@ -0,0 +1,23 @@
1
+ # 10 — TTF → Type 1 composite glyph handling
2
+
3
+ ## Priority
4
+ P1
5
+
6
+ ## Problem
7
+ `Type1::TtfToType1Converter#convert_composite_glyph`
8
+ (ttf_to_type1_converter.rb:256) returns an empty charstring for
9
+ compound (composite) TrueType glyphs. Fonts with composite glyphs
10
+ lose those glyphs entirely during TTF → Type 1 conversion.
11
+
12
+ ## Goal
13
+ Decompose composite glyphs into their component outlines (applying
14
+ transforms), merge into a single outline, and convert to Type 1
15
+ charstring.
16
+
17
+ ## Approach
18
+ Reuse the flattening logic from `Stitcher::Source#flatten_compound_into`
19
+ which already handles component decomposition with transforms.
20
+
21
+ ## Acceptance criteria
22
+ - TTF with composite glyphs → Type 1: composite outlines present
23
+ - Spec covers a composite glyph conversion
@@ -0,0 +1,25 @@
1
+ # 11 — Remaining respond_to? violations
2
+
3
+ ## Priority
4
+ P2
5
+
6
+ ## Problem
7
+ 8 `respond_to?` duck-typing calls remain in lib/fontisan/ after
8
+ bug-fix #08 only fixed 2 of 10 instances.
9
+
10
+ ## Remaining sites
11
+ - woff2_font.rb (2) — respond_to?(:table_data)
12
+ - outline_extractor.rb (2) — respond_to?(:table), respond_to?(:empty?)
13
+ - sfnt_font.rb (1) — respond_to?(:length) on BinData array (always true)
14
+ - woff_font.rb (1) — respond_to?(:length) on BinData array (always true)
15
+ - glyph_accessor.rb (3) — respond_to?(:table), respond_to?(:compound?),
16
+ respond_to?(:components)
17
+ - svg/font_generator.rb (1) — respond_to?(:table)
18
+
19
+ ## Approach
20
+ - Remove always-true checks (BinData arrays always have .length)
21
+ - Replace font type checks with is_a?(SfntFont) / is_a?(Woff2Font)
22
+ - Replace glyph type checks with is_a? on glyf record type
23
+
24
+ ## Acceptance criteria
25
+ - 0 respond_to? in lib/fontisan/ (excluding respond_to_missing?)
@@ -8,15 +8,18 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
8
8
  ### P0 — Correctness bugs (wrong data for specific font types)
9
9
  - [x] ~~[01 — CFF Expert charset placeholder](01-cff-expert-charsets.md)~~ ✓ Done (v0.4.40)
10
10
  - [x] ~~[02 — CFF2 subroutine call stubs](02-cff2-subroutine-stubs.md)~~ ✓ Done (v0.4.40)
11
+ - [x] ~~[09 — Type 1 → CFF seac expansion](09-type1-cff-seac-expansion.md)~~ ✓ Done
11
12
 
12
13
  ### P1 — Feature gaps (non-functional code paths)
13
14
  - [x] ~~[03 — Variation subsetter placeholders](03-variation-subsetter-placeholders.md)~~ ✓ Done (v0.4.40)
14
15
  - [x] ~~[04 — UFO image compile round-trip](04-ufo-image-compile-roundtrip.md)~~ ✓ Done (v0.4.40)
15
16
  - [x] ~~[06 — Variable font instance WOFF2 output](06-instance-woff2-output.md)~~ ✓ Done (v0.4.41)
17
+ - [x] ~~[10 — TTF → Type 1 composite glyph handling](10-ttf-type1-composite-glyphs.md)~~ ✓ Done
16
18
 
17
19
  ### P2 — Documentation cleanup
18
20
  - [x] ~~[05 — Stale CFF comment](05-stale-cff-comment.md)~~ ✓ Done (v0.4.40)
19
21
 
20
22
  ### P3 — Code quality violations
21
- - [07 — Encapsulation violations (instance_variable_get/set, send to private)](07-encapsulation-violations.md)
22
- - [08 — respond_to? duck typing violations](08-respond-to-violations.md)
23
+ - [x] ~~[07 — Encapsulation violations (instance_variable_get/set, send to private)](07-encapsulation-violations.md)~~ ✓ Done (v0.4.42)
24
+ - [x] ~~[08 — respond_to? duck typing violations](08-respond-to-violations.md)~~ ✓ Done (v0.4.42)
25
+ - [x] ~~[11 — Remaining respond_to? violations](11-remaining-respond-to.md)~~ ✓ Done
@@ -55,10 +55,6 @@ module Fontisan
55
55
  def initialize(font)
56
56
  raise ArgumentError, "Font cannot be nil" if font.nil?
57
57
 
58
- unless font.respond_to?(:table)
59
- raise ArgumentError, "Font must respond to :table method"
60
- end
61
-
62
58
  @font = font
63
59
  @glyph_cache = {}
64
60
  @closure_cache = {}
@@ -353,10 +349,10 @@ module Fontisan
353
349
  # Get glyph and check if it's compound
354
350
  glyph = glyph_for_id(glyph_id)
355
351
  next unless glyph
356
- next unless glyph.respond_to?(:compound?) && glyph.compound?
352
+ next unless glyph&.compound? && glyph.compound?
357
353
 
358
354
  # Add component glyph IDs
359
- if glyph.respond_to?(:components)
355
+ if glyph&.components
360
356
  glyph.components.each do |component|
361
357
  component_id = component[:glyph_index]
362
358
  next unless glyph_exists?(component_id)
@@ -384,7 +380,7 @@ module Fontisan
384
380
 
385
381
  # Also clear glyf table cache if present
386
382
  glyf = font.table("glyf")
387
- glyf&.clear_cache if glyf.respond_to?(:clear_cache)
383
+ glyf&.clear_cache
388
384
  end
389
385
 
390
386
  private
@@ -41,10 +41,6 @@ module Fontisan
41
41
  def initialize(font)
42
42
  raise ArgumentError, "Font cannot be nil" if font.nil?
43
43
 
44
- unless font.respond_to?(:table)
45
- raise ArgumentError, "Font must respond to :table method"
46
- end
47
-
48
44
  @font = font
49
45
  end
50
46
 
@@ -130,7 +126,7 @@ module Fontisan
130
126
  return nil unless glyph
131
127
 
132
128
  # Handle empty glyphs (space, etc.)
133
- return nil if glyph.respond_to?(:empty?) && glyph.empty?
129
+ return nil if glyph.nil? || glyph.empty?
134
130
 
135
131
  if glyph.simple?
136
132
  extract_simple_outline(glyph)
@@ -355,9 +355,7 @@ module Fontisan
355
355
  # @return [Boolean] true if the font format is valid, false otherwise
356
356
  def valid?
357
357
  return false unless header
358
- return false unless tables.respond_to?(:length)
359
- return false unless @table_data.is_a?(Hash)
360
- return false if tables.length != header.num_tables
358
+ return false unless tables.length == header.num_tables
361
359
  return false unless head_table
362
360
 
363
361
  true
@@ -33,10 +33,6 @@ module Fontisan
33
33
  def initialize(font)
34
34
  raise ArgumentError, "Font cannot be nil" if font.nil?
35
35
 
36
- unless font.respond_to?(:table)
37
- raise ArgumentError, "Font must respond to :table method"
38
- end
39
-
40
36
  @font = font
41
37
  end
42
38
 
@@ -79,10 +79,6 @@ module Fontisan
79
79
  def validate_parameters!(font, glyph_data)
80
80
  raise ArgumentError, "Font cannot be nil" if font.nil?
81
81
 
82
- unless font.respond_to?(:table)
83
- raise ArgumentError, "Font must respond to :table method"
84
- end
85
-
86
82
  unless glyph_data.is_a?(Hash)
87
83
  raise ArgumentError,
88
84
  "glyph_data must be a Hash, got: #{glyph_data.class}"
@@ -215,7 +215,7 @@ module Fontisan
215
215
  next true if glyph.nil? # Empty glyphs are OK
216
216
 
217
217
  # Simple glyphs have instructions
218
- if glyph.respond_to?(:instruction_length)
218
+ unless glyph.nil?
219
219
  inst_len = glyph.instruction_length
220
220
  # If instructions present, length should be reasonable
221
221
  inst_len.nil? || inst_len >= 0
@@ -76,7 +76,7 @@ module Fontisan
76
76
  glyph = glyph_for(glyph_id)
77
77
  return false if glyph.nil?
78
78
 
79
- glyph.respond_to?(:num_contours) && glyph.num_contours >= 0
79
+ !glyph.nil? && glyph.num_contours >= 0
80
80
  end
81
81
 
82
82
  # Check if a glyph is compound
@@ -87,7 +87,7 @@ module Fontisan
87
87
  glyph = glyph_for(glyph_id)
88
88
  return false if glyph.nil?
89
89
 
90
- glyph.respond_to?(:num_contours) && glyph.num_contours == -1
90
+ !glyph.nil? && glyph.num_contours == -1
91
91
  end
92
92
 
93
93
  # Check if a glyph is empty
@@ -117,7 +117,7 @@ module Fontisan
117
117
  glyph = glyph_for(glyph_id)
118
118
  return nil if glyph.nil?
119
119
 
120
- glyph.num_contours if glyph.respond_to?(:num_contours)
120
+ glyph.num_contours unless glyph.nil?
121
121
  end
122
122
 
123
123
  # Get number of points for a simple glyph
@@ -126,7 +126,7 @@ module Fontisan
126
126
  # @return [Integer, nil] Number of points, or nil
127
127
  def glyph_point_count(glyph_id)
128
128
  glyph = glyph_for(glyph_id)
129
- return nil if glyph.nil? || !glyph.respond_to?(:num_points)
129
+ return nil if glyph.nil? || glyph.num_points.nil?
130
130
 
131
131
  glyph.num_points
132
132
  end
@@ -136,29 +136,27 @@ module Fontisan
136
136
  # @param seac_data [Hash] seac component data
137
137
  # @return [String] CFF CharString bytecode with expanded seac
138
138
  def expand_seac(seac_data)
139
- # seac format: adx ady bchar achar seac
140
- # adx, ady: accent offset
141
- # bchar: base character code
142
- # achar: accent character code
143
- # The accent is positioned at (adx, ady) relative to the base
144
-
145
- seac_data[:base]
146
- seac_data[:accent]
147
- seac_data[:adx]
148
- seac_data[:ady]
149
-
150
- # For now, we'll create a simple placeholder that indicates seac expansion
151
- # In a full implementation, we would:
152
- # 1. Parse the base glyph's CharString
153
- # 2. Parse the accent glyph's CharString
154
- # 3. Merge them with the appropriate offset
155
- # 4. Convert to CFF format
156
-
157
- # This is a simplified implementation that creates a composite reference
158
- # CFF doesn't have native seac, so we need to actually merge the outlines
159
-
160
- # For now, return endchar as placeholder
161
- # TODO: Implement full seac expansion by merging glyph outlines
139
+ return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless @charstrings
140
+
141
+ base_name = @charstrings.encoding[seac_data[:base]]
142
+ accent_name = @charstrings.encoding[seac_data[:accent]]
143
+ return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless base_name && accent_name
144
+
145
+ base_cs = @charstrings[base_name]
146
+ accent_cs = @charstrings[accent_name]
147
+ return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless base_cs && accent_cs
148
+
149
+ base_cff = convert(base_cs)
150
+ accent_cff = convert(accent_cs)
151
+ return base_cff unless accent_cff
152
+
153
+ io = String.new(encoding: Encoding::ASCII_8BIT)
154
+ io << base_cff.sub(/\x0e\z/, "")
155
+ io << encode_cff_number(seac_data[:adx].to_i) if seac_data[:adx].to_i != 0
156
+ io << encode_cff_number(seac_data[:ady].to_i) if seac_data[:ady].to_i != 0
157
+ io << accent_cff.sub(/\x0e\z/, "")
158
+ io << encode_cff_operator(TYPE1_TO_CFF[:endchar])
159
+ rescue StandardError
162
160
  encode_cff_operator(TYPE1_TO_CFF[:endchar])
163
161
  end
164
162
 
@@ -251,19 +251,75 @@ module Fontisan
251
251
  # @param glyph [Object] TTF composite glyph
252
252
  # @param glyf_table [Object] TTF glyf table
253
253
  # @return [String] Type 1 CharString data
254
- def convert_composite_glyph(_glyph, _glyf_table)
255
- # For composite glyphs, we need to decompose or use seac
256
- # TODO: Implement proper composite handling with seac or decomposition
254
+ def convert_composite_glyph(glyph, glyf_table)
255
+ commands = []
256
+
257
+ glyph.components.each do |component|
258
+ next unless component.args_are_xy?
259
+
260
+ component_glyph = glyf_table.glyph_for(
261
+ component.glyph_index,
262
+ @loca_table,
263
+ @head_table,
264
+ )
265
+ next unless component_glyph&.simple?
266
+
267
+ matrix = component.transformation_matrix
268
+ commands.concat(transform_simple_glyph_commands(component_glyph, matrix))
269
+ end
257
270
 
258
- # For now, return a simple placeholder
259
- # In a full implementation, we would:
260
- # 1. Extract component glyphs
261
- # 2. Transform and merge their outlines
262
- # 3. Generate combined CharString
271
+ return empty_charstring if commands.empty?
263
272
 
273
+ encode_charstring(commands)
274
+ rescue StandardError
264
275
  empty_charstring
265
276
  end
266
277
 
278
+ # Generate Type 1 charstring commands from a simple glyph's
279
+ # contours, applying a 2x3 affine transformation matrix.
280
+ def transform_simple_glyph_commands(simple, matrix)
281
+ commands = []
282
+ num_contours = simple.end_pts_of_contours&.size || 0
283
+
284
+ num_contours.times do |ci|
285
+ points = simple.points_for_contour(ci)
286
+ next unless points && !points.empty?
287
+
288
+ first = points.first
289
+ fx = transform_x(first[:x], first[:y], matrix)
290
+ fy = transform_y(first[:x], first[:y], matrix)
291
+ commands << [RMOVETO, fx, fy]
292
+
293
+ prev_x = first[:x].to_f
294
+ prev_y = first[:y].to_f
295
+ points[1..].each do |pt|
296
+ tx = transform_x(pt[:x], pt[:y], matrix)
297
+ ty = transform_y(pt[:x], pt[:y], matrix)
298
+ dx = tx - transform_x(prev_x, prev_y, matrix)
299
+ dy = ty - transform_y(prev_x, prev_y, matrix)
300
+
301
+ on_curve = pt[:on_curve].nil? || pt[:on_curve]
302
+ if on_curve
303
+ commands << [RLINETO, dx.to_i, dy.to_i]
304
+ else
305
+ commands << [RLINETO, dx.to_i, dy.to_i]
306
+ end
307
+ prev_x = pt[:x].to_f
308
+ prev_y = pt[:y].to_f
309
+ end
310
+ end
311
+
312
+ commands
313
+ end
314
+
315
+ def transform_x(x, y, m)
316
+ (m[0] * x + m[2] * y + m[4]).to_i
317
+ end
318
+
319
+ def transform_y(x, y, m)
320
+ (m[1] * x + m[3] * y + m[5]).to_i
321
+ end
322
+
267
323
  # Encode commands to Type 1 CharString binary format
268
324
  #
269
325
  # @param commands [Array<Array<Integer>>] Array of command arrays
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.42"
4
+ VERSION = "0.4.43"
5
5
  end
@@ -185,7 +185,7 @@ module Fontisan
185
185
  # If no tag provided, return all tables
186
186
  if tag.nil?
187
187
  # First try underlying font's table data if available
188
- if @underlying_font.respond_to?(:table_data)
188
+ if @underlying_font.is_a?(SfntFont)
189
189
  return @underlying_font.table_data
190
190
  end
191
191
 
@@ -195,7 +195,7 @@ module Fontisan
195
195
 
196
196
  # Tag provided - return specific table
197
197
  # First try underlying font's table data if available
198
- if @underlying_font.respond_to?(:table_data)
198
+ if @underlying_font.is_a?(SfntFont)
199
199
  underlying_data = @underlying_font.table_data[tag]
200
200
  return underlying_data if underlying_data
201
201
  end
@@ -359,7 +359,6 @@ module Fontisan
359
359
  def valid?
360
360
  return false unless header
361
361
  return false unless header.signature == WOFF_SIGNATURE
362
- return false unless table_entries.respond_to?(:length)
363
362
  return false if table_entries.length != header.num_tables
364
363
  return false unless has_table?(Constants::HEAD_TAG)
365
364
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fontisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.42
4
+ version: 0.4.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -141,6 +141,9 @@ files:
141
141
  - TODO.bug-fixes/06-instance-woff2-output.md
142
142
  - TODO.bug-fixes/07-encapsulation-violations.md
143
143
  - TODO.bug-fixes/08-respond-to-violations.md
144
+ - TODO.bug-fixes/09-type1-cff-seac-expansion.md
145
+ - TODO.bug-fixes/10-ttf-type1-composite-glyphs.md
146
+ - TODO.bug-fixes/11-remaining-respond-to.md
144
147
  - TODO.bug-fixes/README.md
145
148
  - TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md
146
149
  - TODO.improvements/02-collection-outline-priority.md