fontisan 0.4.28 → 0.4.30
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/README.md +3 -3
- data/docs/guide/cli/subset.md +3 -3
- data/lib/fontisan/config/subset_profiles.yml +3 -3
- data/lib/fontisan/subset/table_strategy/cff.rb +92 -0
- data/lib/fontisan/subset/table_strategy.rb +2 -0
- data/lib/fontisan/ufo/compile/feature_writers/kern.rb +2 -3
- data/lib/fontisan/ufo/compile/glyf_loca.rb +70 -11
- data/lib/fontisan/ufo/compile/gpos.rb +28 -7
- data/lib/fontisan/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5dd9aa72ac0517bcfbfed3d6eb3528af3e0b65a52bc721d14ba5fb545198e48f
|
|
4
|
+
data.tar.gz: be1d6d7379a3bd0cda435d02b91e5d7fc213736483e3aac66b93280d63261669
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b889fec5459a0d420ffe21d3ec792f107c69060eeddba8cf357e1a83b1e37669c3b6d017b41921b6d5be28519d51ef13ef2ab4da2d00e9ccfb9a31c204310150
|
|
7
|
+
data.tar.gz: 69952d13b5c18685c8b22d92b5f56c8ed2cfc89da8a5324080a9f206823d1999baf947718fbf56e77e239de422dcbf21033e9a7d060a30e5e5e5df6482727ce0
|
data/TODO.improvements/README.md
CHANGED
|
@@ -12,8 +12,8 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
12
12
|
|
|
13
13
|
### P1 — High-value features
|
|
14
14
|
- [03 — `fontisan audit` command (identity+style+features lens)](03-fontisan-audit-command.md)
|
|
15
|
-
- [04 — UFO composite glyph encoding](04-ufo-composite-glyph-encoding.md)
|
|
16
|
-
- [05 — OTF compiler real CFF charstrings](05-otf-compiler-real-cff.md)
|
|
15
|
+
- [x] ~~[04 — UFO composite glyph encoding](04-ufo-composite-glyph-encoding.md)~~ ✓ Done
|
|
16
|
+
- [x] ~~[05 — OTF compiler real CFF charstrings](05-otf-compiler-real-cff.md)~~ ✓ Already working (discovered in PR #117 — Cff.build produces real Type 2 charstrings; stale TODO marker removed)
|
|
17
17
|
|
|
18
18
|
### P2 — Specialist feature parity
|
|
19
19
|
- [x] ~~[07 — CPAL v1 header fields](07-cpal-v1-header-fields.md)~~ ✓ Done (v0.4.25)
|
|
@@ -21,7 +21,7 @@ Each file is `NN-short-name.md` where `NN` is the priority order.
|
|
|
21
21
|
- [06 — CFF2 blend/vsindex operators](06-cff2-blend-vsindex-operators.md)
|
|
22
22
|
- [09 — Type 1 seac expansion](09-type1-seac-expansion.md)
|
|
23
23
|
- [10 — UFO image set + feature writers](10-ufo-image-set-feature-writers.md)
|
|
24
|
-
- [11 — kern groups.plist support](11-kern-groups-plist.md)
|
|
24
|
+
- [x] ~~[11 — kern groups.plist support](11-kern-groups-plist.md)~~ ✓ Done (Groups model in PR #116 + GPOS group resolution in this PR)
|
|
25
25
|
|
|
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)
|
data/docs/guide/cli/subset.md
CHANGED
|
@@ -93,9 +93,9 @@ The `--profile` option controls which font tables the subset retains. The right
|
|
|
93
93
|
|
|
94
94
|
| Profile | When to use | Notable tables dropped |
|
|
95
95
|
|---------|-------------|------------------------|
|
|
96
|
-
| `pdf` (default) | PDF embedding, smallest valid output | GSUB, GPOS
|
|
97
|
-
| `web` | Browser/web font, **retains color-emoji bitmaps** | nothing that browsers need |
|
|
98
|
-
| `minimal` | Absolute smallest core that still renders | glyf/loca,
|
|
96
|
+
| `pdf` (default) | PDF embedding, smallest valid output | GSUB, GPOS |
|
|
97
|
+
| `web` | Browser/web font, **retains color-emoji bitmaps + CFF outlines** | nothing that browsers need |
|
|
98
|
+
| `minimal` | Absolute smallest core that still renders | glyf/loca, CFF/CFF2, GSUB/GPOS |
|
|
99
99
|
| `full` | Lossless re-export | (none) |
|
|
100
100
|
|
|
101
101
|
```bash
|
|
@@ -38,7 +38,7 @@ pdf:
|
|
|
38
38
|
- post
|
|
39
39
|
- loca
|
|
40
40
|
- glyf
|
|
41
|
-
- CFF
|
|
41
|
+
- "CFF "
|
|
42
42
|
- CFF2
|
|
43
43
|
|
|
44
44
|
# Web Profile: Tables required for web font usage
|
|
@@ -55,7 +55,7 @@ web:
|
|
|
55
55
|
- post
|
|
56
56
|
- loca
|
|
57
57
|
- glyf
|
|
58
|
-
- CFF
|
|
58
|
+
- "CFF "
|
|
59
59
|
- CFF2
|
|
60
60
|
- GSUB
|
|
61
61
|
- GPOS
|
|
@@ -98,7 +98,7 @@ full:
|
|
|
98
98
|
- GDEF
|
|
99
99
|
- BASE
|
|
100
100
|
- JSTF
|
|
101
|
-
- CFF
|
|
101
|
+
- "CFF "
|
|
102
102
|
- CFF2
|
|
103
103
|
- VORG
|
|
104
104
|
- kern
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# CFF (Compact Font Format) subsetter strategy.
|
|
7
|
+
#
|
|
8
|
+
# Subsets CFF tables by routing through the UFO model:
|
|
9
|
+
#
|
|
10
|
+
# source font → Ufo::Convert::FromBinData → UFO (with contours)
|
|
11
|
+
# → filter glyphs to mapping →
|
|
12
|
+
# → Ufo::Compile::Cff.build → new CFF bytes
|
|
13
|
+
#
|
|
14
|
+
# This is the architecturally preferred path: the UFO model is
|
|
15
|
+
# the canonical representation, CFF is a serialization. The
|
|
16
|
+
# strategy reuses the existing compile pipeline rather than
|
|
17
|
+
# reimplementing CFF INDEX arithmetic.
|
|
18
|
+
#
|
|
19
|
+
# Glyphs not in the mapping are dropped from the UFO before
|
|
20
|
+
# compilation. The Cff.build pipeline generates the correct
|
|
21
|
+
# charset (GID → SID mapping) from the retained glyph names.
|
|
22
|
+
#
|
|
23
|
+
# See TODO #15 for the full design rationale and the CFF2
|
|
24
|
+
# extension path.
|
|
25
|
+
class Cff
|
|
26
|
+
# @param context [SubsetContext]
|
|
27
|
+
# @param tag [String] "CFF "
|
|
28
|
+
# @param table [Object] parsed CFF table (unused — we work
|
|
29
|
+
# from the source font directly)
|
|
30
|
+
# @return [String] subset CFF table bytes
|
|
31
|
+
def self.call(context:, tag:, table:)
|
|
32
|
+
source_font = context.font
|
|
33
|
+
mapping = context.mapping
|
|
34
|
+
|
|
35
|
+
ufo = Ufo::Convert::FromBinData.convert(source_font)
|
|
36
|
+
filter_ufo_glyphs!(ufo, mapping)
|
|
37
|
+
|
|
38
|
+
# Rename the first glyph (source GID 0) to .notdef so the
|
|
39
|
+
# CFF compiler's charset generation produces a valid .notdef
|
|
40
|
+
# entry at GID 0.
|
|
41
|
+
first_name = ufo.glyphs.keys.first
|
|
42
|
+
if first_name && first_name != ".notdef"
|
|
43
|
+
notdef = ufo.glyphs.delete(first_name)
|
|
44
|
+
notdef = Ufo::Glyph.new(name: ".notdef") if notdef.nil?
|
|
45
|
+
ufo.glyphs[".notdef"] = notdef
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
glyphs = ufo.layers.default_layer.each.to_a
|
|
49
|
+
Fontisan::Ufo::Compile::Cff.build(ufo, glyphs: glyphs)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Drop glyphs from the UFO that are not in the mapping's
|
|
53
|
+
# new-id set. Keeps .notdef (GID 0) regardless of mapping.
|
|
54
|
+
#
|
|
55
|
+
# @param ufo [Ufo::Font]
|
|
56
|
+
# @param mapping [GlyphMapping]
|
|
57
|
+
def self.filter_ufo_glyphs!(ufo, mapping)
|
|
58
|
+
retained_old_ids = Set.new(mapping.old_ids)
|
|
59
|
+
|
|
60
|
+
# The UFO doesn't carry GID info after conversion; we rely
|
|
61
|
+
# on the mapping's old_ids. Since the UFO was converted
|
|
62
|
+
# from the same source font, the UFO glyph order matches
|
|
63
|
+
# the source GID order. So GID N in the source = Nth glyph
|
|
64
|
+
# added to the UFO default layer.
|
|
65
|
+
all_names = ufo.glyphs.keys
|
|
66
|
+
names_to_keep = all_names.each_with_index.with_object(Set.new) do |(name, gid), keep|
|
|
67
|
+
keep << name if retained_old_ids.include?(gid)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Always keep .notdef
|
|
71
|
+
names_to_keep << ".notdef" if all_names.any?(".notdef")
|
|
72
|
+
|
|
73
|
+
ufo.glyphs.select! { |name, _| names_to_keep.include?(name) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Ensure .notdef is present at GID 0. The CFF compiler
|
|
77
|
+
# expects it.
|
|
78
|
+
#
|
|
79
|
+
# @param ufo [Ufo::Font]
|
|
80
|
+
def self.ensure_notdef_present!(ufo)
|
|
81
|
+
return if ufo.glyphs.key?(".notdef")
|
|
82
|
+
|
|
83
|
+
notdef = Ufo::Glyph.new(name: ".notdef")
|
|
84
|
+
notdef.width = 0
|
|
85
|
+
ufo.layers.default_layer.add(notdef)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private_class_method :filter_ufo_glyphs!, :ensure_notdef_present!
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -16,6 +16,7 @@ module Fontisan
|
|
|
16
16
|
module TableStrategy
|
|
17
17
|
autoload :Cblc, "fontisan/subset/table_strategy/cblc"
|
|
18
18
|
autoload :Cbdt, "fontisan/subset/table_strategy/cbdt"
|
|
19
|
+
autoload :Cff, "fontisan/subset/table_strategy/cff"
|
|
19
20
|
autoload :Cmap, "fontisan/subset/table_strategy/cmap"
|
|
20
21
|
autoload :ColorBitmapPlacement,
|
|
21
22
|
"fontisan/subset/table_strategy/color_bitmap_placement"
|
|
@@ -55,6 +56,7 @@ module Fontisan
|
|
|
55
56
|
"name" => :Name,
|
|
56
57
|
"head" => :Head,
|
|
57
58
|
"OS/2" => :Os2,
|
|
59
|
+
"CFF " => :Cff,
|
|
58
60
|
"CBDT" => :Cbdt,
|
|
59
61
|
"CBLC" => :Cblc,
|
|
60
62
|
}.freeze
|
|
@@ -13,9 +13,8 @@ module Fontisan
|
|
|
13
13
|
# omits the GPOS table entirely.
|
|
14
14
|
#
|
|
15
15
|
# Group-based kerning keys (e.g. +"@MMK_L_A @MMK_R_B"+) are
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# groups.plist data (TODO: groups.plist support is partial).
|
|
16
|
+
# resolved to individual glyph pairs by the GPOS builder via
|
|
17
|
+
# the UFO's groups.plist data (font.groups).
|
|
19
18
|
class Kern < Base
|
|
20
19
|
# GPOS lookup type 2 — PairPos (pair-positioning).
|
|
21
20
|
LOOKUP_TYPE = 2
|
|
@@ -23,11 +23,14 @@ module Fontisan
|
|
|
23
23
|
# @param glyphs [Array<Fontisan::Ufo::Glyph>] in gid order
|
|
24
24
|
# @return [Hash<String, String>] {"glyf" => bytes, "loca" => bytes}
|
|
25
25
|
def self.build(_font, glyphs:)
|
|
26
|
+
name_to_gid = {}
|
|
27
|
+
glyphs.each_with_index { |g, i| name_to_gid[g.name] = i }
|
|
28
|
+
|
|
26
29
|
glyf_bytes = +""
|
|
27
30
|
offsets = [0]
|
|
28
31
|
|
|
29
32
|
glyphs.each do |glyph|
|
|
30
|
-
glyf_bytes << encode_glyph(glyph)
|
|
33
|
+
glyf_bytes << encode_glyph(glyph, name_to_gid)
|
|
31
34
|
glyf_bytes << "\x00" while glyf_bytes.bytesize.odd? # 2-byte align
|
|
32
35
|
offsets << glyf_bytes.bytesize
|
|
33
36
|
end
|
|
@@ -47,9 +50,9 @@ module Fontisan
|
|
|
47
50
|
|
|
48
51
|
# Encode a single glyph into glyf bytes. Empty glyphs (no
|
|
49
52
|
# contours, no components) produce zero bytes per spec.
|
|
50
|
-
def self.encode_glyph(glyph)
|
|
53
|
+
def self.encode_glyph(glyph, name_to_gid = {})
|
|
51
54
|
return "" if glyph.contours.empty? && glyph.components.empty?
|
|
52
|
-
return encode_composite(glyph) if glyph.composite?
|
|
55
|
+
return encode_composite(glyph, name_to_gid) if glyph.composite?
|
|
53
56
|
|
|
54
57
|
encode_simple(glyph)
|
|
55
58
|
end
|
|
@@ -131,16 +134,72 @@ module Fontisan
|
|
|
131
134
|
[flags, x_bytes, y_bytes]
|
|
132
135
|
end
|
|
133
136
|
|
|
134
|
-
# Composite glyph encoding
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
# Composite glyph encoding per OpenType spec section 5.6.
|
|
138
|
+
# Emits a compound glyph record with one component entry per
|
|
139
|
+
# UFO component reference. Each component's base glyph name is
|
|
140
|
+
# resolved to a compiled GID via the name_to_gid map.
|
|
141
|
+
#
|
|
142
|
+
# Transform encoding:
|
|
143
|
+
# - identity transform → no transform flags
|
|
144
|
+
# - uniform scale (a=d, b=c=0) → WE_HAVE_A_SCALE (1 F2Dot14)
|
|
145
|
+
# - non-uniform scale (b=c=0, a≠d) → WE_HAVE_AN_X_AND_Y_SCALE (2 F2Dot14)
|
|
146
|
+
# - full 2×2 → WE_HAVE_A_TWO_BY_TWO (4 F2Dot14)
|
|
147
|
+
def self.encode_composite(glyph, name_to_gid)
|
|
148
|
+
header = [-1, 0, 0, 0, 0].pack("s>n4")
|
|
149
|
+
|
|
150
|
+
body = +""
|
|
151
|
+
components = glyph.components
|
|
152
|
+
components.each_with_index do |comp, idx|
|
|
153
|
+
gid = name_to_gid[comp.base_glyph]
|
|
154
|
+
next unless gid
|
|
155
|
+
|
|
156
|
+
flags = 0x0002 # ARGS_ARE_XY_VALUES (always for UFO)
|
|
157
|
+
|
|
158
|
+
dx = comp.transformation ? comp.transformation.e.to_i : 0
|
|
159
|
+
dy = comp.transformation ? comp.transformation.f.to_i : 0
|
|
160
|
+
|
|
161
|
+
flags |= 0x0001 if dx < -128 || dx > 127 || dy < -128 || dy > 127
|
|
162
|
+
|
|
163
|
+
t = comp.transformation
|
|
164
|
+
if t && !t.identity?
|
|
165
|
+
flags |= if t.b.zero? && t.c.zero? && (t.a - t.d).abs < 1e-6
|
|
166
|
+
0x0008 # WE_HAVE_A_SCALE
|
|
167
|
+
elsif t.b.zero? && t.c.zero?
|
|
168
|
+
0x0040 # WE_HAVE_AN_X_AND_Y_SCALE
|
|
169
|
+
else
|
|
170
|
+
0x0080 # WE_HAVE_A_TWO_BY_TWO
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
flags |= 0x0020 if idx < components.size - 1 # MORE_COMPONENTS
|
|
175
|
+
|
|
176
|
+
body << [flags, gid].pack("n*")
|
|
177
|
+
|
|
178
|
+
body << if (flags & 0x0001).zero?
|
|
179
|
+
[dx, dy].pack("cc")
|
|
180
|
+
else
|
|
181
|
+
[dx, dy].pack("s>s>")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if flags & 0x0008 != 0
|
|
185
|
+
body << [to_f2dot14(t.a)].pack("s>")
|
|
186
|
+
elsif flags & 0x0040 != 0
|
|
187
|
+
body << [to_f2dot14(t.a), to_f2dot14(t.d)].pack("s>s>")
|
|
188
|
+
elsif flags & 0x0080 != 0
|
|
189
|
+
body << [to_f2dot14(t.a), to_f2dot14(t.b),
|
|
190
|
+
to_f2dot14(t.c), to_f2dot14(t.d)].pack("s>4")
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
header + body
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Encode a float as F2Dot14 (2 integer + 14 fractional bits).
|
|
198
|
+
def self.to_f2dot14(value)
|
|
199
|
+
(value * 16_384).round
|
|
141
200
|
end
|
|
142
201
|
private_class_method :encode_glyph, :encode_simple, :encode_points,
|
|
143
|
-
:encode_composite
|
|
202
|
+
:encode_composite, :to_f2dot14
|
|
144
203
|
end
|
|
145
204
|
end
|
|
146
205
|
end
|
|
@@ -40,7 +40,9 @@ module Fontisan
|
|
|
40
40
|
# ---------- pair collection ----------
|
|
41
41
|
|
|
42
42
|
# Collect kerning pairs from the UFO model. Each pair is
|
|
43
|
-
# (gid1, gid2, x_advance_delta).
|
|
43
|
+
# (gid1, gid2, x_advance_delta). Resolves class-based kerning
|
|
44
|
+
# keys (e.g. "@MMK_L_A @MMK_R_T") by expanding group members
|
|
45
|
+
# into individual glyph pairs via font.groups.
|
|
44
46
|
# @return [Array<[Integer, Integer, Integer]>]
|
|
45
47
|
def self.collect_kerning_pairs(font, glyphs)
|
|
46
48
|
name_to_gid = {}
|
|
@@ -48,21 +50,40 @@ module Fontisan
|
|
|
48
50
|
|
|
49
51
|
pairs = []
|
|
50
52
|
font.kerning.each_pair do |key, value|
|
|
51
|
-
# UFO kerning key is "glyph1 glyph2" or a class name.
|
|
52
|
-
# We only handle individual glyph pairs (not classes).
|
|
53
53
|
names = key.split
|
|
54
54
|
next unless names.size == 2
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
next unless gid1 && gid2
|
|
56
|
+
left_names = resolve_kerning_side(names[0], font)
|
|
57
|
+
right_names = resolve_kerning_side(names[1], font)
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
left_names.each do |ln|
|
|
60
|
+
right_names.each do |rn|
|
|
61
|
+
gid1 = name_to_gid[ln]
|
|
62
|
+
gid2 = name_to_gid[rn]
|
|
63
|
+
next unless gid1 && gid2
|
|
64
|
+
|
|
65
|
+
pairs << [gid1, gid2, value.to_i]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
pairs.sort_by { |a| [a[0], a[1]] }
|
|
64
71
|
end
|
|
65
72
|
|
|
73
|
+
# Resolve a kerning key side to individual glyph names. If
|
|
74
|
+
# +name+ is a group reference (starts with "@"), returns all
|
|
75
|
+
# group members. Otherwise returns +[name]+ for an individual
|
|
76
|
+
# glyph.
|
|
77
|
+
# @param name [String] glyph name or group reference
|
|
78
|
+
# @param font [Fontisan::Ufo::Font]
|
|
79
|
+
# @return [Array<String>] glyph names
|
|
80
|
+
def self.resolve_kerning_side(name, font)
|
|
81
|
+
return [name] unless name.start_with?("@")
|
|
82
|
+
|
|
83
|
+
members = font.groups.glyphs(name)
|
|
84
|
+
members.empty? ? [] : members
|
|
85
|
+
end
|
|
86
|
+
|
|
66
87
|
# ---------- GPOS binary assembly ----------
|
|
67
88
|
|
|
68
89
|
def self.build_gpos_table(pairs)
|
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.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -485,6 +485,7 @@ files:
|
|
|
485
485
|
- lib/fontisan/subset/table_strategy.rb
|
|
486
486
|
- lib/fontisan/subset/table_strategy/cbdt.rb
|
|
487
487
|
- lib/fontisan/subset/table_strategy/cblc.rb
|
|
488
|
+
- lib/fontisan/subset/table_strategy/cff.rb
|
|
488
489
|
- lib/fontisan/subset/table_strategy/cmap.rb
|
|
489
490
|
- lib/fontisan/subset/table_strategy/color_bitmap_placement.rb
|
|
490
491
|
- lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb
|