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.
- checksums.yaml +4 -4
- data/TODO.bug-fixes/07-encapsulation-violations.md +38 -0
- data/TODO.bug-fixes/08-respond-to-violations.md +25 -0
- data/TODO.bug-fixes/09-type1-cff-seac-expansion.md +22 -0
- data/TODO.bug-fixes/10-ttf-type1-composite-glyphs.md +23 -0
- data/TODO.bug-fixes/11-remaining-respond-to.md +25 -0
- data/TODO.bug-fixes/README.md +8 -1
- data/lib/fontisan/binary/base_record.rb +4 -1
- data/lib/fontisan/commands/dump_table_command.rb +1 -1
- data/lib/fontisan/converters/outline_converter.rb +1 -1
- data/lib/fontisan/export/exporter.rb +1 -1
- data/lib/fontisan/export/table_serializer.rb +2 -0
- data/lib/fontisan/glyph_accessor.rb +3 -7
- data/lib/fontisan/hints/truetype_hint_extractor.rb +3 -3
- data/lib/fontisan/outline_extractor.rb +1 -5
- data/lib/fontisan/sfnt_font.rb +2 -4
- data/lib/fontisan/svg/font_face_generator.rb +0 -4
- data/lib/fontisan/svg/font_generator.rb +0 -4
- data/lib/fontisan/tables/cff/index.rb +3 -0
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff2.rb +2 -2
- data/lib/fontisan/tables/glyf.rb +3 -3
- data/lib/fontisan/tables/glyf_table.rb +4 -4
- data/lib/fontisan/type1/charstring_converter.rb +21 -23
- data/lib/fontisan/type1/ttf_to_type1_converter.rb +64 -8
- data/lib/fontisan/validators/validator.rb +1 -10
- data/lib/fontisan/variation/converter.rb +1 -1
- data/lib/fontisan/variation/instance_writer.rb +1 -1
- data/lib/fontisan/variation/validator.rb +10 -7
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2_font.rb +2 -2
- data/lib/fontisan/woff_font.rb +0 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e765f39b4bf4e07536caf5c50ef0b97de1194d29623836a90aef0a8aaba21a6
|
|
4
|
+
data.tar.gz: 4f83b4c02796fb6158f73d387e903f2aacab84d465a2704b525e07fbb506218c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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?)
|
data/TODO.bug-fixes/README.md
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -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.
|
|
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.
|
|
191
|
+
data: @serializer.encode_binary(binary_data),
|
|
192
192
|
fields: { error: error.message }.to_json,
|
|
193
193
|
)
|
|
194
194
|
end
|
|
@@ -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
|
|
352
|
+
next unless glyph&.compound? && glyph.compound?
|
|
357
353
|
|
|
358
354
|
# Add component glyph IDs
|
|
359
|
-
if glyph
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
129
|
+
return nil if glyph.nil? || glyph.empty?
|
|
134
130
|
|
|
135
131
|
if glyph.simple?
|
|
136
132
|
extract_simple_outline(glyph)
|
data/lib/fontisan/sfnt_font.rb
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -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.
|
|
102
|
+
start = index.start_offset
|
|
103
103
|
io = StringIO.new(@source.raw_data)
|
|
104
104
|
io.seek(start)
|
|
105
105
|
|
data/lib/fontisan/tables/cff2.rb
CHANGED
|
@@ -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.
|
|
344
|
-
off_size = index.
|
|
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
|
data/lib/fontisan/tables/glyf.rb
CHANGED
|
@@ -43,7 +43,7 @@ module Fontisan
|
|
|
43
43
|
|
|
44
44
|
# Cache for parsed glyphs
|
|
45
45
|
# @return [Hash<Integer, SimpleGlyph|CompoundGlyph>]
|
|
46
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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? ||
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
83
|
+
charstring.parse
|
|
84
84
|
blend_data = charstring.blend_data
|
|
85
85
|
return nil if blend_data.nil? || blend_data.empty?
|
|
86
86
|
|
|
@@ -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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
data/lib/fontisan/version.rb
CHANGED
data/lib/fontisan/woff2_font.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
data/lib/fontisan/woff_font.rb
CHANGED
|
@@ -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.
|
|
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
|