fontisan 0.4.26 → 0.4.28
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.improvements/15-cff-cff2-subsetter-strategy.md +78 -0
- data/TODO.improvements/README.md +8 -7
- data/lib/fontisan/config/subset_profiles.yml +4 -0
- data/lib/fontisan/stitcher/cbdt_propagator.rb +76 -37
- data/lib/fontisan/subset/glyph_mapping.rb +7 -2
- data/lib/fontisan/subset/table_strategy/head.rb +1 -0
- data/lib/fontisan/tables/cff.rb +122 -18
- data/lib/fontisan/ufo/convert/from_bin_data.rb +63 -3
- data/lib/fontisan/ufo/font.rb +2 -1
- data/lib/fontisan/ufo/groups.rb +45 -0
- data/lib/fontisan/ufo/reader.rb +9 -0
- data/lib/fontisan/ufo.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdd148825272b5984f48a9d7e83715161054702eaf84e1c270eff42151c6bcba
|
|
4
|
+
data.tar.gz: d701f78f818f4b49fbcc047094eadd872e01c5dcd2372516bfaed1098bfa9bf5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25d8d8fe6c8fed6067a068ac83aec0f737e627fb81775fdd2fd679a38007c72fbfae0554075f539637746d264b31eddd6125fbaeaee0c566a8b0947cb9ea15cf
|
|
7
|
+
data.tar.gz: 5a6b96d94af207136e925b0a8ed675688c387fbdae471fbbb6120e9703869a42142c598579d25164121a6fd57d009e4ccad760d6c2464c45fe1d711f8071cd63
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# 15 — CFF/CFF2 subsetter strategy
|
|
2
|
+
|
|
3
|
+
## Priority
|
|
4
|
+
P0
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
The subsetter's `TableStrategy::REGISTRY` has entries for glyf/loca (TrueType outlines) but NOT for CFF or CFF2 (PostScript outlines). Both fall through to `PassThroughStrategy`, which copies the full source table verbatim.
|
|
9
|
+
|
|
10
|
+
Two concrete failure modes:
|
|
11
|
+
|
|
12
|
+
1. **Profiles drop CFF entirely.** The `web`, `pdf`, and `minimal` profiles list glyf/loca but not CFF/CFF2. A CFF-based OTF font subsetted to any of these profiles loses ALL outlines — the output has `cmap`/`head`/`hmtx`/`maxp` but no CFF. The font has no glyph data.
|
|
13
|
+
|
|
14
|
+
2. **`full` profile includes CFF but doesn't subset it.** The full CFF table is copied verbatim while `maxp.numGlyphs` is updated to the subset count. The CFF CharStrings INDEX has the original glyph count; `maxp` says fewer. Invalid mismatch — fontTools and Chrome reject.
|
|
15
|
+
|
|
16
|
+
This affects every CFF-based OpenType font (the majority of professional fonts — Adobe, Google Fonts OTFs, variable CFF2 fonts).
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
Two new strategy classes:
|
|
21
|
+
- `Subset::TableStrategy::Cff` — subsets CFF v1 tables
|
|
22
|
+
- `Subset::TableStrategy::Cff2` — subsets CFF2 tables (variable-font aware)
|
|
23
|
+
|
|
24
|
+
Both registered in the REGISTRY. The `web` and `pdf` profiles updated to include CFF/CFF2 (conditionally — only when the source font has them; the profile loader already handles this via "if table present").
|
|
25
|
+
|
|
26
|
+
## Approach
|
|
27
|
+
|
|
28
|
+
**Architecturally preferred path (TODO #05 + #10b dependency):**
|
|
29
|
+
|
|
30
|
+
If the CFF → UFO conversion (`Ufo::Convert::FromBinData#extract_cff_glyphs`, currently stubbed as TODO #10b) is fully implemented, AND the OTF compiler emits real CFF charstrings (TODO #05), then CFF subsetting is trivial:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
CFF font → Ufo::Convert::FromBinData → UFO (with contours)
|
|
34
|
+
→ drop glyphs not in mapping
|
|
35
|
+
→ Ufo::Compile::OtfCompiler → new CFF
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
No standalone INDEX rebuilder needed. The UFO model is the canonical representation; CFF is a serialization. This is DRY, MECE, OCP-compliant.
|
|
39
|
+
|
|
40
|
+
**Fallback path (standalone CFF INDEX rebuilder):**
|
|
41
|
+
|
|
42
|
+
If TODO #05 + #10b are not yet done, a standalone strategy can work at the binary level:
|
|
43
|
+
|
|
44
|
+
1. Parse CFF header → Name INDEX → Top DICT INDEX → String INDEX → Global Subr INDEX
|
|
45
|
+
2. Decode Top DICT operators → get charset offset, CharStrings offset, Private offset
|
|
46
|
+
3. Parse CharStrings INDEX → get byte range per charstring
|
|
47
|
+
4. Filter to subset GIDs
|
|
48
|
+
5. Rebuild CharStrings INDEX with retained charstrings
|
|
49
|
+
6. Rebuild charset to match new GID ordering
|
|
50
|
+
7. Optionally prune GlobalSubrs/PrivateSubrs
|
|
51
|
+
8. Recompute all offsets in Top DICT
|
|
52
|
+
9. Reassemble CFF bytes
|
|
53
|
+
|
|
54
|
+
This is more work than the UFO round-trip path and duplicates logic the compile pipeline already has. Prefer the UFO path.
|
|
55
|
+
|
|
56
|
+
## Out of scope
|
|
57
|
+
|
|
58
|
+
- CFF2 blend/vsindex ENCODING (TODO 06) — that's about generating new CFF2 from scratch, not subsetting existing CFF2.
|
|
59
|
+
- OTF compiler real CFF (TODO 05) — that's about building CFF from UFO outlines.
|
|
60
|
+
- Font-level outline conversion (TTF→OTF) — that's `Converters::OutlineConverter`.
|
|
61
|
+
|
|
62
|
+
## Effort
|
|
63
|
+
|
|
64
|
+
~1-2 days for CFF v1 (well-understood algorithm).
|
|
65
|
+
~1 additional day for CFF2 (adds FDSelect + ItemVariationStore handling).
|
|
66
|
+
|
|
67
|
+
## Dependencies
|
|
68
|
+
|
|
69
|
+
None. The CFF/CFF2 BinData models already exist.
|
|
70
|
+
|
|
71
|
+
## Acceptance criteria
|
|
72
|
+
|
|
73
|
+
- A CFF-based OTF font subsetted to the `web` profile produces valid output (fontTools accepts it, Chrome OTS doesn't reject).
|
|
74
|
+
- The subset CFF table has exactly `subset_glyph_count` charstrings (not the source's full count).
|
|
75
|
+
- Subroutines referenced only by dropped glyphs are pruned.
|
|
76
|
+
- CFF2 variable fonts retain their ItemVariationStore and variation deltas after subsetting.
|
|
77
|
+
- New spec covers: CFF font → web subset → fontTools decode → glyph count matches.
|
|
78
|
+
- Profile YAML updated: `web` and `pdf` profiles include CFF/CFF2 conditionally.
|
data/TODO.improvements/README.md
CHANGED
|
@@ -6,25 +6,26 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
6
6
|
## Priorities
|
|
7
7
|
|
|
8
8
|
### P0 — Correctness gaps (open bugs / silent failures)
|
|
9
|
-
- [01 — CBDT/CBLC GID-stable propagation](01-cbdt-cblc-gid-stable-propagation.md)
|
|
10
|
-
- [02 — Collection-mode outline-priority regression](02-collection-outline-priority.md)
|
|
9
|
+
- [x] ~~[01 — CBDT/CBLC GID-stable propagation](01-cbdt-cblc-gid-stable-propagation.md)~~ ✓ Done (PR #115)
|
|
10
|
+
- [x] ~~[02 — Collection-mode outline-priority regression](02-collection-outline-priority.md)~~ ✓ Already fixed
|
|
11
|
+
- [15 — CFF/CFF2 subsetter strategy](15-cff-cff2-subsetter-strategy.md) — Head crash fixed, profiles updated; CFF→UFO extraction done (TODO #10b); blocked on TODO #05 for full round-trip (PR #108 unskipped the test; now passing with TODO 01)
|
|
11
12
|
|
|
12
13
|
### P1 — High-value features
|
|
13
14
|
- [03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)
|
|
14
15
|
- [04 — UFO composite glyph encoding](04-ufo-composite-glyph-encoding.md)
|
|
15
|
-
- [05 — OTF compiler real CFF charstrings](05-otf-compiler-real-cff.md)
|
|
16
|
+
- [05 — OTF compiler real CFF charstrings](05-otf-compiler-real-cff.md) ← blocks TODO #15
|
|
16
17
|
|
|
17
18
|
### P2 — Specialist feature parity
|
|
18
|
-
- [x] ~~[07 — CPAL v1 header fields](07-cpal-v1-header-fields.md)~~ ✓ Done
|
|
19
|
+
- [x] ~~[07 — CPAL v1 header fields](07-cpal-v1-header-fields.md)~~ ✓ Done (v0.4.25)
|
|
20
|
+
- [x] ~~[08 — CFF standard string table](08-cff-standard-string-table.md)~~ ✓ Partial (SID 0-95, ASCII subset)
|
|
19
21
|
- [06 — CFF2 blend/vsindex operators](06-cff2-blend-vsindex-operators.md)
|
|
20
|
-
- [08 — CFF standard string table](08-cff-standard-string-table.md)
|
|
21
22
|
- [09 — Type 1 seac expansion](09-type1-seac-expansion.md)
|
|
22
23
|
- [10 — UFO image set + feature writers](10-ufo-image-set-feature-writers.md)
|
|
23
24
|
- [11 — kern groups.plist support](11-kern-groups-plist.md)
|
|
24
25
|
|
|
25
26
|
### P3 — Code-quality cleanup
|
|
26
|
-
- [x] ~~[13 — Split `OctokitFetcher` out of `fixture_downloader.rb`](13-split-octokit-fetcher.md)~~ ✓ Done (
|
|
27
|
-
- [12 — `cbdt_fixture.rb` full BinData conversion](12-cbdt-fixture-bindata-conversion.md)
|
|
27
|
+
- [x] ~~[13 — Split `OctokitFetcher` out of `fixture_downloader.rb`](13-split-octokit-fetcher.md)~~ ✓ Done (v0.4.24)
|
|
28
|
+
- [x] ~~[12 — `cbdt_fixture.rb` full BinData conversion](12-cbdt-fixture-bindata-conversion.md)~~ ✓ Partial (head/hhea/post converted; os2/name/hmtx/cmap remain)
|
|
28
29
|
- [14 — Rubocop baseline chip (per-namespace)](14-rubocop-baseline-chip.md)
|
|
29
30
|
|
|
30
31
|
## Convention
|
|
@@ -80,6 +80,8 @@ module Fontisan
|
|
|
80
80
|
# @param source [Stitcher::Source] the CBDT source
|
|
81
81
|
# @param target [Ufo::Font] target UFO font to receive placeholders
|
|
82
82
|
def add_placeholder_glyphs(source, target)
|
|
83
|
+
@placeholder_names = {}
|
|
84
|
+
|
|
83
85
|
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
84
86
|
if ufo
|
|
85
87
|
ufo.glyphs.each_value do |g|
|
|
@@ -104,43 +106,28 @@ module Fontisan
|
|
|
104
106
|
glyph.width = 0
|
|
105
107
|
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
106
108
|
target.layers.default_layer.add(glyph)
|
|
109
|
+
@placeholder_names[gid] = name
|
|
107
110
|
end
|
|
108
111
|
end
|
|
109
112
|
|
|
110
|
-
# Propagate
|
|
111
|
-
#
|
|
113
|
+
# Propagate CBDT/CBLC tables from the CBDT source into the
|
|
114
|
+
# compiled font at `path`, rewriting the file in place.
|
|
112
115
|
#
|
|
113
116
|
# Reads every table from the compiled font as raw bytes (bypassing
|
|
114
117
|
# BinData #table because some tables — notably CFF2 — don't yet
|
|
115
|
-
# have round-trippable BinData models), splices in CBDT/CBLC
|
|
116
|
-
# and rewrites the file.
|
|
117
|
-
#
|
|
118
|
-
# No-op when called with a non-CBDT source or nil.
|
|
118
|
+
# have round-trippable BinData models), splices in CBDT/CBLC
|
|
119
|
+
# rebuilt with compiled-font GIDs, and rewrites the file.
|
|
119
120
|
#
|
|
120
|
-
#
|
|
121
|
+
# The CBDT/CBLC rebuild uses Subset::TableStrategy::ColorBitmapSubsetter
|
|
122
|
+
# to remap every bitmap block from source GID to compiled GID.
|
|
123
|
+
# This ensures CBLC's IndexSubTableArray references the compiled
|
|
124
|
+
# font's actual GIDs, not the source's.
|
|
121
125
|
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
126
|
+
# Falls back to raw-byte copy when the GID mapping can't be
|
|
127
|
+
# resolved (no placeholder names recorded, or compiled post table
|
|
128
|
+
# is v3.0 with no glyph names).
|
|
125
129
|
#
|
|
126
|
-
#
|
|
127
|
-
# UniqueGlyphName when their default "gid{N}" name collides
|
|
128
|
-
# with outline glyphs sharing the same donor-gid scheme
|
|
129
|
-
# (see Layer's naming contract).
|
|
130
|
-
# 2. The compiler assigns GIDs in target-namespace order, which
|
|
131
|
-
# is not the same as source-GID order once renaming happens.
|
|
132
|
-
#
|
|
133
|
-
# The bitmaps line up correctly only when the CBDT source and
|
|
134
|
-
# every outline source cover disjoint codepoint ranges (the
|
|
135
|
-
# Essenfont TTC case: emoji vs CJK Ext G). When ranges overlap,
|
|
136
|
-
# CBLC's source-GID-indexed bitmaps may point at the wrong
|
|
137
|
-
# compiled glyphs and the colour rendering for affected
|
|
138
|
-
# codepoints will fall back to outlines.
|
|
139
|
-
#
|
|
140
|
-
# A proper fix requires a CBLC rebuild pass: walk the compiled
|
|
141
|
-
# font's cmap to find the new GID for every CBDT-covered source
|
|
142
|
-
# glyph, then rewrite CBLC's IndexSubTableArray + IndexSubTable
|
|
143
|
-
# offsets to match. Tracked as a follow-up.
|
|
130
|
+
# No-op when called with a non-CBDT source or nil.
|
|
144
131
|
#
|
|
145
132
|
# @param source [Stitcher::Source, nil] the CBDT source
|
|
146
133
|
# @param path [String] compiled font file to rewrite
|
|
@@ -149,25 +136,77 @@ module Fontisan
|
|
|
149
136
|
|
|
150
137
|
compiled = FontLoader.load(path)
|
|
151
138
|
|
|
152
|
-
# Read every table as raw bytes straight from the file's table
|
|
153
|
-
# directory. We deliberately bypass #table (which parses via
|
|
154
|
-
# BinData) because some tables — notably CFF2 — don't yet have
|
|
155
|
-
# round-trippable BinData models; calling #table on them returns
|
|
156
|
-
# nil and would silently drop them from the rewritten font.
|
|
157
139
|
tables = {}
|
|
158
140
|
compiled.table_names.each do |tag|
|
|
159
141
|
raw = compiled.table_data[tag]
|
|
160
142
|
tables[tag] = raw if raw
|
|
161
143
|
end
|
|
162
144
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
tables["
|
|
166
|
-
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
145
|
+
rebuilt = rebuild_color_tables(source, compiled)
|
|
146
|
+
tables["CBDT"] = rebuilt[:cbdt] if rebuilt[:cbdt]
|
|
147
|
+
tables["CBLC"] = rebuilt[:cblc] if rebuilt[:cblc]
|
|
167
148
|
|
|
168
149
|
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
169
150
|
FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
170
151
|
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
# Rebuild CBDT/CBLC bytes with compiled-font GIDs instead of
|
|
156
|
+
# source GIDs. Delegates to Subset::TableStrategy::ColorBitmapSubsetter
|
|
157
|
+
# which already implements the offset-remap algorithm for the
|
|
158
|
+
# subsetter — the Stitcher case is the same algorithm with a
|
|
159
|
+
# different GID mapping (source → compiled instead of source →
|
|
160
|
+
# sequential subset GID).
|
|
161
|
+
#
|
|
162
|
+
# Falls back to raw-byte copy when the GID mapping can't be
|
|
163
|
+
# resolved (no placeholder names recorded, or compiled post/cmap
|
|
164
|
+
# can't reverse-map them).
|
|
165
|
+
def rebuild_color_tables(source, compiled)
|
|
166
|
+
source_cbdt = source.raw_table_bytes("CBDT")
|
|
167
|
+
source_cblc = source.raw_table_bytes("CBLC")
|
|
168
|
+
return {} unless source_cbdt && source_cblc
|
|
169
|
+
|
|
170
|
+
gid_map = resolve_gid_mapping(compiled)
|
|
171
|
+
return raw_bytes_fallback(source_cbdt, source_cblc) unless gid_map
|
|
172
|
+
|
|
173
|
+
mapping = Subset::GlyphMapping.new(mapping: gid_map)
|
|
174
|
+
subsetter = Subset::TableStrategy::ColorBitmapSubsetter.new(
|
|
175
|
+
font: source.font, mapping: mapping,
|
|
176
|
+
).build
|
|
177
|
+
|
|
178
|
+
{ cbdt: subsetter.cbdt_bytes, cblc: subsetter.cblc_bytes }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Build {source_gid => compiled_gid} from the placeholder names
|
|
182
|
+
# recorded during add_placeholder_glyphs. Walks the compiled
|
|
183
|
+
# font's post table to find each placeholder's compiled GID.
|
|
184
|
+
# Returns nil when the mapping can't be resolved (e.g., no
|
|
185
|
+
# placeholders recorded, or post v3.0 with no names).
|
|
186
|
+
def resolve_gid_mapping(compiled)
|
|
187
|
+
return nil unless @placeholder_names && !@placeholder_names.empty?
|
|
188
|
+
|
|
189
|
+
post = compiled.table("post")
|
|
190
|
+
return nil unless post
|
|
191
|
+
|
|
192
|
+
names = post.glyph_names
|
|
193
|
+
return nil unless names
|
|
194
|
+
|
|
195
|
+
name_to_gid = {}
|
|
196
|
+
names.each_with_index { |name, gid| name_to_gid[name] = gid }
|
|
197
|
+
|
|
198
|
+
gid_map = {}
|
|
199
|
+
@placeholder_names.each do |source_gid, placeholder_name|
|
|
200
|
+
compiled_gid = name_to_gid[placeholder_name]
|
|
201
|
+
gid_map[source_gid] = compiled_gid if compiled_gid
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
gid_map.empty? ? nil : gid_map
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def raw_bytes_fallback(cbdt_bytes, cblc_bytes)
|
|
208
|
+
{ cbdt: cbdt_bytes, cblc: cblc_bytes }
|
|
209
|
+
end
|
|
171
210
|
end
|
|
172
211
|
end
|
|
173
212
|
end
|
|
@@ -52,12 +52,17 @@ module Fontisan
|
|
|
52
52
|
#
|
|
53
53
|
# @example Create mapping that retains GIDs
|
|
54
54
|
# mapping = GlyphMapping.new([0, 3, 5, 10], retain_gids: true)
|
|
55
|
-
def initialize(old_glyph_ids, retain_gids: false)
|
|
55
|
+
def initialize(old_glyph_ids = [], retain_gids: false, mapping: nil)
|
|
56
56
|
@old_to_new = {}
|
|
57
57
|
@new_to_old = {}
|
|
58
58
|
@retain_gids = retain_gids
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
if mapping
|
|
61
|
+
@old_to_new = mapping
|
|
62
|
+
@new_to_old = mapping.invert
|
|
63
|
+
else
|
|
64
|
+
build_mappings(old_glyph_ids)
|
|
65
|
+
end
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
# Get new glyph ID for an old glyph ID
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -408,30 +408,134 @@ module Fontisan
|
|
|
408
408
|
true
|
|
409
409
|
end
|
|
410
410
|
|
|
411
|
+
# Adobe Standard Encoding glyph names that map 1:1 to CFF SIDs
|
|
412
|
+
# 0..95. Source: Adobe CFF Specification 1.0 (TN 5176), Appendix A.
|
|
413
|
+
# These cover the printable ASCII range plus .notdef — the most
|
|
414
|
+
# common glyph names encountered in CFF fonts.
|
|
415
|
+
STANDARD_STRINGS_ASCII = [
|
|
416
|
+
".notdef", # SID 0
|
|
417
|
+
"space", # SID 1 (code 32)
|
|
418
|
+
"exclam", # SID 2 (code 33)
|
|
419
|
+
"quotedbl", # SID 3 (code 34)
|
|
420
|
+
"numbersign", # SID 4 (code 35)
|
|
421
|
+
"dollar", # SID 5 (code 36)
|
|
422
|
+
"percent", # SID 6 (code 37)
|
|
423
|
+
"ampersand", # SID 7 (code 38)
|
|
424
|
+
"quoteright", # SID 8 (code 39)
|
|
425
|
+
"parenleft", # SID 9 (code 40)
|
|
426
|
+
"parenright", # SID 10 (code 41)
|
|
427
|
+
"asterisk", # SID 11 (code 42)
|
|
428
|
+
"plus", # SID 12 (code 43)
|
|
429
|
+
"comma", # SID 13 (code 44)
|
|
430
|
+
"hyphen", # SID 14 (code 45)
|
|
431
|
+
"period", # SID 15 (code 46)
|
|
432
|
+
"slash", # SID 16 (code 47)
|
|
433
|
+
"zero", # SID 17 (code 48)
|
|
434
|
+
"one", # SID 18 (code 49)
|
|
435
|
+
"two", # SID 19 (code 50)
|
|
436
|
+
"three", # SID 20 (code 51)
|
|
437
|
+
"four", # SID 21 (code 52)
|
|
438
|
+
"five", # SID 22 (code 53)
|
|
439
|
+
"six", # SID 23 (code 54)
|
|
440
|
+
"seven", # SID 24 (code 55)
|
|
441
|
+
"eight", # SID 25 (code 56)
|
|
442
|
+
"nine", # SID 26 (code 57)
|
|
443
|
+
"colon", # SID 27 (code 58)
|
|
444
|
+
"semicolon", # SID 28 (code 59)
|
|
445
|
+
"less", # SID 29 (code 60)
|
|
446
|
+
"equal", # SID 30 (code 61)
|
|
447
|
+
"greater", # SID 31 (code 62)
|
|
448
|
+
"question", # SID 32 (code 63)
|
|
449
|
+
"at", # SID 33 (code 64)
|
|
450
|
+
"A", # SID 34 (code 65)
|
|
451
|
+
"B", # SID 35 (code 66)
|
|
452
|
+
"C", # SID 36 (code 67)
|
|
453
|
+
"D", # SID 37 (code 68)
|
|
454
|
+
"E", # SID 38 (code 69)
|
|
455
|
+
"F", # SID 39 (code 70)
|
|
456
|
+
"G", # SID 40 (code 71)
|
|
457
|
+
"H", # SID 41 (code 72)
|
|
458
|
+
"I", # SID 42 (code 73)
|
|
459
|
+
"J", # SID 43 (code 74)
|
|
460
|
+
"K", # SID 44 (code 75)
|
|
461
|
+
"L", # SID 45 (code 76)
|
|
462
|
+
"M", # SID 46 (code 77)
|
|
463
|
+
"N", # SID 47 (code 78)
|
|
464
|
+
"O", # SID 48 (code 79)
|
|
465
|
+
"P", # SID 49 (code 80)
|
|
466
|
+
"Q", # SID 50 (code 81)
|
|
467
|
+
"R", # SID 51 (code 82)
|
|
468
|
+
"S", # SID 52 (code 83)
|
|
469
|
+
"T", # SID 53 (code 84)
|
|
470
|
+
"U", # SID 54 (code 85)
|
|
471
|
+
"V", # SID 55 (code 86)
|
|
472
|
+
"W", # SID 56 (code 87)
|
|
473
|
+
"X", # SID 57 (code 88)
|
|
474
|
+
"Y", # SID 58 (code 89)
|
|
475
|
+
"Z", # SID 59 (code 90)
|
|
476
|
+
"bracketleft", # SID 60 (code 91)
|
|
477
|
+
"backslash", # SID 61 (code 92)
|
|
478
|
+
"bracketright", # SID 62 (code 93)
|
|
479
|
+
"asciicircum", # SID 63 (code 94)
|
|
480
|
+
"underscore", # SID 64 (code 95)
|
|
481
|
+
"quoteleft", # SID 65 (code 96)
|
|
482
|
+
"a", # SID 66 (code 97)
|
|
483
|
+
"b", # SID 67 (code 98)
|
|
484
|
+
"c", # SID 68 (code 99)
|
|
485
|
+
"d", # SID 69 (code 100)
|
|
486
|
+
"e", # SID 70 (code 101)
|
|
487
|
+
"f", # SID 71 (code 102)
|
|
488
|
+
"g", # SID 72 (code 103)
|
|
489
|
+
"h", # SID 73 (code 104)
|
|
490
|
+
"i", # SID 74 (code 105)
|
|
491
|
+
"j", # SID 75 (code 106)
|
|
492
|
+
"k", # SID 76 (code 107)
|
|
493
|
+
"l", # SID 77 (code 108)
|
|
494
|
+
"m", # SID 78 (code 109)
|
|
495
|
+
"n", # SID 79 (code 110)
|
|
496
|
+
"o", # SID 80 (code 111)
|
|
497
|
+
"p", # SID 81 (code 112)
|
|
498
|
+
"q", # SID 82 (code 113)
|
|
499
|
+
"r", # SID 83 (code 114)
|
|
500
|
+
"s", # SID 84 (code 115)
|
|
501
|
+
"t", # SID 85 (code 116)
|
|
502
|
+
"u", # SID 86 (code 117)
|
|
503
|
+
"v", # SID 87 (code 118)
|
|
504
|
+
"w", # SID 88 (code 119)
|
|
505
|
+
"x", # SID 89 (code 120)
|
|
506
|
+
"y", # SID 90 (code 121)
|
|
507
|
+
"z", # SID 91 (code 122)
|
|
508
|
+
"braceleft", # SID 92 (code 123)
|
|
509
|
+
"bar", # SID 93 (code 124)
|
|
510
|
+
"braceright", # SID 94 (code 125)
|
|
511
|
+
"asciitilde", # SID 95 (code 126)
|
|
512
|
+
].freeze
|
|
513
|
+
|
|
514
|
+
# First SID outside the ASCII subset. SIDs in
|
|
515
|
+
# EXTENDED_STRINGS_START..390 cover ligatures, currency symbols,
|
|
516
|
+
# Cyrillic (afii*), etc. Resolving those requires the full
|
|
517
|
+
# Adobe CFF spec Appendix A table; tracked as a follow-up to
|
|
518
|
+
# avoid embedding unverified data.
|
|
519
|
+
EXTENDED_STRINGS_START = STANDARD_STRINGS_ASCII.length
|
|
520
|
+
|
|
411
521
|
private
|
|
412
522
|
|
|
413
|
-
# Get a standard CFF string by SID
|
|
414
|
-
#
|
|
415
|
-
# This is a placeholder that returns a generic string.
|
|
416
|
-
# A complete implementation would include all 391 standard strings
|
|
417
|
-
# from CFF spec Appendix A.
|
|
523
|
+
# Get a standard CFF string by SID (0-390).
|
|
418
524
|
#
|
|
419
|
-
#
|
|
525
|
+
# The ASCII subset (SID 0-95) is fully covered. Extended SIDs
|
|
526
|
+
# (96-390) cover ligatures, currency, Cyrillic, etc., and
|
|
527
|
+
# require the full Adobe CFF spec Appendix A table to fill in
|
|
528
|
+
# correctly. Until that follow-up lands, extended SIDs return
|
|
529
|
+
# nil so callers can detect the gap and fall back to the
|
|
530
|
+
# custom string index.
|
|
420
531
|
#
|
|
421
532
|
# @param sid [Integer] String ID (0-390)
|
|
422
|
-
# @return [String]
|
|
533
|
+
# @return [String, nil] standard string, or nil for uncovered SIDs
|
|
423
534
|
def standard_string(sid)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
when 0 then ".notdef"
|
|
429
|
-
when 1 then "space"
|
|
430
|
-
when 2 then "exclam"
|
|
431
|
-
# ... (388 more standard strings)
|
|
432
|
-
else
|
|
433
|
-
".notdef" # Fallback
|
|
434
|
-
end
|
|
535
|
+
return nil if sid.negative?
|
|
536
|
+
return STANDARD_STRINGS_ASCII[sid] if sid < EXTENDED_STRINGS_START
|
|
537
|
+
|
|
538
|
+
nil
|
|
435
539
|
end
|
|
436
540
|
|
|
437
541
|
# Get the Charset for a specific font
|
|
@@ -215,18 +215,78 @@ module Fontisan
|
|
|
215
215
|
end
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
-
# OTF: extract outlines from CFF charstrings.
|
|
219
|
-
# for now, stub with advance widths only (no contours).
|
|
218
|
+
# OTF: extract outlines from CFF charstrings.
|
|
220
219
|
def self.extract_cff_glyphs(font, ufo, cmap, widths, num_glyphs)
|
|
220
|
+
cff = font.table("CFF ")
|
|
221
|
+
return extract_cff_glyphs_stub(font, ufo, cmap, widths, num_glyphs) unless cff
|
|
222
|
+
|
|
221
223
|
num_glyphs.times do |gid|
|
|
222
|
-
glyph_name = glyph_name_for(font, gid) || "
|
|
224
|
+
glyph_name = glyph_name_for(font, gid) || "gid#{gid}"
|
|
223
225
|
ufo_glyph = Ufo::Glyph.new(name: glyph_name)
|
|
224
226
|
ufo_glyph.width = widths.fetch(gid, 0).to_f
|
|
225
227
|
cmap.fetch(gid, []).each { |cp| ufo_glyph.add_unicode(cp) }
|
|
228
|
+
|
|
229
|
+
charstring = cff.charstring_for_glyph(gid)
|
|
230
|
+
convert_cff_path_to_ufo(charstring.path, ufo_glyph) if charstring
|
|
231
|
+
|
|
226
232
|
ufo.layers.default_layer.add(ufo_glyph)
|
|
227
233
|
end
|
|
228
234
|
end
|
|
229
235
|
|
|
236
|
+
# Fallback: widths only, no contours (used when CFF table can't
|
|
237
|
+
# be parsed — e.g., corrupted or unsupported variant).
|
|
238
|
+
def self.extract_cff_glyphs_stub(font, ufo, cmap, widths, num_glyphs)
|
|
239
|
+
num_glyphs.times do |gid|
|
|
240
|
+
glyph_name = glyph_name_for(font, gid) || "gid#{gid}"
|
|
241
|
+
ufo_glyph = Ufo::Glyph.new(name: glyph_name)
|
|
242
|
+
ufo_glyph.width = widths.fetch(gid, 0).to_f
|
|
243
|
+
cmap.fetch(gid, []).each { |cp| ufo_glyph.add_unicode(cp) }
|
|
244
|
+
ufo.layers.default_layer.add(ufo_glyph)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Convert a CFF CharString path (array of command hashes) to
|
|
249
|
+
# UFO contours. Each :move_to starts a new contour; :line_to
|
|
250
|
+
# and :curve_to append points to the current contour. Cubic
|
|
251
|
+
# bezier curves produce 2 offcurve + 1 curve points per
|
|
252
|
+
# segment (GLIF convention).
|
|
253
|
+
def self.convert_cff_path_to_ufo(path, glyph)
|
|
254
|
+
return if path.empty?
|
|
255
|
+
|
|
256
|
+
current_contour = nil
|
|
257
|
+
|
|
258
|
+
path.each do |cmd|
|
|
259
|
+
case cmd[:type]
|
|
260
|
+
when :move_to
|
|
261
|
+
glyph.add_contour(current_contour) if current_contour
|
|
262
|
+
current_contour = Ufo::Contour.new
|
|
263
|
+
current_contour.points << Ufo::Point.new(
|
|
264
|
+
x: cmd[:x].to_i, y: cmd[:y].to_i, type: "move",
|
|
265
|
+
)
|
|
266
|
+
when :line_to
|
|
267
|
+
next unless current_contour
|
|
268
|
+
|
|
269
|
+
current_contour.points << Ufo::Point.new(
|
|
270
|
+
x: cmd[:x].to_i, y: cmd[:y].to_i, type: "line",
|
|
271
|
+
)
|
|
272
|
+
when :curve_to
|
|
273
|
+
next unless current_contour
|
|
274
|
+
|
|
275
|
+
current_contour.points << Ufo::Point.new(
|
|
276
|
+
x: cmd[:x1].to_i, y: cmd[:y1].to_i, type: "offcurve",
|
|
277
|
+
)
|
|
278
|
+
current_contour.points << Ufo::Point.new(
|
|
279
|
+
x: cmd[:x2].to_i, y: cmd[:y2].to_i, type: "offcurve",
|
|
280
|
+
)
|
|
281
|
+
current_contour.points << Ufo::Point.new(
|
|
282
|
+
x: cmd[:x].to_i, y: cmd[:y].to_i, type: "curve",
|
|
283
|
+
)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
glyph.add_contour(current_contour) if current_contour
|
|
288
|
+
end
|
|
289
|
+
|
|
230
290
|
# Look up a glyph name from the post table (v2.0) or synthesize.
|
|
231
291
|
def self.glyph_name_for(font, gid)
|
|
232
292
|
post = font.table("post")
|
data/lib/fontisan/ufo/font.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Fontisan
|
|
|
11
11
|
# The class is the read/write API; serialization is handled by
|
|
12
12
|
# Fontisan::Ufo::Reader and Fontisan::Ufo::Writer.
|
|
13
13
|
class Font
|
|
14
|
-
attr_accessor :path, :info, :features, :kerning, :lib, :ufo_version
|
|
14
|
+
attr_accessor :path, :info, :features, :kerning, :groups, :lib, :ufo_version
|
|
15
15
|
attr_reader :layers, :data, :images
|
|
16
16
|
|
|
17
17
|
def initialize
|
|
@@ -21,6 +21,7 @@ module Fontisan
|
|
|
21
21
|
@layers = LayerSet.new
|
|
22
22
|
@features = Features.new
|
|
23
23
|
@kerning = Kerning.new
|
|
24
|
+
@groups = Groups.new
|
|
24
25
|
@lib = Lib.new
|
|
25
26
|
@data = nil # DataSet needs the Font ref, set by Reader
|
|
26
27
|
@images = ImageSet.new
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Ufo
|
|
5
|
+
# Group definitions parsed from `groups.plist`. Maps a group name
|
|
6
|
+
# to a list of glyph names belonging to that group:
|
|
7
|
+
#
|
|
8
|
+
# { "MMK_L_A" => ["A", "Agrave", "Aacute", ...],
|
|
9
|
+
# "MMK_R_T" => ["T", "Tcommaaccent", ...] }
|
|
10
|
+
#
|
|
11
|
+
# Group pair keys in `kerning.plist` (e.g. `"MMK_L_A MMK_R_T"`)
|
|
12
|
+
# reference these names — kern writer + GPOS builder resolve them
|
|
13
|
+
# to glyph sets before emitting PairPos records.
|
|
14
|
+
class Groups
|
|
15
|
+
attr_reader :groups
|
|
16
|
+
|
|
17
|
+
def initialize(values = {})
|
|
18
|
+
@groups = values
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# All group names.
|
|
22
|
+
def names
|
|
23
|
+
@groups.keys
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Glyph names belonging to +name+. Empty array if unknown.
|
|
27
|
+
def glyphs(name)
|
|
28
|
+
@groups[name] || []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Whether +name+ is a known group.
|
|
32
|
+
def include?(name)
|
|
33
|
+
@groups.key?(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def empty?
|
|
37
|
+
@groups.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_plist
|
|
41
|
+
@groups
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/fontisan/ufo/reader.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Fontisan
|
|
|
32
32
|
read_layercontents
|
|
33
33
|
read_glyphs_contents
|
|
34
34
|
read_kerning
|
|
35
|
+
read_groups
|
|
35
36
|
read_features
|
|
36
37
|
read_lib
|
|
37
38
|
@font
|
|
@@ -123,6 +124,14 @@ module Fontisan
|
|
|
123
124
|
@font.kerning = Kerning.new(data)
|
|
124
125
|
end
|
|
125
126
|
|
|
127
|
+
def read_groups
|
|
128
|
+
path = join(@font.path, "groups.plist")
|
|
129
|
+
return unless File.exist?(path)
|
|
130
|
+
|
|
131
|
+
data = Plist.parse(File.read(path))
|
|
132
|
+
@font.groups = Groups.new(data)
|
|
133
|
+
end
|
|
134
|
+
|
|
126
135
|
def read_features
|
|
127
136
|
path = join(@font.path, "features.fea")
|
|
128
137
|
return unless File.exist?(path)
|
data/lib/fontisan/ufo.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Fontisan
|
|
|
33
33
|
autoload :Kerning, "fontisan/ufo/kerning"
|
|
34
34
|
autoload :Features, "fontisan/ufo/features"
|
|
35
35
|
autoload :Lib, "fontisan/ufo/lib"
|
|
36
|
+
autoload :Groups, "fontisan/ufo/groups"
|
|
36
37
|
autoload :DataSet, "fontisan/ufo/data_set"
|
|
37
38
|
autoload :ImageSet, "fontisan/ufo/image_set"
|
|
38
39
|
autoload :Plist, "fontisan/ufo/plist"
|
data/lib/fontisan/version.rb
CHANGED
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.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -147,6 +147,7 @@ files:
|
|
|
147
147
|
- TODO.improvements/12-cbdt-fixture-bindata-conversion.md
|
|
148
148
|
- TODO.improvements/13-split-octokit-fetcher.md
|
|
149
149
|
- TODO.improvements/14-rubocop-baseline-chip.md
|
|
150
|
+
- TODO.improvements/15-cff-cff2-subsetter-strategy.md
|
|
150
151
|
- TODO.improvements/README.md
|
|
151
152
|
- benchmark/compile_benchmark.rb
|
|
152
153
|
- benchmark/variation_quick_bench.rb
|
|
@@ -704,6 +705,7 @@ files:
|
|
|
704
705
|
- lib/fontisan/ufo/font.rb
|
|
705
706
|
- lib/fontisan/ufo/glyph.rb
|
|
706
707
|
- lib/fontisan/ufo/glyph_exists_error.rb
|
|
708
|
+
- lib/fontisan/ufo/groups.rb
|
|
707
709
|
- lib/fontisan/ufo/guideline.rb
|
|
708
710
|
- lib/fontisan/ufo/image.rb
|
|
709
711
|
- lib/fontisan/ufo/image_set.rb
|