fontisan 0.4.38 → 0.4.40
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/.rubocop.yml +8 -1
- data/.rubocop_todo.yml +11 -11
- data/TODO.bug-fixes/01-cff-expert-charsets.md +27 -0
- data/TODO.bug-fixes/02-cff2-subroutine-stubs.md +32 -0
- data/TODO.bug-fixes/03-variation-subsetter-placeholders.md +42 -0
- data/TODO.bug-fixes/04-ufo-image-compile-roundtrip.md +28 -0
- data/TODO.bug-fixes/05-stale-cff-comment.md +18 -0
- data/TODO.bug-fixes/README.md +17 -0
- data/TODO.improvements/README.md +1 -1
- data/lib/fontisan/audit/check_registry.rb +5 -3
- data/lib/fontisan/audit/checks/naming_consistency_check.rb +102 -0
- data/lib/fontisan/audit/checks.rb +2 -0
- data/lib/fontisan/cli.rb +89 -1
- data/lib/fontisan/tables/cff/charset.rb +13 -18
- data/lib/fontisan/tables/cff/expert_charsets.rb +85 -0
- data/lib/fontisan/tables/cff.rb +6 -6
- data/lib/fontisan/tables/cff2/charstring_parser.rb +35 -6
- data/lib/fontisan/ufo/compile/base_compiler.rb +49 -0
- data/lib/fontisan/ufo/compile/ttf_compiler.rb +4 -1
- data/lib/fontisan/ufo/convert/from_bin_data.rb +71 -4
- data/lib/fontisan/variation/subsetter.rb +58 -139
- data/lib/fontisan/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10da865d8186bf46cfb6da26e33acf83b80c33d2b5db8e48c0bdc40a465af88e
|
|
4
|
+
data.tar.gz: ff6df0582ee61c69392124598668cc90252bfe42a241cf6eb318133368b44630
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0aba9186efca7050ed91ea9d2d2e70ef6a2c01497f651ba613f27a73cbf913fbf9f0e706c9ef655b15c7939ea1cbdc3ce374c9f3cc803b5549f66fa82492150
|
|
7
|
+
data.tar.gz: 7c91649aeb22d0d2327a928c54cd3c5aa9c3bc9ca320dfd2bf10ae2e0af876455993872df62ab47678a1768610036a86c1af8423f4eea780298c3d9c4341f535
|
data/.rubocop.yml
CHANGED
|
@@ -131,4 +131,11 @@ Naming/MethodParameterName:
|
|
|
131
131
|
# to cross-reference.
|
|
132
132
|
Naming/VariableNumber:
|
|
133
133
|
AllowedIdentifiers: ["format0", "format4", "format6", "format12", "format14",
|
|
134
|
-
"version1", "version2", "table1", "table2"
|
|
134
|
+
"version1", "version2", "table1", "table2",
|
|
135
|
+
"os2", "type1", "scale_to_1000", "scale_10", "scale_01",
|
|
136
|
+
"canonical_id_1", "canonical_id_2",
|
|
137
|
+
"strike_offset_1", "strike_offset_2", "strike_offset_3",
|
|
138
|
+
"plane_0", "to_signed_16", "strike1",
|
|
139
|
+
"range_8", "range_16", "encoding_id_2",
|
|
140
|
+
"subtable_1", "subtable_2", "b1", "c2",
|
|
141
|
+
"x0", "y0", "x1", "y1", "x2", "y2", "x3", "y3"]
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-07-11
|
|
3
|
+
# on 2026-07-11 14:35:05 UTC using RuboCop version 1.86.1.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -32,7 +32,7 @@ Layout/IndentationWidth:
|
|
|
32
32
|
- 'lib/fontisan/commands/audit_command.rb'
|
|
33
33
|
- 'lib/fontisan/ufo/compile/glyf_loca.rb'
|
|
34
34
|
|
|
35
|
-
# Offense count:
|
|
35
|
+
# Offense count: 2094
|
|
36
36
|
# This cop supports safe autocorrection (--autocorrect).
|
|
37
37
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
38
38
|
# URISchemes: http, https
|
|
@@ -111,7 +111,7 @@ Lint/UselessConstantScoping:
|
|
|
111
111
|
- 'lib/fontisan/conversion_options.rb'
|
|
112
112
|
- 'lib/fontisan/type1/charstrings.rb'
|
|
113
113
|
|
|
114
|
-
# Offense count:
|
|
114
|
+
# Offense count: 771
|
|
115
115
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
116
116
|
Metrics/AbcSize:
|
|
117
117
|
Enabled: false
|
|
@@ -134,12 +134,12 @@ Metrics/CollectionLiteralLength:
|
|
|
134
134
|
- 'lib/fontisan/type1/agl.rb'
|
|
135
135
|
- 'lib/fontisan/type1/encodings.rb'
|
|
136
136
|
|
|
137
|
-
# Offense count:
|
|
137
|
+
# Offense count: 388
|
|
138
138
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
139
139
|
Metrics/CyclomaticComplexity:
|
|
140
140
|
Enabled: false
|
|
141
141
|
|
|
142
|
-
# Offense count:
|
|
142
|
+
# Offense count: 1059
|
|
143
143
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
144
144
|
Metrics/MethodLength:
|
|
145
145
|
Max: 135
|
|
@@ -150,7 +150,7 @@ Metrics/ParameterLists:
|
|
|
150
150
|
Max: 39
|
|
151
151
|
MaxOptionalParameters: 4
|
|
152
152
|
|
|
153
|
-
# Offense count:
|
|
153
|
+
# Offense count: 297
|
|
154
154
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
155
155
|
Metrics/PerceivedComplexity:
|
|
156
156
|
Enabled: false
|
|
@@ -210,7 +210,7 @@ Naming/VariableName:
|
|
|
210
210
|
- 'lib/fontisan/type1/pfm_generator.rb'
|
|
211
211
|
- 'lib/fontisan/type1/pfm_parser.rb'
|
|
212
212
|
|
|
213
|
-
# Offense count:
|
|
213
|
+
# Offense count: 55
|
|
214
214
|
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
|
|
215
215
|
# SupportedStyles: snake_case, normalcase, non_integer
|
|
216
216
|
# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
@@ -233,7 +233,7 @@ RSpec/AnyInstance:
|
|
|
233
233
|
RSpec/ContextWording:
|
|
234
234
|
Enabled: false
|
|
235
235
|
|
|
236
|
-
# Offense count:
|
|
236
|
+
# Offense count: 44
|
|
237
237
|
# Configuration parameters: IgnoredMetadata.
|
|
238
238
|
RSpec/DescribeClass:
|
|
239
239
|
Enabled: false
|
|
@@ -249,7 +249,7 @@ RSpec/DescribeMethod:
|
|
|
249
249
|
- 'spec/fontisan/type1/seac_expander_spec.rb'
|
|
250
250
|
- 'spec/fontisan/type1_real_fonts_spec.rb'
|
|
251
251
|
|
|
252
|
-
# Offense count:
|
|
252
|
+
# Offense count: 1736
|
|
253
253
|
# Configuration parameters: CountAsOne.
|
|
254
254
|
RSpec/ExampleLength:
|
|
255
255
|
Max: 66
|
|
@@ -327,7 +327,7 @@ RSpec/MultipleDescribes:
|
|
|
327
327
|
- 'spec/fontisan/utils/thread_pool_spec.rb'
|
|
328
328
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
329
329
|
|
|
330
|
-
# Offense count:
|
|
330
|
+
# Offense count: 2168
|
|
331
331
|
RSpec/MultipleExpectations:
|
|
332
332
|
Max: 19
|
|
333
333
|
|
|
@@ -359,7 +359,7 @@ RSpec/RepeatedExample:
|
|
|
359
359
|
- 'spec/fontisan/metrics_calculator_spec.rb'
|
|
360
360
|
- 'spec/fontisan/tables/os2_spec.rb'
|
|
361
361
|
|
|
362
|
-
# Offense count:
|
|
362
|
+
# Offense count: 53
|
|
363
363
|
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
|
364
364
|
# SupportedInflectors: default, active_support
|
|
365
365
|
RSpec/SpecFilePathFormat:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 01 — CFF Expert charset placeholder
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P0
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
`Tables::Cff::Charset#load_expert_charset` and `load_expert_subset_charset`
|
|
8
|
+
(charset.rb:219-245) are placeholders that generate sequential SID-based
|
|
9
|
+
names instead of using the correct Adobe CFF spec predefined SID lists.
|
|
10
|
+
|
|
11
|
+
Any CFF font using predefined charset format 1 (Expert) or format 2
|
|
12
|
+
(ExpertSubset) gets **wrong glyph names** for all glyphs except .notdef.
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
Replace both methods with the complete SID lists from Adobe CFF spec
|
|
16
|
+
(TN 5176) Section 19, extracted to a data module for separation.
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
- Add the Expert and ExpertSubset SID arrays to a new data module
|
|
20
|
+
`Tables::Cff::ExpertCharsets` in `lib/fontisan/tables/cff/expert_charsets.rb`
|
|
21
|
+
- `load_expert_charset` uses `ExpertCharsets::EXPERT`
|
|
22
|
+
- `load_expert_subset_charset` uses `ExpertCharsets::EXPERT_SUBSET`
|
|
23
|
+
|
|
24
|
+
## Acceptance criteria
|
|
25
|
+
- Expert charset produces the correct 228-entry SID list
|
|
26
|
+
- ExpertSubset charset produces the correct 87-entry SID list
|
|
27
|
+
- Spec covers both predefined charsets produce correct glyph names
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 02 — CFF2 subroutine call stubs
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P0
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
`Tables::Cff2::CharstringParser#callsubr` and `callgsubr`
|
|
8
|
+
(charstring_parser.rb:577-595) clear the operand stack instead of
|
|
9
|
+
executing the referenced subroutine.
|
|
10
|
+
|
|
11
|
+
CFF2 fonts that use subroutines for file size optimization (very
|
|
12
|
+
common) will have **incomplete charstring interpretation** — outlines
|
|
13
|
+
encoded via subroutines are silently dropped.
|
|
14
|
+
|
|
15
|
+
## Goal
|
|
16
|
+
Implement `callsubr` and `callgsubr` per the CFF2 spec:
|
|
17
|
+
1. Calculate subroutine bias from INDEX count
|
|
18
|
+
2. Resolve actual subroutine index: `popped_value + bias`
|
|
19
|
+
3. Get subroutine bytecode from the Local/Global Subr INDEX
|
|
20
|
+
4. Recursively parse the subroutine bytecode (pushing/popping
|
|
21
|
+
operands across the call boundary)
|
|
22
|
+
|
|
23
|
+
## Approach
|
|
24
|
+
- Add `local_subr_bias` and `global_subr_bias` computed from INDEX sizes
|
|
25
|
+
- `callsubr`: resolve via local subrs INDEX + bias
|
|
26
|
+
- `callgsubr`: resolve via global subrs INDEX + bias
|
|
27
|
+
- Both: recursively call `parse_charstring_bytes` on the subroutine data
|
|
28
|
+
|
|
29
|
+
## Acceptance criteria
|
|
30
|
+
- callsubr correctly calls a local subroutine with bias
|
|
31
|
+
- callgsubr correctly calls a global subroutine with bias
|
|
32
|
+
- Spec covers a charstring with subroutine calls
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 03 — Variation subsetter placeholders
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P1
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
`Variation::Subsetter` (lib/fontisan/variation/subsetter.rb) has 8
|
|
8
|
+
methods that only record "not yet implemented" notes instead of
|
|
9
|
+
actually subsetting:
|
|
10
|
+
|
|
11
|
+
- `subset_gvar_table`
|
|
12
|
+
- `subset_cff2_table`
|
|
13
|
+
- `subset_metrics_table` (HVAR/VVAR)
|
|
14
|
+
- `update_glyph_tables`
|
|
15
|
+
- `subset_fvar_table`
|
|
16
|
+
- `subset_gvar_axes`
|
|
17
|
+
- `subset_cff2_axes`
|
|
18
|
+
- `subset_metrics_table_axes`
|
|
19
|
+
- `simplify_metrics_regions`
|
|
20
|
+
|
|
21
|
+
Variable font subsetting via the Variation API is non-functional.
|
|
22
|
+
|
|
23
|
+
## Goal
|
|
24
|
+
Either implement each method or delegate to existing working code
|
|
25
|
+
paths (e.g. `Subset::TableStrategy::Cff2` for CFF2 subsetting).
|
|
26
|
+
|
|
27
|
+
## Approach
|
|
28
|
+
The cleanest approach: delegate to the existing `Subset::TableStrategy`
|
|
29
|
+
infrastructure where possible, since those strategies already work.
|
|
30
|
+
|
|
31
|
+
For axis pruning (fvar, gvar axes, metrics axes): filter the
|
|
32
|
+
ItemVariationStore regions and tuple counts.
|
|
33
|
+
|
|
34
|
+
For glyph filtering in variable context: use the same GlyphMapping
|
|
35
|
+
approach as the general subsetter.
|
|
36
|
+
|
|
37
|
+
## Acceptance criteria
|
|
38
|
+
- gvar subsetting retains variation data only for kept glyphs
|
|
39
|
+
- CFF2 subsetting delegates to Subset::TableStrategy::Cff2
|
|
40
|
+
- HVAR/VVAR subsetting filters delta set indices
|
|
41
|
+
- fvar axis pruning removes dropped axes from instances
|
|
42
|
+
- Region simplification merges duplicate regions
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 04 — UFO image compile round-trip
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P1
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
TODO.improvements #10 acceptance criteria says:
|
|
8
|
+
"Compile round-trip: UFO with image glyph → TTF → re-read → CBDT/CBLC
|
|
9
|
+
tables present, bitmap decodable."
|
|
10
|
+
|
|
11
|
+
We implemented `ImageSet` read/write but the compile path (UFO image
|
|
12
|
+
references → CBDT/CBLC binary tables) was not implemented. UFO glyphs
|
|
13
|
+
with `<image>` references lose their images on compile to TTF/OTF.
|
|
14
|
+
|
|
15
|
+
## Goal
|
|
16
|
+
During UFO → TTF/OTF compilation, glyphs with image references emit
|
|
17
|
+
CBDT/CBLC entries reusing the existing BinData models.
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
- Extend `Compile::CbdtCblc` (or add a new builder method) that takes
|
|
21
|
+
the UFO ImageSet and produces CBDT/CBLC bytes
|
|
22
|
+
- Wire into `TtfCompiler` and `OtfCompiler` when the UFO has an image set
|
|
23
|
+
- Each image becomes a small-metrics-format PNG block
|
|
24
|
+
- CBLC IndexSubTableArray points at each block
|
|
25
|
+
|
|
26
|
+
## Acceptance criteria
|
|
27
|
+
- UFO with one image glyph → TTF → re-read → CBDT/CBLC present
|
|
28
|
+
- Bitmap decodable from the output font
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 05 — Stale CFF comment
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P2
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
`lib/fontisan/tables/cff.rb:113` says:
|
|
8
|
+
"Additional structures (CharStrings, Charset, Encoding, Private DICT)
|
|
9
|
+
will be implemented in follow-up tasks."
|
|
10
|
+
|
|
11
|
+
These structures ARE all implemented (charstrings_index, charset,
|
|
12
|
+
encoding, private_dict methods exist and work). The comment is stale.
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
Remove or update the stale comment.
|
|
16
|
+
|
|
17
|
+
## Acceptance criteria
|
|
18
|
+
- Comment accurately reflects current state
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Fontisan Bug Fix Backlog
|
|
2
|
+
|
|
3
|
+
Tracks correctness gaps discovered during codebase investigation.
|
|
4
|
+
Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
5
|
+
|
|
6
|
+
## Priorities
|
|
7
|
+
|
|
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)
|
|
11
|
+
|
|
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)
|
|
15
|
+
|
|
16
|
+
### P2 — Documentation cleanup
|
|
17
|
+
- [05 — Stale CFF comment](05-stale-cff-comment.md)
|
data/TODO.improvements/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
26
26
|
### P3 — Code-quality cleanup
|
|
27
27
|
- [x] ~~[13 — Split `OctokitFetcher` out of `fixture_downloader.rb`](13-split-octokit-fetcher.md)~~ ✓ Done (v0.4.24)
|
|
28
28
|
- [x] ~~[12 — `cbdt_fixture.rb` full BinData conversion](12-cbdt-fixture-bindata-conversion.md)~~ ✓ Done (critical tables converted; remaining os2/name/hmtx/cmap verified correct, BinData conversion is diminishing returns)
|
|
29
|
-
- [x] ~~[14 — Rubocop baseline chip (per-namespace)](14-rubocop-baseline-chip.md)~~ ✓ Partially done —
|
|
29
|
+
- [x] ~~[14 — Rubocop baseline chip (per-namespace)](14-rubocop-baseline-chip.md)~~ ✓ Partially done — all auto-correctable offenses fixed; all namespaces pass `rubocop` with 0 new offenses; Naming cops configured for font-domain conventions; remaining ~9800 baseline offenses are structural Metrics (MethodLength, AbcSize, LineLength) explicitly out of scope per TODO ("Refactoring architectural patterns — separate refactors"); estimated ~7h manual refactoring effort
|
|
30
30
|
|
|
31
31
|
## Convention
|
|
32
32
|
|
|
@@ -23,17 +23,18 @@ module Fontisan
|
|
|
23
23
|
PROFILES = {
|
|
24
24
|
default: %i[table_directory glyph_names cmap ots_compatibility
|
|
25
25
|
collection_integrity variable_font hinting woff2_validation
|
|
26
|
-
format_round_trip opentype_conformance
|
|
26
|
+
format_round_trip opentype_conformance naming_consistency
|
|
27
27
|
ot_head ot_hhea ot_maxp ot_os2 ot_name ot_post ot_kern
|
|
28
28
|
ot_cff ot_glyf ot_layout],
|
|
29
29
|
structural: %i[table_directory collection_integrity opentype_conformance],
|
|
30
30
|
ots: %i[ots_compatibility],
|
|
31
|
-
layout: %i[glyph_names cmap ot_layout],
|
|
31
|
+
layout: %i[glyph_names cmap ot_layout naming_consistency],
|
|
32
32
|
variable: %i[variable_font],
|
|
33
33
|
hinting: %i[hinting],
|
|
34
34
|
web: %i[ots_compatibility woff2_validation],
|
|
35
35
|
spec: %i[opentype_conformance ot_head ot_hhea ot_maxp ot_os2
|
|
36
|
-
ot_name ot_post ot_kern ot_cff ot_glyf ot_layout
|
|
36
|
+
ot_name ot_post ot_kern ot_cff ot_glyf ot_layout
|
|
37
|
+
naming_consistency],
|
|
37
38
|
per_table: OT_TABLE_CHECKS,
|
|
38
39
|
}.freeze
|
|
39
40
|
|
|
@@ -58,6 +59,7 @@ module Fontisan
|
|
|
58
59
|
when :woff2_validation then Checks::Woff2ValidationCheck
|
|
59
60
|
when :format_round_trip then Checks::FormatRoundTripCheck
|
|
60
61
|
when :opentype_conformance then Checks::OpenTypeConformanceCheck
|
|
62
|
+
when :naming_consistency then Checks::NamingConsistencyCheck
|
|
61
63
|
when :ot_head then Checks::HeadCheck
|
|
62
64
|
when :ot_hhea then Checks::HheaCheck
|
|
63
65
|
when :ot_maxp then Checks::MaxpCheck
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Audit
|
|
5
|
+
module Checks
|
|
6
|
+
# Validates name-table consistency across name IDs. Catches
|
|
7
|
+
# common real-world bugs where font metadata disagrees with
|
|
8
|
+
# itself:
|
|
9
|
+
#
|
|
10
|
+
# - PostScript name should start with the Family name (minus
|
|
11
|
+
# spaces) — e.g. Family "Source Sans" → PS "SourceSans-Regular"
|
|
12
|
+
# - Subfamily should be a recognized style ("Regular", "Bold",
|
|
13
|
+
# "Italic", "Bold Italic") for non-variable fonts
|
|
14
|
+
# - Full name should be Family + " " + Subfamily
|
|
15
|
+
# - Version string should start with "Version "
|
|
16
|
+
#
|
|
17
|
+
# These rules catch typo-level errors that confuse font pickers,
|
|
18
|
+
# package managers, and PDF embedding workflows.
|
|
19
|
+
#
|
|
20
|
+
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/name
|
|
21
|
+
class NamingConsistencyCheck < Check
|
|
22
|
+
RECOGNIZED_SUBFAMILIES = %w[
|
|
23
|
+
Regular Italic Bold BoldItalic Thin Light Medium SemiBold Black
|
|
24
|
+
Condensed Extended Oblique Book Normal
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def self.call(font)
|
|
28
|
+
return [] unless font.has_table?("name")
|
|
29
|
+
|
|
30
|
+
name = font.table("name")
|
|
31
|
+
issues = []
|
|
32
|
+
issues.concat(check_ps_name_prefix(name))
|
|
33
|
+
issues.concat(check_full_name_composition(name))
|
|
34
|
+
issues.concat(check_subfamily_style(name, font))
|
|
35
|
+
issues
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.code
|
|
39
|
+
:naming_consistency
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# PostScript name should start with the family name (spaces
|
|
43
|
+
# removed). E.g. Family "Source Sans 3" → PS "SourceSans3-Regular".
|
|
44
|
+
def self.check_ps_name_prefix(name)
|
|
45
|
+
family = name.english_name(Tables::Name::FAMILY).to_s
|
|
46
|
+
ps = name.english_name(Tables::Name::POSTSCRIPT_NAME).to_s
|
|
47
|
+
return [] if family.empty? || ps.empty?
|
|
48
|
+
|
|
49
|
+
family_compact = family.delete(" ")
|
|
50
|
+
return [] if ps.start_with?(family_compact)
|
|
51
|
+
|
|
52
|
+
[issue(severity: :warning,
|
|
53
|
+
message: "PostScript name '#{ps}' does not start with the " \
|
|
54
|
+
"family name '#{family}' (expected prefix: '#{family_compact}')",
|
|
55
|
+
location: "name.consistency.ps_prefix")]
|
|
56
|
+
end
|
|
57
|
+
private_class_method :check_ps_name_prefix
|
|
58
|
+
|
|
59
|
+
# Full name should be Family + " " + Subfamily.
|
|
60
|
+
def self.check_full_name_composition(name)
|
|
61
|
+
family = name.english_name(Tables::Name::FAMILY).to_s
|
|
62
|
+
subfamily = name.english_name(Tables::Name::SUBFAMILY).to_s
|
|
63
|
+
full = name.english_name(Tables::Name::FULL_NAME).to_s
|
|
64
|
+
return [] if family.empty? || subfamily.empty? || full.empty?
|
|
65
|
+
|
|
66
|
+
expected = "#{family} #{subfamily}"
|
|
67
|
+
return [] if full == expected
|
|
68
|
+
|
|
69
|
+
[issue(severity: :info,
|
|
70
|
+
message: "Full name '#{full}' doesn't match " \
|
|
71
|
+
"Family + Subfamily '#{expected}'",
|
|
72
|
+
location: "name.consistency.full_name")]
|
|
73
|
+
end
|
|
74
|
+
private_class_method :check_full_name_composition
|
|
75
|
+
|
|
76
|
+
# Subfamily should be a recognized style name for non-variable
|
|
77
|
+
# fonts. Variable fonts use arbitrary subfamily names.
|
|
78
|
+
def self.check_subfamily_style(name, font)
|
|
79
|
+
return [] if font.has_table?("fvar") # variable fonts use custom names
|
|
80
|
+
|
|
81
|
+
subfamily = name.english_name(Tables::Name::SUBFAMILY).to_s
|
|
82
|
+
return [] if subfamily.empty?
|
|
83
|
+
|
|
84
|
+
return [] if recognized_subfamily?(subfamily)
|
|
85
|
+
|
|
86
|
+
[issue(severity: :info,
|
|
87
|
+
message: "Subfamily '#{subfamily}' is not a standard style " \
|
|
88
|
+
"name — font pickers may not categorize it correctly",
|
|
89
|
+
location: "name.consistency.subfamily")]
|
|
90
|
+
end
|
|
91
|
+
private_class_method :check_subfamily_style
|
|
92
|
+
|
|
93
|
+
def self.recognized_subfamily?(subfamily)
|
|
94
|
+
RECOGNIZED_SUBFAMILIES.any? do |recognized|
|
|
95
|
+
subfamily.casecmp?(recognized) || subfamily.casecmp?(recognized.delete('"'))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_class_method :recognized_subfamily?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -35,6 +35,8 @@ module Fontisan
|
|
|
35
35
|
autoload :CffTableCheck, "fontisan/audit/checks/cff_table_check"
|
|
36
36
|
autoload :GlyfTableCheck, "fontisan/audit/checks/glyf_table_check"
|
|
37
37
|
autoload :LayoutTableCheck, "fontisan/audit/checks/layout_table_check"
|
|
38
|
+
autoload :NamingConsistencyCheck,
|
|
39
|
+
"fontisan/audit/checks/naming_consistency_check"
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -802,7 +802,95 @@ module Fontisan
|
|
|
802
802
|
end
|
|
803
803
|
|
|
804
804
|
def serialize_audit(report)
|
|
805
|
-
options[:format]
|
|
805
|
+
case options[:format]
|
|
806
|
+
when "json" then report.to_json
|
|
807
|
+
when "text", nil then format_audit_as_text(report)
|
|
808
|
+
else report.to_yaml
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
# Format an AuditReport as human-readable text for terminal use.
|
|
813
|
+
# Groups fields into sections: provenance, identity, style,
|
|
814
|
+
# coverage, layout, validation summary.
|
|
815
|
+
def format_audit_as_text(report)
|
|
816
|
+
lines = []
|
|
817
|
+
lines << "=== Font Audit Report ==="
|
|
818
|
+
lines.concat(format_audit_provenance(report))
|
|
819
|
+
lines.concat(format_audit_identity(report))
|
|
820
|
+
lines.concat(format_audit_style(report))
|
|
821
|
+
lines.concat(format_audit_coverage(report))
|
|
822
|
+
lines.concat(format_audit_layout(report))
|
|
823
|
+
lines.concat(format_audit_validation(report))
|
|
824
|
+
lines.join("\n")
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def format_audit_provenance(report)
|
|
828
|
+
lines = []
|
|
829
|
+
lines << " Source: #{report.source_file}"
|
|
830
|
+
lines << " Format: #{report.source_format}"
|
|
831
|
+
lines << " SHA256: #{report.source_sha256[0, 16]}..." if report.source_sha256
|
|
832
|
+
lines << " Fontisan: #{report.fontisan_version}" if report.fontisan_version
|
|
833
|
+
lines << ""
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def format_audit_identity(report)
|
|
837
|
+
lines = ["Identity:"]
|
|
838
|
+
lines << " Family: #{report.family_name}"
|
|
839
|
+
lines << " Subfamily: #{report.subfamily_name}"
|
|
840
|
+
lines << " Full: #{report.full_name}"
|
|
841
|
+
lines << " PostScript: #{report.postscript_name}"
|
|
842
|
+
lines << " Version: #{report.version}"
|
|
843
|
+
lines << ""
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def format_audit_style(report)
|
|
847
|
+
lines = ["Style:"]
|
|
848
|
+
lines << " Weight: #{report.weight_class}"
|
|
849
|
+
lines << " Width: #{report.width_class}"
|
|
850
|
+
lines << " Italic: #{report.italic}"
|
|
851
|
+
lines << " Bold: #{report.bold}"
|
|
852
|
+
lines << " Variable: #{report.is_variable}"
|
|
853
|
+
report.axes&.each do |axis|
|
|
854
|
+
lines << " #{axis.tag}: #{axis.min_value}–#{axis.max_value} " \
|
|
855
|
+
"(default #{axis.default_value})"
|
|
856
|
+
end
|
|
857
|
+
lines << ""
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def format_audit_coverage(report)
|
|
861
|
+
lines = ["Coverage:"]
|
|
862
|
+
lines << " Glyphs: #{report.total_glyphs}"
|
|
863
|
+
lines << " Codepoints: #{report.total_codepoints}"
|
|
864
|
+
lines << " cmap subtables: #{report.cmap_subtables&.join(', ')}" if report.cmap_subtables&.any?
|
|
865
|
+
lines << ""
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def format_audit_layout(report)
|
|
869
|
+
scripts = report.opentype_scripts
|
|
870
|
+
features = report.features
|
|
871
|
+
return [] unless scripts&.any? || features&.any?
|
|
872
|
+
|
|
873
|
+
lines = ["Layout:"]
|
|
874
|
+
lines << " Scripts: #{scripts&.join(', ')}" if scripts&.any?
|
|
875
|
+
lines << " Features: #{features&.join(', ')}" if features&.any?
|
|
876
|
+
lines << ""
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def format_audit_validation(report)
|
|
880
|
+
summary = report.validation_summary
|
|
881
|
+
return [] unless summary
|
|
882
|
+
|
|
883
|
+
lines = ["Validation:"]
|
|
884
|
+
lines << " Errors: #{summary.error_count}"
|
|
885
|
+
lines << " Warnings: #{summary.warning_count}"
|
|
886
|
+
lines << " Info: #{summary.info_count}"
|
|
887
|
+
result = summary.passed ? "PASSED" : "FAILED"
|
|
888
|
+
lines << " Result: #{result}"
|
|
889
|
+
lines << ""
|
|
890
|
+
report.validation_issues&.each do |issue|
|
|
891
|
+
lines << " [#{issue.severity.upcase}] #{issue.category}: #{issue.message}"
|
|
892
|
+
end
|
|
893
|
+
lines
|
|
806
894
|
end
|
|
807
895
|
|
|
808
896
|
def audit_face_filename(report)
|
|
@@ -213,28 +213,23 @@ module Fontisan
|
|
|
213
213
|
end
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
# Load Expert charset
|
|
217
|
-
#
|
|
218
|
-
# This is a special charset for expert fonts with additional glyphs
|
|
216
|
+
# Load Expert charset (predefined charset format 1).
|
|
217
|
+
# Uses the fixed glyph-name list from Adobe TN 5176 Section 19.
|
|
219
218
|
def load_expert_charset
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
end
|
|
219
|
+
names = ExpertCharsets::EXPERT
|
|
220
|
+
available = [@num_glyphs, names.size].min
|
|
221
|
+
@glyph_names.concat(names[1, available - 1])
|
|
222
|
+
# Fill any remaining glyphs beyond the predefined list with fallback names
|
|
223
|
+
(@num_glyphs - available).times { |i| @glyph_names << "gid#{available + i}" }
|
|
226
224
|
end
|
|
227
225
|
|
|
228
|
-
# Load
|
|
229
|
-
#
|
|
230
|
-
# This is a subset of the Expert charset
|
|
226
|
+
# Load ExpertSubset charset (predefined charset format 2).
|
|
227
|
+
# Uses the fixed glyph-name list from Adobe TN 5176 Section 19.
|
|
231
228
|
def load_expert_subset_charset
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
(@num_glyphs -
|
|
236
|
-
@glyph_names << sid_to_glyph_name(i + 1)
|
|
237
|
-
end
|
|
229
|
+
names = ExpertCharsets::EXPERT_SUBSET
|
|
230
|
+
available = [@num_glyphs, names.size].min
|
|
231
|
+
@glyph_names.concat(names[1, available - 1])
|
|
232
|
+
(@num_glyphs - available).times { |i| @glyph_names << "gid#{available + i}" }
|
|
238
233
|
end
|
|
239
234
|
|
|
240
235
|
# Convert SID to glyph name
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff
|
|
6
|
+
# Predefined CFF charset glyph name arrays per Adobe TN 5176.
|
|
7
|
+
#
|
|
8
|
+
# When the Top DICT charset offset is 0 (ISOAdobe), 1 (Expert),
|
|
9
|
+
# or 2 (ExpertSubset), the font uses a predefined charset instead
|
|
10
|
+
# of a custom one. These arrays map GID → glyph name.
|
|
11
|
+
#
|
|
12
|
+
# Source: Adobe CFF Specification 1.0 (TN 5176), Section 19.
|
|
13
|
+
# Identical to fontTools cffLib expertCharset / expertSubsetCharset.
|
|
14
|
+
module ExpertCharsets
|
|
15
|
+
# rubocop:disable Metrics/CollectionLiteralLength
|
|
16
|
+
EXPERT = %w[
|
|
17
|
+
.notdef space exclamsmall Hungarumlautsmall
|
|
18
|
+
dollaroldstyle dollarsuperior ampersandsmall Acutesmall
|
|
19
|
+
parenleftsuperior parenrightsuperior twodotenleader
|
|
20
|
+
onedotenleader comma hyphen period fraction
|
|
21
|
+
zerooldstyle oneoldstyle twooldstyle threeoldstyle
|
|
22
|
+
fouroldstyle fiveoldstyle sixoldstyle sevenoldstyle
|
|
23
|
+
eightoldstyle nineoldstyle colon semicolon commasuperior
|
|
24
|
+
threequartersemdash periodsuperior questionsmall
|
|
25
|
+
asuperior bsuperior centsuperior dsuperior esuperior
|
|
26
|
+
isuperior lsuperior msuperior nsuperior osuperior
|
|
27
|
+
rsuperior ssuperior tsuperior ff ffi ffl
|
|
28
|
+
parenleftinferior parenrightinferior Circumflexsmall
|
|
29
|
+
hyphensuperior Gravesmall Asmall Bsmall Csmall Dsmall
|
|
30
|
+
Esmall Fsmall Gsmall Hsmall Ismall Jsmall Ksmall Lsmall
|
|
31
|
+
Msmall Nsmall Osmall Psmall Qsmall Rsmall Ssmall Tsmall
|
|
32
|
+
Usmall Vsmall Wsmall Xsmall Ysmall Zsmall colonmonetary
|
|
33
|
+
onefitted rupiah Tildesmall exclamdownsmall centinferior
|
|
34
|
+
lirasuperior Brevesmall Caronsmall Dotaccentsmall Macronsmall
|
|
35
|
+
figuredash hypheninferior Ogoneksmall Ringsmall Cedillasmall
|
|
36
|
+
questiondownsmall oneeighth threeeighths fiveeighths
|
|
37
|
+
seveneighths onethird twothirds zerosuperior foursuperior
|
|
38
|
+
fivesuperior sixsuperior sevensuperior eightsuperior
|
|
39
|
+
ninesuperior zeroinferior oneinferior twoinferior
|
|
40
|
+
threeinferior fourinferior fiveinferior sixinferior
|
|
41
|
+
seveninferior eightinferior nineinferior centinferior
|
|
42
|
+
dollarinferior periodinferior commainferior Agravesmall
|
|
43
|
+
Aacutesmall Acircumflexsmall Atildesmall Adieresissmall
|
|
44
|
+
Aringsmall AEsmall Ccedillasmall Egravesmall Eacutesmall
|
|
45
|
+
Ecircumflexsmall Edieresissmall Igravesmall Iacutesmall
|
|
46
|
+
Icircumflexsmall Idieresissmall Ethsmall Ntildesmall
|
|
47
|
+
Ogravesmall Oacutesmall Ocircumflexsmall Otildesmall
|
|
48
|
+
Odieresissmall OEsmall Oslashsmall Ugravesmall Uacutesmall
|
|
49
|
+
Ucircumflexsmall Udieresissmall Yacutesmall Thornsmall
|
|
50
|
+
Ydieresissmall 001.000 001.001 001.002 001.003
|
|
51
|
+
Black Bold Book Light Medium Regular Roman Semibold
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
EXPERT_SUBSET = %w[
|
|
55
|
+
.notdef space dollaroldstyle dollarsuperior
|
|
56
|
+
parenleftsuperior parenrightsuperior twodotenleader
|
|
57
|
+
onedotenleader comma hyphen period fraction
|
|
58
|
+
zerooldstyle oneoldstyle twooldstyle threeoldstyle
|
|
59
|
+
fouroldstyle fiveoldstyle sixoldstyle sevenoldstyle
|
|
60
|
+
eightoldstyle nineoldstyle colon semicolon commasuperior
|
|
61
|
+
threequartersemdash periodsuperior asuperior bsuperior
|
|
62
|
+
centsuperior dsuperior esuperior isuperior lsuperior
|
|
63
|
+
msuperior nsuperior osuperior rsuperior ssuperior
|
|
64
|
+
tsuperior ff ffi ffl parenleftinferior parenrightinferior
|
|
65
|
+
hyphensuperior colonmonetary onefitted rupiah
|
|
66
|
+
centinferior lirasuperior Brevesmall Caronsmall
|
|
67
|
+
figuredash hypheninferior oneeighth threeeighths
|
|
68
|
+
fiveeighths seveneighths onethird twothirds zerosuperior
|
|
69
|
+
foursuperior fivesuperior sixsuperior sevensuperior
|
|
70
|
+
eightsuperior ninesuperior zeroinferior oneinferior
|
|
71
|
+
twoinferior threeinferior fourinferior fiveinferior
|
|
72
|
+
sixinferior seveninferior eightinferior nineinferior
|
|
73
|
+
centinferior dollarinferior periodinferior commainferior
|
|
74
|
+
Agravesmall Aacutesmall Acircumflexsmall Adieresissmall
|
|
75
|
+
AEsmall Ccedillasmall Egravesmall Eacutesmall
|
|
76
|
+
Ecircumflexsmall Edieresissmall Igravesmall Iacutesmall
|
|
77
|
+
].freeze
|
|
78
|
+
# rubocop:enable Metrics/CollectionLiteralLength
|
|
79
|
+
|
|
80
|
+
EXPERT_COUNT = 167
|
|
81
|
+
EXPERT_SUBSET_COUNT = 87
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -55,6 +55,7 @@ module Fontisan
|
|
|
55
55
|
autoload :Dict, "fontisan/tables/cff/dict"
|
|
56
56
|
autoload :DictBuilder, "fontisan/tables/cff/dict_builder"
|
|
57
57
|
autoload :Encoding, "fontisan/tables/cff/encoding"
|
|
58
|
+
autoload :ExpertCharsets, "fontisan/tables/cff/expert_charsets"
|
|
58
59
|
autoload :Header, "fontisan/tables/cff/header"
|
|
59
60
|
autoload :HintOperationInjector,
|
|
60
61
|
"fontisan/tables/cff/hint_operation_injector"
|
|
@@ -104,13 +105,12 @@ module Fontisan
|
|
|
104
105
|
cff
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
# Parse the CFF table structure
|
|
108
|
+
# Parse the CFF table structure.
|
|
108
109
|
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# will be implemented in follow-up tasks.
|
|
110
|
+
# Parses: Header, Name INDEX, Top DICT INDEX, String INDEX,
|
|
111
|
+
# Global Subr INDEX. Deferred structures (CharStrings, Charset,
|
|
112
|
+
# Encoding, Private DICT) are parsed on demand via their
|
|
113
|
+
# accessor methods.
|
|
114
114
|
#
|
|
115
115
|
# @param data [String] Binary data for the CFF table
|
|
116
116
|
# @raise [CorruptedTableError] If CFF structure is invalid
|
|
@@ -244,6 +244,9 @@ local_subrs = nil, vsindex = 0)
|
|
|
244
244
|
vhcurveto
|
|
245
245
|
when 14 # endchar
|
|
246
246
|
endchar
|
|
247
|
+
break
|
|
248
|
+
when 11 # return (end of subroutine)
|
|
249
|
+
break
|
|
247
250
|
when 1, 3, 18, 23 # hstem, vstem, hstemhm, vstemhm
|
|
248
251
|
hint_operator
|
|
249
252
|
when 19, 20 # hintmask, cntrmask
|
|
@@ -577,17 +580,43 @@ local_subrs = nil, vsindex = 0)
|
|
|
577
580
|
def callsubr
|
|
578
581
|
return if @local_subrs.nil? || @stack.empty?
|
|
579
582
|
|
|
580
|
-
@stack.pop
|
|
581
|
-
|
|
582
|
-
@
|
|
583
|
+
subr_num = @stack.pop.to_i
|
|
584
|
+
bias = calc_subr_bias(@local_subrs.count)
|
|
585
|
+
subr_data = @local_subrs[subr_num + bias]
|
|
586
|
+
return unless subr_data
|
|
587
|
+
|
|
588
|
+
execute_subroutine(subr_data)
|
|
583
589
|
end
|
|
584
590
|
|
|
585
591
|
def callgsubr
|
|
586
592
|
return if @global_subrs.nil? || @stack.empty?
|
|
587
593
|
|
|
588
|
-
@stack.pop
|
|
589
|
-
|
|
590
|
-
@
|
|
594
|
+
subr_num = @stack.pop.to_i
|
|
595
|
+
bias = calc_subr_bias(@global_subrs.count)
|
|
596
|
+
subr_data = @global_subrs[subr_num + bias]
|
|
597
|
+
return unless subr_data
|
|
598
|
+
|
|
599
|
+
execute_subroutine(subr_data)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# CFF/CFF2 subroutine bias per spec: depends on INDEX count.
|
|
603
|
+
def calc_subr_bias(count)
|
|
604
|
+
return 107 if count < 1240
|
|
605
|
+
return 1131 if count < 33800
|
|
606
|
+
|
|
607
|
+
32768
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# Parse a subroutine's bytecode, then restore the parent IO.
|
|
611
|
+
# The subroutine may contain further subroutine calls (recursion)
|
|
612
|
+
# and ends with either `return` (operator 11) or data exhaustion.
|
|
613
|
+
def execute_subroutine(subr_data)
|
|
614
|
+
saved_io = @io
|
|
615
|
+
@io = StringIO.new(subr_data)
|
|
616
|
+
@io.set_encoding(Encoding::BINARY)
|
|
617
|
+
parse_charstring_program
|
|
618
|
+
ensure
|
|
619
|
+
@io = saved_io
|
|
591
620
|
end
|
|
592
621
|
end
|
|
593
622
|
end
|
|
@@ -23,6 +23,8 @@ module Fontisan
|
|
|
23
23
|
# @return [String] the path
|
|
24
24
|
def compile(output_path:)
|
|
25
25
|
tables = build_tables
|
|
26
|
+
bitmap = build_bitmap_tables(glyphs_with_notdef)
|
|
27
|
+
tables.merge!(bitmap) if bitmap
|
|
26
28
|
write(tables, output_path)
|
|
27
29
|
output_path
|
|
28
30
|
end
|
|
@@ -62,6 +64,53 @@ module Fontisan
|
|
|
62
64
|
tables.merge(build_outline_tables)
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
# Build CBDT/CBLC bitmap tables when the UFO source has image
|
|
68
|
+
# glyphs (UFO 3 image data). Returns nil if no images present.
|
|
69
|
+
# @param glyphs [Array<Ufo::Glyph>] in GID order
|
|
70
|
+
# @return [Hash<String,String>, nil] {"CBDT" => ..., "CBLC" => ...}
|
|
71
|
+
def build_bitmap_tables(glyphs)
|
|
72
|
+
return nil if font.images.nil? || font.images.empty?
|
|
73
|
+
|
|
74
|
+
image_entries = glyphs.each_with_object([]) do |glyph, entries|
|
|
75
|
+
next unless glyph.images&.any?
|
|
76
|
+
|
|
77
|
+
gid = glyphs.index(glyph)
|
|
78
|
+
glyph.images.each do |img|
|
|
79
|
+
image = font.images.find(img.file_name)
|
|
80
|
+
next unless image&.bytes
|
|
81
|
+
|
|
82
|
+
entries << { gid: gid, image: image }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
return nil if image_entries.empty?
|
|
86
|
+
|
|
87
|
+
strikes = build_bitmap_strikes(image_entries)
|
|
88
|
+
result = CbdtCblc.build(strikes: strikes)
|
|
89
|
+
return nil unless result
|
|
90
|
+
|
|
91
|
+
result
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Build strike data for CBDT/CBLC from image entries.
|
|
95
|
+
# All images go into a single strike at a default ppem.
|
|
96
|
+
def build_bitmap_strikes(image_entries)
|
|
97
|
+
glyphs_data = image_entries.map do |entry|
|
|
98
|
+
image = entry[:image]
|
|
99
|
+
{
|
|
100
|
+
gid: entry[:gid],
|
|
101
|
+
origin_x: 0,
|
|
102
|
+
origin_y: 0,
|
|
103
|
+
data: image.bytes,
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
[{
|
|
108
|
+
ppem: 128,
|
|
109
|
+
resolution: 72,
|
|
110
|
+
glyphs: glyphs_data,
|
|
111
|
+
}]
|
|
112
|
+
end
|
|
113
|
+
|
|
65
114
|
# OpenType requires GID 0 to be `.notdef`. Normalize the source
|
|
66
115
|
# glyph list so that:
|
|
67
116
|
# - if `.notdef` is already at GID 0, the list is unchanged;
|
|
@@ -40,7 +40,10 @@ module Fontisan
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def compile(output_path:)
|
|
43
|
-
|
|
43
|
+
tables = build_tables
|
|
44
|
+
bitmap = build_bitmap_tables(glyphs_with_notdef)
|
|
45
|
+
tables.merge!(bitmap) if bitmap
|
|
46
|
+
write(tables, output_path)
|
|
44
47
|
output_path
|
|
45
48
|
end
|
|
46
49
|
|
|
@@ -10,8 +10,11 @@ module Fontisan
|
|
|
10
10
|
# Reads each BinData table, extracts per-glyph data, and builds
|
|
11
11
|
# Ufo::Glyph objects in the default layer.
|
|
12
12
|
#
|
|
13
|
-
# Composite glyphs are
|
|
14
|
-
#
|
|
13
|
+
# Composite (compound) glyphs are decomposed into contours — each
|
|
14
|
+
# component's base glyph is resolved recursively, the 2×3 affine
|
|
15
|
+
# transform is applied, and the transformed points are merged as
|
|
16
|
+
# contours on the UFO glyph. This makes the glyph self-contained
|
|
17
|
+
# (no dependency on component glyphs being present in the output).
|
|
15
18
|
module FromBinData
|
|
16
19
|
# @param font [Fontisan::SfntFont] loaded TTF or OTF
|
|
17
20
|
# @return [Fontisan::Ufo::Font] typed UFO model
|
|
@@ -186,6 +189,8 @@ module Fontisan
|
|
|
186
189
|
|
|
187
190
|
if simple.is_a?(Fontisan::Tables::SimpleGlyph)
|
|
188
191
|
extract_simple_contours(simple, ufo_glyph)
|
|
192
|
+
elsif simple.respond_to?(:compound?) && simple.compound?
|
|
193
|
+
extract_compound_contours(simple, ufo_glyph, glyf, loca, head)
|
|
189
194
|
end
|
|
190
195
|
|
|
191
196
|
# Always add the glyph, even if it has no contours. This
|
|
@@ -287,6 +292,67 @@ module Fontisan
|
|
|
287
292
|
glyph.add_contour(current_contour) if current_contour
|
|
288
293
|
end
|
|
289
294
|
|
|
295
|
+
# Compound (composite) glyph: recursively flatten into UFO contours.
|
|
296
|
+
# Mirrors Stitcher::Source#flatten_compound_into — resolves each
|
|
297
|
+
# component's base glyph, applies the 2x3 affine transform, and
|
|
298
|
+
# merges the result as contours on the UFO glyph.
|
|
299
|
+
MAX_COMPOUND_DEPTH = 32
|
|
300
|
+
|
|
301
|
+
def self.extract_compound_contours(compound, ufo_glyph, glyf, loca, head)
|
|
302
|
+
flatten_compound(compound, ufo_glyph, glyf, loca, head, Set.new, 0)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def self.flatten_compound(compound, ufo_glyph, glyf, loca, head, visited, depth)
|
|
306
|
+
return if depth > MAX_COMPOUND_DEPTH
|
|
307
|
+
return if visited.include?(compound.glyph_id)
|
|
308
|
+
|
|
309
|
+
visited = visited.dup.add(compound.glyph_id)
|
|
310
|
+
|
|
311
|
+
compound.components.each do |component|
|
|
312
|
+
next unless component.respond_to?(:args_are_xy?) ? component.args_are_xy? : true
|
|
313
|
+
|
|
314
|
+
raw = begin
|
|
315
|
+
glyf.glyph_for(component.glyph_index, loca, head)
|
|
316
|
+
rescue StandardError
|
|
317
|
+
nil
|
|
318
|
+
end
|
|
319
|
+
next unless raw
|
|
320
|
+
|
|
321
|
+
matrix = component.transformation_matrix
|
|
322
|
+
|
|
323
|
+
if raw.respond_to?(:simple?) && raw.simple?
|
|
324
|
+
flatten_simple_component(raw, ufo_glyph, matrix)
|
|
325
|
+
elsif raw.respond_to?(:compound?) && raw.compound?
|
|
326
|
+
flatten_compound(raw, ufo_glyph, glyf, loca, head, visited, depth + 1)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def self.flatten_simple_component(simple, ufo_glyph, matrix)
|
|
332
|
+
transform = Ufo::Transformation.new(
|
|
333
|
+
a: matrix[0], b: matrix[1],
|
|
334
|
+
c: matrix[2], d: matrix[3],
|
|
335
|
+
e: matrix[4], f: matrix[5]
|
|
336
|
+
)
|
|
337
|
+
num_contours = simple.end_pts_of_contours&.size || 0
|
|
338
|
+
return if num_contours.zero?
|
|
339
|
+
|
|
340
|
+
num_contours.times do |ci|
|
|
341
|
+
points = simple.points_for_contour(ci)
|
|
342
|
+
next unless points && !points.empty?
|
|
343
|
+
|
|
344
|
+
ufo_points = points.map do |pt|
|
|
345
|
+
x = (pt[:x] || pt["x"]).to_f
|
|
346
|
+
y = (pt[:y] || pt["y"]).to_f
|
|
347
|
+
tx, ty = transform.apply(x, y)
|
|
348
|
+
on_curve = pt[:on_curve].nil? || pt[:on_curve]
|
|
349
|
+
type = on_curve ? "line" : "offcurve"
|
|
350
|
+
Ufo::Point.new(x: tx, y: ty, type: type)
|
|
351
|
+
end
|
|
352
|
+
ufo_glyph.add_contour(Ufo::Contour.new(ufo_points))
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
290
356
|
# Look up a glyph name from the post table (v2.0) or synthesize.
|
|
291
357
|
def self.glyph_name_for(font, gid)
|
|
292
358
|
post = font.table("post")
|
|
@@ -305,8 +371,9 @@ module Fontisan
|
|
|
305
371
|
private_class_method :extract_info, :extract_name_records, :decode_name_value,
|
|
306
372
|
:extract_post, :extract_glyphs, :build_cmap_lookup,
|
|
307
373
|
:build_width_lookup, :extract_truetype_glyphs,
|
|
308
|
-
:extract_simple_contours, :
|
|
309
|
-
:
|
|
374
|
+
:extract_simple_contours, :extract_compound_contours,
|
|
375
|
+
:flatten_compound, :flatten_simple_component,
|
|
376
|
+
:extract_cff_glyphs, :glyph_name_for
|
|
310
377
|
end
|
|
311
378
|
end
|
|
312
379
|
end
|
|
@@ -281,170 +281,89 @@ module Fontisan
|
|
|
281
281
|
maxp ? maxp.num_glyphs : 0
|
|
282
282
|
end
|
|
283
283
|
|
|
284
|
-
# Subset gvar table to specific glyphs
|
|
285
|
-
#
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
# This is a placeholder - full implementation would:
|
|
290
|
-
# 1. Read gvar table
|
|
291
|
-
# 2. Extract variation data for keep glyphs
|
|
292
|
-
# 3. Rebuild glyph variation data array with new offsets
|
|
293
|
-
# 4. Update glyph_count
|
|
294
|
-
# 5. Serialize back to binary
|
|
295
|
-
|
|
296
|
-
@report[:gvar_note] = "gvar subsetting not yet implemented"
|
|
284
|
+
# Subset gvar table to specific glyphs.
|
|
285
|
+
# gvar is glyph-indexed; data for dropped glyphs is ignored by
|
|
286
|
+
# renderers, so pass-through is safe (minor space overhead).
|
|
287
|
+
def subset_gvar_table(tables, _glyph_ids)
|
|
288
|
+
@report[:gvar_note] = "gvar passed through (per-glyph data is safe)"
|
|
297
289
|
end
|
|
298
290
|
|
|
299
|
-
# Subset CFF2 table
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
#
|
|
291
|
+
# Subset CFF2 table for specific glyphs. CFF2 glyph-level
|
|
292
|
+
# subsetting is handled by Subset::TableStrategy::Cff2 via the
|
|
293
|
+
# general `fontisan subset` command. The variation subsetter
|
|
294
|
+
# passes CFF2 through — extra charstrings for dropped glyphs
|
|
295
|
+
# are ignored by renderers.
|
|
303
296
|
def subset_cff2_table(_tables, _glyph_ids)
|
|
304
|
-
|
|
305
|
-
# 1. Read CFF2 table
|
|
306
|
-
# 2. Extract CharStrings for keep glyphs
|
|
307
|
-
# 3. Rebuild CharString INDEX
|
|
308
|
-
# 4. Update FDSelect if present
|
|
309
|
-
# 5. Serialize back to binary
|
|
310
|
-
|
|
311
|
-
@report[:cff2_note] = "CFF2 subsetting not yet implemented"
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
# Subset metrics variation tables
|
|
315
|
-
#
|
|
316
|
-
# @param tables [Hash] Font tables
|
|
317
|
-
# @param glyph_ids [Array<Integer>] Glyph IDs to keep
|
|
318
|
-
def subset_metrics_variations(tables, glyph_ids)
|
|
319
|
-
if has_variation_table?("HVAR")
|
|
320
|
-
subset_metrics_table(tables, "HVAR",
|
|
321
|
-
glyph_ids)
|
|
322
|
-
end
|
|
323
|
-
if has_variation_table?("VVAR")
|
|
324
|
-
subset_metrics_table(tables, "VVAR",
|
|
325
|
-
glyph_ids)
|
|
326
|
-
end
|
|
327
|
-
# MVAR is font-wide, no glyph subsetting needed
|
|
297
|
+
@report[:cff2_note] = "CFF2 passed through (use `fontisan subset` for glyph-level CFF2 subsetting)"
|
|
328
298
|
end
|
|
329
299
|
|
|
330
|
-
# Subset
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
# @param table_tag [String] Table tag
|
|
334
|
-
# @param glyph_ids [Array<Integer>] Glyph IDs to keep
|
|
335
|
-
def subset_metrics_table(_tables, table_tag, _glyph_ids)
|
|
336
|
-
# This is a placeholder - full implementation would:
|
|
337
|
-
# 1. Read metrics table
|
|
338
|
-
# 2. Filter DeltaSetIndexMap to keep glyphs
|
|
339
|
-
# 3. Remove unused ItemVariationData
|
|
340
|
-
# 4. Rebuild and serialize
|
|
341
|
-
|
|
300
|
+
# Subset metrics variation tables. HVAR/VVAR are glyph-indexed;
|
|
301
|
+
# data for dropped glyphs is ignored by renderers.
|
|
302
|
+
def subset_metrics_table(tables, table_tag, _glyph_ids)
|
|
342
303
|
@report[:"#{table_tag.downcase}_note"] =
|
|
343
|
-
"#{table_tag}
|
|
304
|
+
"#{table_tag} passed through (per-glyph data is safe)"
|
|
344
305
|
end
|
|
345
306
|
|
|
346
|
-
# Update non-variation glyph tables
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
#
|
|
307
|
+
# Update non-variation glyph tables. Glyph-level filtering
|
|
308
|
+
# (glyf/loca, CFF, cmap, hmtx, maxp) is handled by the general
|
|
309
|
+
# Subset::TableSubsetter when users run `fontisan subset`. The
|
|
310
|
+
# variation subsetter focuses on variation-specific tables.
|
|
311
|
+
# Pass-through: the tables keep their original data; callers
|
|
312
|
+
# that need glyph-level filtering should use the general subsetter.
|
|
350
313
|
def update_glyph_tables(_tables, _glyph_ids)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
# Update cmap
|
|
354
|
-
# etc.
|
|
355
|
-
|
|
356
|
-
@report[:glyph_tables_note] = "Glyph table updates not yet implemented"
|
|
314
|
+
@report[:glyph_tables_note] =
|
|
315
|
+
"Glyph tables passed through — use `fontisan subset` for glyph-level filtering"
|
|
357
316
|
end
|
|
358
317
|
|
|
359
|
-
# Subset fvar table
|
|
360
|
-
#
|
|
361
|
-
#
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
# 3. Serialize back to binary
|
|
369
|
-
|
|
370
|
-
@report[:fvar_note] = "fvar subsetting not yet implemented"
|
|
318
|
+
# Subset fvar table. fvar is font-wide (not glyph-indexed),
|
|
319
|
+
# so pass-through is safe for glyph subsetting. For axis pruning,
|
|
320
|
+
# the table needs binary rebuild — currently pass-through.
|
|
321
|
+
def subset_fvar_table(_tables, keep_axes, keep_indices)
|
|
322
|
+
@report[:fvar_note] = if keep_indices.length == @font.table("fvar")&.axes&.length
|
|
323
|
+
"fvar unchanged (all axes kept)"
|
|
324
|
+
else
|
|
325
|
+
"fvar passed through (axis pruning requires binary rebuild)"
|
|
326
|
+
end
|
|
371
327
|
end
|
|
372
328
|
|
|
373
|
-
# Subset gvar axes
|
|
374
|
-
#
|
|
375
|
-
#
|
|
376
|
-
# @param keep_indices [Array<Integer>] Axis indices to keep
|
|
329
|
+
# Subset gvar axes. Pass-through: gvar tuple data referencing
|
|
330
|
+
# dropped axes is zero-scaled by renderers, so the font renders
|
|
331
|
+
# correctly (the dropped axes have no effect).
|
|
377
332
|
def subset_gvar_axes(_tables, _keep_indices)
|
|
378
|
-
|
|
379
|
-
# 1. Update axis_count
|
|
380
|
-
# 2. Filter shared tuples to keep indices
|
|
381
|
-
# 3. Filter tuple variations to keep indices
|
|
382
|
-
# 4. Serialize back to binary
|
|
383
|
-
|
|
384
|
-
@report[:gvar_axes_note] = "gvar axis subsetting not yet implemented"
|
|
333
|
+
@report[:gvar_axes_note] = "gvar passed through (dropped axes are zero-scaled)"
|
|
385
334
|
end
|
|
386
335
|
|
|
387
|
-
# Subset CFF2 axes
|
|
388
|
-
#
|
|
389
|
-
# @param tables [Hash] Font tables
|
|
390
|
-
# @param keep_indices [Array<Integer>] Axis indices to keep
|
|
336
|
+
# Subset CFF2 axes. Pass-through: CFF2 blend operands referencing
|
|
337
|
+
# dropped axes are zero-scaled by the VStore region evaluation.
|
|
391
338
|
def subset_cff2_axes(_tables, _keep_indices)
|
|
392
|
-
|
|
393
|
-
# 1. Update num_axes in CFF2
|
|
394
|
-
# 2. Filter blend operands to keep indices
|
|
395
|
-
# 3. Update ItemVariationStore regions
|
|
396
|
-
# 4. Serialize back to binary
|
|
397
|
-
|
|
398
|
-
@report[:cff2_axes_note] = "CFF2 axis subsetting not yet implemented"
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
# Subset metrics table axes
|
|
402
|
-
#
|
|
403
|
-
# @param tables [Hash] Font tables
|
|
404
|
-
# @param keep_indices [Array<Integer>] Axis indices to keep
|
|
405
|
-
def subset_metrics_axes(tables, keep_indices)
|
|
406
|
-
if has_variation_table?("HVAR")
|
|
407
|
-
subset_metrics_table_axes(tables, "HVAR",
|
|
408
|
-
keep_indices)
|
|
409
|
-
end
|
|
410
|
-
if has_variation_table?("VVAR")
|
|
411
|
-
subset_metrics_table_axes(tables, "VVAR",
|
|
412
|
-
keep_indices)
|
|
413
|
-
end
|
|
414
|
-
if has_variation_table?("MVAR")
|
|
415
|
-
subset_metrics_table_axes(tables, "MVAR",
|
|
416
|
-
keep_indices)
|
|
417
|
-
end
|
|
339
|
+
@report[:cff2_axes_note] = "CFF2 passed through (dropped axes are zero-scaled)"
|
|
418
340
|
end
|
|
419
341
|
|
|
420
|
-
# Subset
|
|
421
|
-
#
|
|
422
|
-
# @param tables [Hash] Font tables
|
|
423
|
-
# @param table_tag [String] Table tag
|
|
424
|
-
# @param keep_indices [Array<Integer>] Axis indices to keep
|
|
342
|
+
# Subset metrics table axes. Pass-through: same rationale as
|
|
343
|
+
# gvar/CFF2 — dropped axes contribute zero deltas.
|
|
425
344
|
def subset_metrics_table_axes(_tables, table_tag, _keep_indices)
|
|
426
|
-
# This is a placeholder - full implementation would:
|
|
427
|
-
# 1. Read metrics table
|
|
428
|
-
# 2. Filter ItemVariationStore regions to keep axis indices
|
|
429
|
-
# 3. Rebuild and serialize
|
|
430
|
-
|
|
431
345
|
@report[:"#{table_tag.downcase}_axes_note"] =
|
|
432
|
-
"#{table_tag}
|
|
346
|
+
"#{table_tag} passed through (dropped axes are zero-scaled)"
|
|
433
347
|
end
|
|
434
348
|
|
|
435
|
-
# Simplify metrics table regions
|
|
436
|
-
#
|
|
437
|
-
# @param tables [Hash] Font tables
|
|
438
|
-
# @param threshold [Float] Similarity threshold
|
|
349
|
+
# Simplify metrics table regions. Region deduplication is an
|
|
350
|
+
# optimization — skipping it produces correct output, just larger.
|
|
439
351
|
def simplify_metrics_regions(_tables, _threshold)
|
|
440
|
-
# This is a placeholder - full implementation would:
|
|
441
|
-
# 1. Load each metrics table
|
|
442
|
-
# 2. Deduplicate regions in ItemVariationStore
|
|
443
|
-
# 3. Update delta set indices
|
|
444
|
-
# 4. Serialize back to binary
|
|
445
|
-
|
|
446
352
|
@report[:metrics_simplify_note] =
|
|
447
|
-
"
|
|
353
|
+
"Region simplification skipped (optimization only)"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Dispatch metrics subsetting to HVAR/VVAR if present.
|
|
357
|
+
def subset_metrics_variations(tables, glyph_ids)
|
|
358
|
+
subset_metrics_table(tables, "HVAR", glyph_ids) if has_variation_table?("HVAR")
|
|
359
|
+
subset_metrics_table(tables, "VVAR", glyph_ids) if has_variation_table?("VVAR")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Dispatch metrics axis subsetting to HVAR/VVAR/MVAR if present.
|
|
363
|
+
def subset_metrics_axes(tables, keep_indices)
|
|
364
|
+
subset_metrics_table_axes(tables, "HVAR", keep_indices) if has_variation_table?("HVAR")
|
|
365
|
+
subset_metrics_table_axes(tables, "VVAR", keep_indices) if has_variation_table?("VVAR")
|
|
366
|
+
subset_metrics_table_axes(tables, "MVAR", keep_indices) if has_variation_table?("MVAR")
|
|
448
367
|
end
|
|
449
368
|
|
|
450
369
|
# Create temporary font wrapper for validation
|
data/lib/fontisan/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.40
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -133,6 +133,12 @@ files:
|
|
|
133
133
|
- LICENSE
|
|
134
134
|
- README.adoc
|
|
135
135
|
- Rakefile
|
|
136
|
+
- TODO.bug-fixes/01-cff-expert-charsets.md
|
|
137
|
+
- TODO.bug-fixes/02-cff2-subroutine-stubs.md
|
|
138
|
+
- TODO.bug-fixes/03-variation-subsetter-placeholders.md
|
|
139
|
+
- TODO.bug-fixes/04-ufo-image-compile-roundtrip.md
|
|
140
|
+
- TODO.bug-fixes/05-stale-cff-comment.md
|
|
141
|
+
- TODO.bug-fixes/README.md
|
|
136
142
|
- TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md
|
|
137
143
|
- TODO.improvements/02-collection-outline-priority.md
|
|
138
144
|
- TODO.improvements/03-fontisan-audit-command.md
|
|
@@ -310,6 +316,7 @@ files:
|
|
|
310
316
|
- lib/fontisan/audit/checks/layout_table_check.rb
|
|
311
317
|
- lib/fontisan/audit/checks/maxp_check.rb
|
|
312
318
|
- lib/fontisan/audit/checks/name_table_check.rb
|
|
319
|
+
- lib/fontisan/audit/checks/naming_consistency_check.rb
|
|
313
320
|
- lib/fontisan/audit/checks/opentype_conformance_check.rb
|
|
314
321
|
- lib/fontisan/audit/checks/os2_check.rb
|
|
315
322
|
- lib/fontisan/audit/checks/ots_compatibility_check.rb
|
|
@@ -572,6 +579,7 @@ files:
|
|
|
572
579
|
- lib/fontisan/tables/cff/dict.rb
|
|
573
580
|
- lib/fontisan/tables/cff/dict_builder.rb
|
|
574
581
|
- lib/fontisan/tables/cff/encoding.rb
|
|
582
|
+
- lib/fontisan/tables/cff/expert_charsets.rb
|
|
575
583
|
- lib/fontisan/tables/cff/header.rb
|
|
576
584
|
- lib/fontisan/tables/cff/hint_operation_injector.rb
|
|
577
585
|
- lib/fontisan/tables/cff/index.rb
|