fontisan 0.4.40 → 0.4.42

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: 10da865d8186bf46cfb6da26e33acf83b80c33d2b5db8e48c0bdc40a465af88e
4
- data.tar.gz: ff6df0582ee61c69392124598668cc90252bfe42a241cf6eb318133368b44630
3
+ metadata.gz: 5e075a18affb55f4954ab88944fb9922030ab5217af23ef495ecd18c7ac23eef
4
+ data.tar.gz: aff041d7ffe7357787f5d8069d529f20f0c2da4c225e5443cf29b301f3865bca
5
5
  SHA512:
6
- metadata.gz: d0aba9186efca7050ed91ea9d2d2e70ef6a2c01497f651ba613f27a73cbf913fbf9f0e706c9ef655b15c7939ea1cbdc3ce374c9f3cc803b5549f66fa82492150
7
- data.tar.gz: 7c91649aeb22d0d2327a928c54cd3c5aa9c3bc9ca320dfd2bf10ae2e0af876455993872df62ab47678a1768610036a86c1af8423f4eea780298c3d9c4341f535
6
+ metadata.gz: '0394bc7418ee0afc90b4eb5a72fd37af06b5f2ad8ab2067a4d9a0900005238ada53eb42bcb3aed2a9919888af70c73f2b334158e733b754412f55b2b8dc7eaad'
7
+ data.tar.gz: a921adde2bf87574c9069a86d253973f5ee232ce56d106426b1691cf6c97b6919c697235659f2f44fa6475a17fd72ee226c8b8140ada6933b38576008ab39b50
@@ -0,0 +1,24 @@
1
+ # 06 — Variable font instance WOFF2 output
2
+
3
+ ## Priority
4
+ P1
5
+
6
+ ## Problem
7
+ `Variation::InstanceWriter` raises `Fontisan::Error` for WOFF2 output
8
+ instead of writing the file. Variable font instances can be written
9
+ to TTF, OTF, and WOFF but not WOFF2.
10
+
11
+ ## Goal
12
+ Wire the existing WOFF2 encoder into the instance writer so
13
+ `write(format: :woff2)` produces a valid WOFF2 file.
14
+
15
+ ## Approach
16
+ The instance writer already calls `write_sfnt` for TTF/OTF and
17
+ `write_woff` for WOFF. Add a `write_woff2` method that:
18
+ 1. Writes the SFNT tables to a temp TTF
19
+ 2. Loads the temp TTF
20
+ 3. Encodes to WOFF2 via `Converters::FormatConverter`
21
+
22
+ ## Acceptance criteria
23
+ - `instance_writer.write(format: :woff2)` produces a valid WOFF2 file
24
+ - The WOFF2 file decodes back to the correct instance tables
@@ -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/)
@@ -6,12 +6,17 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
6
6
  ## Priorities
7
7
 
8
8
  ### P0 — Correctness bugs (wrong data for specific font types)
9
- - [01 — CFF Expert charset placeholder](01-cff-expert-charsets.md)
10
- - [02 — CFF2 subroutine call stubs](02-cff2-subroutine-stubs.md)
9
+ - [x] ~~[01 — CFF Expert charset placeholder](01-cff-expert-charsets.md)~~ ✓ Done (v0.4.40)
10
+ - [x] ~~[02 — CFF2 subroutine call stubs](02-cff2-subroutine-stubs.md)~~ ✓ Done (v0.4.40)
11
11
 
12
12
  ### P1 — Feature gaps (non-functional code paths)
13
- - [03 — Variation subsetter placeholders](03-variation-subsetter-placeholders.md)
14
- - [04 — UFO image compile round-trip](04-ufo-image-compile-roundtrip.md)
13
+ - [x] ~~[03 — Variation subsetter placeholders](03-variation-subsetter-placeholders.md)~~ ✓ Done (v0.4.40)
14
+ - [x] ~~[04 — UFO image compile round-trip](04-ufo-image-compile-roundtrip.md)~~ ✓ Done (v0.4.40)
15
+ - [x] ~~[06 — Variable font instance WOFF2 output](06-instance-woff2-output.md)~~ ✓ Done (v0.4.41)
15
16
 
16
17
  ### P2 — Documentation cleanup
17
- - [05 — Stale CFF comment](05-stale-cff-comment.md)
18
+ - [x] ~~[05 — Stale CFF comment](05-stale-cff-comment.md)~~ ✓ Done (v0.4.40)
19
+
20
+ ### 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)
@@ -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
@@ -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)
@@ -624,7 +624,7 @@ module Fontisan
624
624
  # @param tag [String] The tag to normalize
625
625
  # @return [String] UTF-8 encoded tag
626
626
  def normalize_tag(tag)
627
- @tag_encoding_cache[tag] ||= tag.dup.force_encoding("UTF-8")
627
+ (@tag_encoding_cache ||= {})[tag] ||= tag.dup.force_encoding("UTF-8")
628
628
  end
629
629
 
630
630
  # Load a single table's data on demand
@@ -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?
@@ -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
 
@@ -105,8 +105,7 @@ module Fontisan
105
105
  when :woff
106
106
  write_woff(output_tables, output_path, source_format)
107
107
  when :woff2
108
- raise Fontisan::Error,
109
- "WOFF2 output not yet implemented (planned for Phase 6)"
108
+ write_woff2(output_tables, output_path, source_format)
110
109
  end
111
110
  end
112
111
 
@@ -231,7 +230,7 @@ module Fontisan
231
230
  font = font_class.new
232
231
 
233
232
  # Set table data
234
- font.instance_variable_set(:@table_data, tables)
233
+ font.table_data = tables
235
234
 
236
235
  # Define required methods
237
236
  font.define_singleton_method(:table_data) { tables }
@@ -324,6 +323,29 @@ module Fontisan
324
323
  "Failed to write WOFF output: #{e.message}"
325
324
  end
326
325
 
326
+ # Write WOFF2 format
327
+ #
328
+ # @param tables [Hash<String, String>] Output tables
329
+ # @param output_path [String] Output file path
330
+ # @param source_format [Symbol] Source format (for flavor detection)
331
+ # @return [Integer] Number of bytes written
332
+ def write_woff2(tables, output_path, source_format)
333
+ require "tmpdir"
334
+ sfnt_version = sfnt_version_for_format(source_format)
335
+
336
+ Dir.mktmpdir("fontisan-woff2-") do |dir|
337
+ temp_ttf = File.join(dir, "instance.ttf")
338
+ FontWriter.write_to_file(tables, temp_ttf, sfnt_version: sfnt_version)
339
+
340
+ font = FontLoader.load(temp_ttf)
341
+ woff2_data = Converters::Woff2Encoder.new.convert(font)
342
+ File.binwrite(output_path, woff2_data)
343
+ end
344
+ rescue StandardError => e
345
+ raise Fontisan::Error,
346
+ "Failed to write WOFF2 output: #{e.message}"
347
+ end
348
+
327
349
  # Get SFNT version for output format
328
350
  #
329
351
  # @param format [Symbol] Output format
@@ -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.40"
4
+ VERSION = "0.4.42"
5
5
  end
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.40
4
+ version: 0.4.42
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -138,6 +138,9 @@ files:
138
138
  - TODO.bug-fixes/03-variation-subsetter-placeholders.md
139
139
  - TODO.bug-fixes/04-ufo-image-compile-roundtrip.md
140
140
  - TODO.bug-fixes/05-stale-cff-comment.md
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
141
144
  - TODO.bug-fixes/README.md
142
145
  - TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md
143
146
  - TODO.improvements/02-collection-outline-priority.md