fontisan 0.4.41 → 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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.bug-fixes/07-encapsulation-violations.md +38 -0
  3. data/TODO.bug-fixes/08-respond-to-violations.md +25 -0
  4. data/TODO.bug-fixes/09-type1-cff-seac-expansion.md +22 -0
  5. data/TODO.bug-fixes/10-ttf-type1-composite-glyphs.md +23 -0
  6. data/TODO.bug-fixes/11-remaining-respond-to.md +25 -0
  7. data/TODO.bug-fixes/README.md +8 -1
  8. data/lib/fontisan/binary/base_record.rb +4 -1
  9. data/lib/fontisan/commands/dump_table_command.rb +1 -1
  10. data/lib/fontisan/converters/outline_converter.rb +1 -1
  11. data/lib/fontisan/export/exporter.rb +1 -1
  12. data/lib/fontisan/export/table_serializer.rb +2 -0
  13. data/lib/fontisan/glyph_accessor.rb +3 -7
  14. data/lib/fontisan/hints/truetype_hint_extractor.rb +3 -3
  15. data/lib/fontisan/outline_extractor.rb +1 -5
  16. data/lib/fontisan/sfnt_font.rb +2 -4
  17. data/lib/fontisan/svg/font_face_generator.rb +0 -4
  18. data/lib/fontisan/svg/font_generator.rb +0 -4
  19. data/lib/fontisan/tables/cff/index.rb +3 -0
  20. data/lib/fontisan/tables/cff/table_builder.rb +1 -1
  21. data/lib/fontisan/tables/cff2.rb +2 -2
  22. data/lib/fontisan/tables/glyf.rb +3 -3
  23. data/lib/fontisan/tables/glyf_table.rb +4 -4
  24. data/lib/fontisan/type1/charstring_converter.rb +21 -23
  25. data/lib/fontisan/type1/ttf_to_type1_converter.rb +64 -8
  26. data/lib/fontisan/validators/validator.rb +1 -10
  27. data/lib/fontisan/variation/converter.rb +1 -1
  28. data/lib/fontisan/variation/instance_writer.rb +1 -1
  29. data/lib/fontisan/variation/validator.rb +10 -7
  30. data/lib/fontisan/version.rb +1 -1
  31. data/lib/fontisan/woff2_font.rb +2 -2
  32. data/lib/fontisan/woff_font.rb +0 -1
  33. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '080dc8ffc4c406353a04ba6fddbd0e597e3fb0b77edefbaba42a9dfcfe2b15ae'
4
- data.tar.gz: 4e7fa9e0d8c93f03438d92c4b94a031a1f5d3d8e4bd367a344d4bb2016fc3664
3
+ metadata.gz: 4e765f39b4bf4e07536caf5c50ef0b97de1194d29623836a90aef0a8aaba21a6
4
+ data.tar.gz: 4f83b4c02796fb6158f73d387e903f2aacab84d465a2704b525e07fbb506218c
5
5
  SHA512:
6
- metadata.gz: d15e8c36f17173e7ad8ebd12439568378e3455b4d5a332bd2ce8054655d5f2f2e0b6b171966fd3beff01a29931f2bff237bab458dcc992702f214f80b82d2b53
7
- data.tar.gz: 330d2c58519661c079b41b371b15ce1d71f7f3c3f43982ee43efbcfb3f849673971900412a530fff5f1fe8e4a1cbff28db1ce74eee2000846523f1eb8e562930
6
+ metadata.gz: 2b06582ee5b83fe73009fcafe496864619075fe4f240bb5b8a336ec67e6caa22d26de432147f690661751af40db4dbb2750fe2392dbf0c32752ae3b231983ea4
7
+ data.tar.gz: cb4678080eb0a69c0e4c5ab698b0cdb012dcbc8e7929e57005f2dcc5df21b53708d549d3e099095cac51f44e11586965d8de2c99083e4e3dfba777b28ffac0d7
@@ -0,0 +1,38 @@
1
+ # 07 — Encapsulation violations (instance_variable_get/set, send to private)
2
+
3
+ ## Priority
4
+ P0
5
+
6
+ ## Problem
7
+ The codebase has 14+ `instance_variable_get` calls, 4 `instance_variable_set`
8
+ calls, and 3 `send` calls to methods that should be accessed through public
9
+ APIs. These break encapsulation and make the code fragile to refactoring.
10
+
11
+ ## Specific violations
12
+
13
+ ### instance_variable_get (should use public accessors)
14
+ - `cff2.rb:343-344` — reads `@data_size`, `@off_size` from Cff2::Index
15
+ - `cff/table_builder.rb:102` — reads `@start_offset` from Cff::Index
16
+ - `dump_table_command.rb:35` — reads `@table_data` from font (public reader exists!)
17
+ - `truetype_hint_extractor.rb:188,205,222` — reads `@table_data` from font
18
+ - `validator.rb:439` — reads `@filename` from font
19
+ - `converter.rb:83` — reads `@parsed` from charstring
20
+
21
+ ### instance_variable_set (should use constructors or public setters)
22
+ - `instance_writer.rb:233` — sets `@table_data` on temp font
23
+ - `outline_converter.rb:425` — sets `@table_data` on temp font
24
+ - `base_record.rb:41` — sets `@raw_data` on BaseRecord
25
+ - `glyf.rb:56` — sets `@glyphs_cache` on BinData record
26
+
27
+ ### send to potentially private methods
28
+ - `exporter.rb:191` — `send(:encode_binary, ...)`
29
+
30
+ ## Approach
31
+ 1. Add missing public readers where they don't exist (Index#data_size, etc.)
32
+ 2. Replace all instance_variable_get/set with public method calls
33
+ 3. Replace send with direct method calls or public dispatch
34
+
35
+ ## Acceptance criteria
36
+ - 0 `instance_variable_get` in lib/fontisan/
37
+ - 0 `instance_variable_set` in lib/fontisan/
38
+ - 0 `send` to private methods in lib/fontisan/
@@ -0,0 +1,25 @@
1
+ # 08 — respond_to? duck typing violations
2
+
3
+ ## Priority
4
+ P1
5
+
6
+ ## Problem
7
+ 10 `respond_to?` calls use duck typing where proper type checks or
8
+ architecture redesigns would be more correct and safer.
9
+
10
+ ## Specific violations
11
+ - `woff2_font.rb:188,198` — `respond_to?(:table_data)` on underlying font
12
+ - `outline_extractor.rb:44` — `respond_to?(:table)` on font
13
+ - `outline_extractor.rb:133` — `respond_to?(:empty?)` on glyph
14
+ - `woff_font.rb:362` — `respond_to?(:length)` on table_entries
15
+ - `sfnt_font.rb:358` — `respond_to?(:length)` on tables array
16
+ - `glyph_accessor.rb:58,356,359,387` — multiple respond_to? on glyph/font
17
+
18
+ ## Approach
19
+ - Replace `respond_to?(:table)` with `is_a?(SfntFont)` type checks
20
+ - Replace `respond_to?(:table_data)` with proper type checks
21
+ - For BinData records that always have `.length`, remove the check entirely
22
+ - For glyph compound check, use `is_a?` on the glyf record type
23
+
24
+ ## Acceptance criteria
25
+ - 0 `respond_to?` in lib/fontisan/ (excluding spec/)
@@ -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,11 +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
- - [06 — Variable font instance WOFF2 output](06-instance-woff2-output.md)
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)
21
+
22
+ ### P3 — Code quality violations
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
@@ -38,7 +38,7 @@ module Fontisan
38
38
  instance = super(io)
39
39
  end
40
40
 
41
- instance.instance_variable_set(:@raw_data, data)
41
+ instance.raw_data = data
42
42
  instance
43
43
  end
44
44
 
@@ -49,6 +49,9 @@ module Fontisan
49
49
  @raw_data || to_binary_s
50
50
  end
51
51
 
52
+ # Set the raw binary data read from the source.
53
+ attr_writer :raw_data
54
+
52
55
  # Check if the record is valid
53
56
  #
54
57
  # @return [Boolean] True if valid, false otherwise
@@ -32,7 +32,7 @@ module Fontisan
32
32
  end
33
33
 
34
34
  # Get raw table data
35
- table_data = @font.instance_variable_get(:@table_data)
35
+ table_data = @font.table_data
36
36
  raw_data = table_data[@table_tag]
37
37
 
38
38
  unless raw_data
@@ -422,7 +422,7 @@ module Fontisan
422
422
  else
423
423
  # Create temporary font with instance tables
424
424
  temp_font = font.class.new
425
- temp_font.instance_variable_set(:@table_data, instance_tables)
425
+ temp_font.table_data = instance_tables
426
426
 
427
427
  # Convert outline format
428
428
  case [source_format, target_format]
@@ -188,7 +188,7 @@ module Fontisan
188
188
  tag: tag,
189
189
  checksum: format_checksum(checksum),
190
190
  parsed: false,
191
- data: @serializer.send(:encode_binary, binary_data),
191
+ data: @serializer.encode_binary(binary_data),
192
192
  fields: { error: error.message }.to_json,
193
193
  )
194
194
  end
@@ -226,6 +226,8 @@ module Fontisan
226
226
  }
227
227
  end
228
228
 
229
+ public
230
+
229
231
  # Encode binary data based on format
230
232
  #
231
233
  # @param data [String] Binary data
@@ -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
@@ -185,7 +185,7 @@ module Fontisan
185
185
  def extract_font_program(font)
186
186
  return "" unless font.has_table?("fpgm")
187
187
 
188
- font_program_data = font.instance_variable_get(:@table_data)["fpgm"]
188
+ font_program_data = font.table_data&.[]("fpgm")
189
189
  return "" unless font_program_data
190
190
 
191
191
  # Return as binary string
@@ -202,7 +202,7 @@ module Fontisan
202
202
  def extract_control_value_program(font)
203
203
  return "" unless font.has_table?("prep")
204
204
 
205
- prep_data = font.instance_variable_get(:@table_data)["prep"]
205
+ prep_data = font.table_data&.[]("prep")
206
206
  return "" unless prep_data
207
207
 
208
208
  # Return as binary string
@@ -219,7 +219,7 @@ module Fontisan
219
219
  def extract_control_values(font)
220
220
  return [] unless font.has_table?("cvt ")
221
221
 
222
- cvt_data = font.instance_variable_get(:@table_data)["cvt "]
222
+ cvt_data = font.table_data&.[]("cvt ")
223
223
  return [] unless cvt_data
224
224
 
225
225
  # CVT table is an array of 16-bit signed integers (FWord values)
@@ -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
@@ -624,7 +622,7 @@ module Fontisan
624
622
  # @param tag [String] The tag to normalize
625
623
  # @return [String] UTF-8 encoded tag
626
624
  def normalize_tag(tag)
627
- @tag_encoding_cache[tag] ||= tag.dup.force_encoding("UTF-8")
625
+ (@tag_encoding_cache ||= {})[tag] ||= tag.dup.force_encoding("UTF-8")
628
626
  end
629
627
 
630
628
  # Load a single table's data on demand
@@ -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}"
@@ -48,6 +48,9 @@ module Fontisan
48
48
  # @return [String] Binary string containing all data
49
49
  attr_reader :data
50
50
 
51
+ # @return [Integer] Byte offset where this INDEX starts in the source data
52
+ attr_reader :start_offset
53
+
51
54
  # Initialize an INDEX from binary data
52
55
  #
53
56
  # @param io [IO, StringIO, String] Binary data to parse
@@ -99,7 +99,7 @@ module Fontisan
99
99
  def extract_index(index)
100
100
  return [0].pack("n") if index.nil? || index.count.zero?
101
101
 
102
- start = index.instance_variable_get(:@start_offset)
102
+ start = index.start_offset
103
103
  io = StringIO.new(@source.raw_data)
104
104
  io.seek(start)
105
105
 
@@ -340,8 +340,8 @@ module Fontisan
340
340
 
341
341
  # count (2) + offSize (1) + offsets + data
342
342
  count = index.count
343
- data_size = index.instance_variable_get(:@data_size) || 0
344
- off_size = index.instance_variable_get(:@off_size) || 4
343
+ data_size = index.data&.bytesize || 0
344
+ off_size = index.off_size || 4
345
345
 
346
346
  2 + 1 + ((count + 1) * off_size) + data_size
347
347
  end
@@ -43,7 +43,7 @@ module Fontisan
43
43
 
44
44
  # Cache for parsed glyphs
45
45
  # @return [Hash<Integer, SimpleGlyph|CompoundGlyph>]
46
- attr_reader :glyphs_cache
46
+ attr_accessor :glyphs_cache
47
47
 
48
48
  # Override read to capture raw data
49
49
  #
@@ -53,7 +53,7 @@ module Fontisan
53
53
  instance = new
54
54
 
55
55
  # Initialize cache
56
- instance.instance_variable_set(:@glyphs_cache, {})
56
+ instance.glyphs_cache = {}
57
57
 
58
58
  # Handle nil or empty data gracefully
59
59
  instance.raw_data = if io.nil?
@@ -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
@@ -431,16 +431,7 @@ module Fontisan
431
431
  # @return [ValidationReport] Complete report
432
432
  def build_report(font, all_results, _elapsed)
433
433
  # Extract font path from font object
434
- font_path = if font.respond_to?(:path)
435
- font.path
436
- elsif font.respond_to?(:filename)
437
- font.filename
438
- elsif font.instance_variable_defined?(:@filename)
439
- font.instance_variable_get(:@filename)
440
- else
441
- "unknown"
442
- end
443
-
434
+ font_path = "unknown"
444
435
  report = Models::ValidationReport.new(
445
436
  font_path: font_path,
446
437
  valid: true,
@@ -80,7 +80,7 @@ module Fontisan
80
80
  return nil unless charstring
81
81
 
82
82
  # Parse CharString to extract blend data
83
- charstring.parse unless charstring.instance_variable_get(:@parsed)
83
+ charstring.parse
84
84
  blend_data = charstring.blend_data
85
85
  return nil if blend_data.nil? || blend_data.empty?
86
86
 
@@ -230,7 +230,7 @@ module Fontisan
230
230
  font = font_class.new
231
231
 
232
232
  # Set table data
233
- font.instance_variable_set(:@table_data, tables)
233
+ font.table_data = tables
234
234
 
235
235
  # Define required methods
236
236
  font.define_singleton_method(:table_data) { tables }
@@ -290,13 +290,16 @@ module Fontisan
290
290
  next unless reg_axis
291
291
 
292
292
  # Check coordinates are in valid range [-1, 1]
293
- %i[start_coord peak_coord end_coord].each do |coord_method|
294
- next unless reg_axis.respond_to?(coord_method)
295
-
296
- coord = reg_axis.send(coord_method)
297
- if coord < -1.0 || coord > 1.0
298
- @warnings << "#{table_tag} region #{idx} axis #{axis_idx} #{coord_method} out of range: #{coord}"
299
- end
293
+ coords = {
294
+ start_coord: reg_axis.start_coord,
295
+ peak_coord: reg_axis.peak_coord,
296
+ end_coord: reg_axis.end_coord,
297
+ }
298
+ coords.each do |name, coord|
299
+ next unless coord
300
+ next unless coord < -1.0 || coord > 1.0
301
+
302
+ @warnings << "#{table_tag} region #{idx} axis #{axis_idx} #{name} out of range: #{coord}"
300
303
  end
301
304
  end
302
305
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.41"
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.41
4
+ version: 0.4.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -139,6 +139,11 @@ files:
139
139
  - TODO.bug-fixes/04-ufo-image-compile-roundtrip.md
140
140
  - TODO.bug-fixes/05-stale-cff-comment.md
141
141
  - TODO.bug-fixes/06-instance-woff2-output.md
142
+ - TODO.bug-fixes/07-encapsulation-violations.md
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
142
147
  - TODO.bug-fixes/README.md
143
148
  - TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md
144
149
  - TODO.improvements/02-collection-outline-priority.md