fontisan 0.4.27 → 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 +3 -2
- data/lib/fontisan/config/subset_profiles.yml +4 -0
- data/lib/fontisan/subset/table_strategy/head.rb +1 -0
- 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
|
@@ -7,12 +7,13 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
7
7
|
|
|
8
8
|
### P0 — Correctness gaps (open bugs / silent failures)
|
|
9
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
|
|
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
19
|
- [x] ~~[07 — CPAL v1 header fields](07-cpal-v1-header-fields.md)~~ ✓ Done (v0.4.25)
|
|
@@ -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
|