fontisan 0.4.21 → 0.4.23
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_todo.yml +10 -5
- data/lib/fontisan/config/subset_profiles.yml +2 -0
- data/lib/fontisan/stitcher/cbdt_propagator.rb +173 -0
- data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +109 -0
- data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
- data/lib/fontisan/stitcher.rb +14 -159
- data/lib/fontisan/subset/shared_state.rb +42 -0
- data/lib/fontisan/subset/subset_context.rb +20 -0
- data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
- data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
- data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
- data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +145 -0
- data/lib/fontisan/subset/table_strategy/head.rb +42 -0
- data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
- data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
- data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
- data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
- data/lib/fontisan/subset/table_strategy/name.rb +19 -0
- data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
- data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
- data/lib/fontisan/subset/table_strategy/post.rb +37 -0
- data/lib/fontisan/subset/table_strategy.rb +75 -0
- data/lib/fontisan/subset/table_subsetter.rb +49 -616
- data/lib/fontisan/subset.rb +3 -0
- data/lib/fontisan/tables/cbdt.rb +66 -121
- data/lib/fontisan/tables/cblc.rb +110 -231
- data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
- data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
- data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
- data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
- data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
- data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
- data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
- data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
- data/lib/fontisan/tables.rb +11 -0
- data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
- data/lib/fontisan/ufo/layer.rb +42 -2
- data/lib/fontisan/ufo.rb +2 -1
- data/lib/fontisan/version.rb +1 -1
- metadata +37 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7e00355898ff37e38b4948f9a607bf95b96b73df17225c81ebe0b6f6b757b1d
|
|
4
|
+
data.tar.gz: 9009d6fb96c8168a098236d7b80355749d6c65463376119695ab2a65c20de01d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c82bc6f0d6a871e84d85e4ac6bd51260a39f2b1d373b3055801354c89d99eda1ce3f78ba62ce640e5193eededeba9c8f5101146378124b5d5ac8e5c52bb60c0
|
|
7
|
+
data.tar.gz: 3e957b040dd2660ce7b8ddea3d4592cfef37ad8d434dd5e43d1be4417459992150fcd414f7547ead7f89a9ad8fd4ea8e01a847eb57ef352a70335e691cb386d6
|
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-08
|
|
3
|
+
# on 2026-07-08 08:05:45 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
|
|
@@ -12,7 +12,7 @@ Gemspec/RequiredRubyVersion:
|
|
|
12
12
|
- 'docs/node_modules/speakingurl/speakingurl-rails.gemspec'
|
|
13
13
|
- 'fontisan.gemspec'
|
|
14
14
|
|
|
15
|
-
# Offense count:
|
|
15
|
+
# Offense count: 2116
|
|
16
16
|
# This cop supports safe autocorrection (--autocorrect).
|
|
17
17
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
18
18
|
# URISchemes: http, https
|
|
@@ -88,6 +88,11 @@ Lint/UnusedMethodArgument:
|
|
|
88
88
|
- 'lib/fontisan/tables/cff2/private_dict_blend_handler.rb'
|
|
89
89
|
- 'lib/fontisan/variation/table_accessor.rb'
|
|
90
90
|
- 'lib/fontisan/woff_font.rb'
|
|
91
|
+
# Subset strategies share a uniform (context:, tag:, table:) interface
|
|
92
|
+
# so the dispatcher can call every registered class the same way. Not
|
|
93
|
+
# every strategy reads every arg; the interface contract justifies the
|
|
94
|
+
# unused warnings here.
|
|
95
|
+
- 'lib/fontisan/subset/table_strategy/**/*'
|
|
91
96
|
|
|
92
97
|
# Offense count: 5
|
|
93
98
|
Lint/UselessConstantScoping:
|
|
@@ -95,7 +100,7 @@ Lint/UselessConstantScoping:
|
|
|
95
100
|
- 'lib/fontisan/conversion_options.rb'
|
|
96
101
|
- 'lib/fontisan/type1/charstrings.rb'
|
|
97
102
|
|
|
98
|
-
# Offense count:
|
|
103
|
+
# Offense count: 705
|
|
99
104
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
100
105
|
Metrics/AbcSize:
|
|
101
106
|
Enabled: false
|
|
@@ -221,7 +226,7 @@ RSpec/DescribeMethod:
|
|
|
221
226
|
- 'spec/fontisan/type1/seac_expander_spec.rb'
|
|
222
227
|
- 'spec/fontisan/type1_real_fonts_spec.rb'
|
|
223
228
|
|
|
224
|
-
# Offense count:
|
|
229
|
+
# Offense count: 1673
|
|
225
230
|
# Configuration parameters: CountAsOne.
|
|
226
231
|
RSpec/ExampleLength:
|
|
227
232
|
Max: 66
|
|
@@ -298,7 +303,7 @@ RSpec/MultipleDescribes:
|
|
|
298
303
|
- 'spec/fontisan/utils/thread_pool_spec.rb'
|
|
299
304
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
300
305
|
|
|
301
|
-
# Offense count:
|
|
306
|
+
# Offense count: 2105
|
|
302
307
|
RSpec/MultipleExpectations:
|
|
303
308
|
Max: 19
|
|
304
309
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Owns every CBDT-specific concern in the stitch pipeline.
|
|
6
|
+
#
|
|
7
|
+
# The Stitcher has two distinct CBDT responsibilities that benefit
|
|
8
|
+
# from being in one place:
|
|
9
|
+
#
|
|
10
|
+
# 1. Detection: exactly one CBDT source per stitch is supported.
|
|
11
|
+
# Multiple CBDT sources raise MultipleCbdtSourcesError.
|
|
12
|
+
#
|
|
13
|
+
# 2. Action: when a CBDT source exists, the stitcher
|
|
14
|
+
# - injects empty CBDT placeholder glyphs into the target UFO
|
|
15
|
+
# (the bitmap data lives in CBDT/CBLC tables, not in outlines),
|
|
16
|
+
# - propagates the raw CBDT/CBLC tables into the compiled font
|
|
17
|
+
# file so the bitmaps survive the round-trip.
|
|
18
|
+
#
|
|
19
|
+
# Both responsibilities are model-driven from the CBDT Source itself
|
|
20
|
+
# (`Source#bitmap_mode == :cbdt`), so the propagator is stateless
|
|
21
|
+
# given a list of sources. Tests can construct one with synthetic
|
|
22
|
+
# sources directly.
|
|
23
|
+
class CbdtPropagator
|
|
24
|
+
# @param sources [Array<Stitcher::Source>] all sources the stitcher
|
|
25
|
+
# knows about, in label order (the propagator does not care
|
|
26
|
+
# about labels).
|
|
27
|
+
def initialize(sources)
|
|
28
|
+
@sources = sources
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Stitcher::Source, nil] the single CBDT source, or nil
|
|
32
|
+
# if there is none.
|
|
33
|
+
# @raise [MultipleCbdtSourcesError] if more than one source is
|
|
34
|
+
# marked :cbdt.
|
|
35
|
+
def cbdt_source
|
|
36
|
+
cbdts = @sources.select { |s| s.bitmap_mode == :cbdt }
|
|
37
|
+
return nil if cbdts.empty?
|
|
38
|
+
|
|
39
|
+
if cbdts.size > 1
|
|
40
|
+
raise MultipleCbdtSourcesError,
|
|
41
|
+
"multiple CBDT sources not supported (found #{cbdts.size})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
cbdts.first
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Safe variant: returns nil instead of raising when there are
|
|
48
|
+
# multiple CBDT sources. Used inside the per-subfont glyph-copy
|
|
49
|
+
# pass where raising mid-compile would leave partial state.
|
|
50
|
+
#
|
|
51
|
+
# @return [Stitcher::Source, nil]
|
|
52
|
+
def safe_cbdt_source
|
|
53
|
+
cbdt_source
|
|
54
|
+
rescue MultipleCbdtSourcesError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Inject empty CBDT placeholder glyphs from `source` into the
|
|
59
|
+
# target UFO. One placeholder per gid in the source's maxp/cmap.
|
|
60
|
+
# Outline data is intentionally absent — the bitmap lives in
|
|
61
|
+
# the CBDT table that #propagate_tables_into will copy later.
|
|
62
|
+
#
|
|
63
|
+
# == Why placeholders are name-deconflicted
|
|
64
|
+
#
|
|
65
|
+
# Placeholders are named after the CBDT source's gid ("gid{N}").
|
|
66
|
+
# Outline donors compiled earlier into the same target use the
|
|
67
|
+
# SAME naming scheme (see Source#extract_truetype_glyph), so a
|
|
68
|
+
# CBDT placeholder at "gid110" would collide with an outline
|
|
69
|
+
# glyph at "gid110" from a different donor. Layer#add raises on
|
|
70
|
+
# conflict (it never silently overwrites — that path previously
|
|
71
|
+
# erased real outline glyphs and dropped their cmap entries,
|
|
72
|
+
# causing the CJK Ext G loss in Essenfont-Regular.ttc). We
|
|
73
|
+
# allocate a unique name via UniqueGlyphName so each placeholder
|
|
74
|
+
# lands at its own GID slot without touching the outlines.
|
|
75
|
+
#
|
|
76
|
+
# Glyphs are NOT registered with the deduplicator: each CBDT
|
|
77
|
+
# glyph is unique to its source, and deduplication would
|
|
78
|
+
# incorrectly collapse distinct bitmaps.
|
|
79
|
+
#
|
|
80
|
+
# @param source [Stitcher::Source] the CBDT source
|
|
81
|
+
# @param target [Ufo::Font] target UFO font to receive placeholders
|
|
82
|
+
def add_placeholder_glyphs(source, target)
|
|
83
|
+
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
84
|
+
if ufo
|
|
85
|
+
ufo.glyphs.each_value do |g|
|
|
86
|
+
name = UniqueGlyphName.in(target, g.name)
|
|
87
|
+
target.layers.default_layer.add(GlyphCloner.clone(g, name: name))
|
|
88
|
+
end
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
maxp = source.font.table("maxp")
|
|
93
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
94
|
+
cmap = source.font.table("cmap")
|
|
95
|
+
mappings = cmap&.unicode_mappings || {}
|
|
96
|
+
|
|
97
|
+
gid_cps = Hash.new { |h, k| h[k] = [] }
|
|
98
|
+
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
99
|
+
|
|
100
|
+
num_glyphs.times do |gid|
|
|
101
|
+
base_name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
102
|
+
name = UniqueGlyphName.in(target, base_name)
|
|
103
|
+
glyph = Ufo::Glyph.new(name: name)
|
|
104
|
+
glyph.width = 0
|
|
105
|
+
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
106
|
+
target.layers.default_layer.add(glyph)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Propagate the raw CBDT/CBLC tables from the CBDT source into
|
|
111
|
+
# the compiled font at `path`, rewriting the file in place.
|
|
112
|
+
#
|
|
113
|
+
# Reads every table from the compiled font as raw bytes (bypassing
|
|
114
|
+
# 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.
|
|
119
|
+
#
|
|
120
|
+
# == Limitation: GID stability
|
|
121
|
+
#
|
|
122
|
+
# The propagation copies CBDT/CBLC bytes verbatim. CBLC indexes
|
|
123
|
+
# glyphs by SOURCE GID — the GIDs of the CBDT donor. The compiled
|
|
124
|
+
# font's GIDs may differ because:
|
|
125
|
+
#
|
|
126
|
+
# 1. +add_placeholder_glyphs+ renames CBDT placeholders via
|
|
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.
|
|
144
|
+
#
|
|
145
|
+
# @param source [Stitcher::Source, nil] the CBDT source
|
|
146
|
+
# @param path [String] compiled font file to rewrite
|
|
147
|
+
def propagate_tables_into(source, path)
|
|
148
|
+
return unless source
|
|
149
|
+
|
|
150
|
+
compiled = FontLoader.load(path)
|
|
151
|
+
|
|
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
|
+
tables = {}
|
|
158
|
+
compiled.table_names.each do |tag|
|
|
159
|
+
raw = compiled.table_data[tag]
|
|
160
|
+
tables[tag] = raw if raw
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
cbdt_bytes = source.raw_table_bytes("CBDT")
|
|
164
|
+
cblc_bytes = source.raw_table_bytes("CBLC")
|
|
165
|
+
tables["CBDT"] = cbdt_bytes if cbdt_bytes
|
|
166
|
+
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
167
|
+
|
|
168
|
+
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
169
|
+
FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Deep-clone helper for UFO glyphs. Stateless — every call returns
|
|
6
|
+
# a fresh +Ufo::Glyph+ with copied contours, components, anchors,
|
|
7
|
+
# and guidelines plus the original's width/height.
|
|
8
|
+
#
|
|
9
|
+
# Extracted as a collaborator so +GlyphCopier+ and +CbdtPropagator+
|
|
10
|
+
# share one implementation of the clone algorithm. Both previously
|
|
11
|
+
# defined identical +clone_glyph+ / +clone_contour+ private methods
|
|
12
|
+
# — a DRY violation that risked drifting apart as the glyph model
|
|
13
|
+
# gained fields.
|
|
14
|
+
module GlyphCloner
|
|
15
|
+
# @param original [Ufo::Glyph] glyph to copy
|
|
16
|
+
# @param name [String, nil] name for the copy; defaults to the
|
|
17
|
+
# original's name (used by callers that have already allocated
|
|
18
|
+
# a unique name via +UniqueGlyphName+).
|
|
19
|
+
# @return [Ufo::Glyph] a fresh glyph with the original's geometry
|
|
20
|
+
# and metrics, sharing no mutable state with it.
|
|
21
|
+
def self.clone(original, name: nil)
|
|
22
|
+
copy = Ufo::Glyph.new(name: name || original.name)
|
|
23
|
+
copy.width = original.width
|
|
24
|
+
copy.height = original.height
|
|
25
|
+
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
26
|
+
original.components.each { |c| copy.add_component(c) }
|
|
27
|
+
original.anchors.each { |a| copy.add_anchor(a) }
|
|
28
|
+
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
29
|
+
copy
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.clone_contour(original)
|
|
33
|
+
points = original.points.map do |p|
|
|
34
|
+
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
35
|
+
end
|
|
36
|
+
Ufo::Contour.new(points)
|
|
37
|
+
end
|
|
38
|
+
private_class_method :clone_contour
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Owns the mechanical concerns of copying glyphs from donor sources
|
|
6
|
+
# into a target UFO font.
|
|
7
|
+
#
|
|
8
|
+
# The Stitcher's per-subfont build needs to:
|
|
9
|
+
# - inject a `.notdef` glyph at GID 0
|
|
10
|
+
# - copy outline glyphs from donor sources, optionally skipping
|
|
11
|
+
# donors the caller names (e.g. the CBDT source, whose glyphs
|
|
12
|
+
# take a different code path)
|
|
13
|
+
# - register each copied glyph with the deduplicator so visually
|
|
14
|
+
# identical glyphs from different donors share a gid
|
|
15
|
+
#
|
|
16
|
+
# GlyphCopier centralizes that logic in one stateless-per-call
|
|
17
|
+
# collaborator. The Stitcher constructs one with a deduplicator and
|
|
18
|
+
# calls its methods in the order the cmap contract requires
|
|
19
|
+
# (see Fontisan::Ufo::Compile::Cmap for first-wins semantics).
|
|
20
|
+
class GlyphCopier
|
|
21
|
+
# @param deduplicator [Stitcher::Deduplicator, nil] when non-nil,
|
|
22
|
+
# visually identical glyphs share a gid in the target. Pass nil
|
|
23
|
+
# to disable deduplication.
|
|
24
|
+
def initialize(deduplicator)
|
|
25
|
+
@deduplicator = deduplicator
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Inject a `.notdef` glyph at GID 0 of `target`.
|
|
29
|
+
#
|
|
30
|
+
# If a binding exists with `donor_gid == 0`, copy the donor's
|
|
31
|
+
# .notdef; otherwise synthesize an empty one. The injected glyph
|
|
32
|
+
# is registered with the deduplicator so subsequent .notdef
|
|
33
|
+
# references deduplicate against it.
|
|
34
|
+
#
|
|
35
|
+
# @param bindings [Array<Hash>] stitch bindings to search
|
|
36
|
+
# @param target [Ufo::Font] target UFO
|
|
37
|
+
def inject_notdef(bindings, target)
|
|
38
|
+
notdef_binding = bindings.find { |b| b[:donor_gid].zero? }
|
|
39
|
+
if notdef_binding
|
|
40
|
+
copy_glyph_into(target, name: ".notdef",
|
|
41
|
+
source: notdef_binding[:source],
|
|
42
|
+
donor_gid: 0)
|
|
43
|
+
else
|
|
44
|
+
target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
|
|
45
|
+
end
|
|
46
|
+
dedup_target = target.glyphs[".notdef"]
|
|
47
|
+
@deduplicator&.register(dedup_target, ".notdef") if dedup_target
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Copy outline glyphs from `bindings` into `target`, skipping any
|
|
51
|
+
# sources the caller names (e.g. the CBDT source).
|
|
52
|
+
#
|
|
53
|
+
# Bindings are sorted by `[codepoint, donor_gid]` for deterministic
|
|
54
|
+
# output. For each binding:
|
|
55
|
+
# - if deduplicator has seen a visually identical glyph that's
|
|
56
|
+
# already in the target, attach the codepoint to that glyph
|
|
57
|
+
# (extra unicode mapping);
|
|
58
|
+
# - otherwise copy the glyph under a unique name and register
|
|
59
|
+
# with the deduplicator.
|
|
60
|
+
#
|
|
61
|
+
# @param bindings [Array<Hash>] stitch bindings
|
|
62
|
+
# @param target [Ufo::Font] target UFO
|
|
63
|
+
# @param skip_sources [Array<Stitcher::Source>] sources whose
|
|
64
|
+
# bindings should be skipped (e.g. CBDT placeholder source)
|
|
65
|
+
def copy_outlines(bindings, target, skip_sources: [])
|
|
66
|
+
sorted_bindings(bindings).each do |binding|
|
|
67
|
+
next if binding[:donor_gid].zero?
|
|
68
|
+
next if skip_sources.any? { |s| s.equal?(binding[:source]) }
|
|
69
|
+
|
|
70
|
+
glyph = binding[:source].glyph_for_gid(binding[:donor_gid])
|
|
71
|
+
next unless glyph
|
|
72
|
+
|
|
73
|
+
canonical = @deduplicator&.find(glyph)
|
|
74
|
+
if canonical && target.glyphs.key?(canonical)
|
|
75
|
+
add_extra_unicode(target, canonical, binding[:codepoint])
|
|
76
|
+
else
|
|
77
|
+
name = UniqueGlyphName.in(target, glyph.name)
|
|
78
|
+
copy_glyph_into(target, name: name, source: binding[:source],
|
|
79
|
+
donor_gid: binding[:donor_gid],
|
|
80
|
+
codepoint: binding[:codepoint])
|
|
81
|
+
@deduplicator&.register(glyph, name)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def sorted_bindings(bindings)
|
|
89
|
+
bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def copy_glyph_into(target_font, name:, source:, donor_gid:, codepoint: nil)
|
|
93
|
+
original = source.glyph_for_gid(donor_gid)
|
|
94
|
+
return unless original
|
|
95
|
+
|
|
96
|
+
copy = GlyphCloner.clone(original, name: name)
|
|
97
|
+
copy.add_unicode(codepoint) if codepoint
|
|
98
|
+
target_font.layers.default_layer.add(copy)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def add_extra_unicode(target_font, glyph_name, codepoint)
|
|
102
|
+
return unless codepoint
|
|
103
|
+
|
|
104
|
+
glyph = target_font.glyph(glyph_name)
|
|
105
|
+
glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Stitcher
|
|
5
|
+
# Stateless glyph-name deconfliction for the Stitcher's target UFO.
|
|
6
|
+
#
|
|
7
|
+
# The Layer's contract (see +Ufo::Layer+) is that two glyphs cannot
|
|
8
|
+
# share a name — +#add+ raises on conflict. Most Stitcher call sites
|
|
9
|
+
# already know the name they want is free (e.g. they just synthesised
|
|
10
|
+
# it). The two that don't — GlyphCopier (outline donors may share
|
|
11
|
+
# post-table names) and CbdtPropagator (placeholders named "gid{N}"
|
|
12
|
+
# collide with outline glyphs sharing the same donor-gid scheme) —
|
|
13
|
+
# ask this helper to allocate a non-colliding name first.
|
|
14
|
+
#
|
|
15
|
+
# Centralising the rule here keeps the deconfliction algorithm in
|
|
16
|
+
# one place: the suffix-search loop, the separator, the order in
|
|
17
|
+
# which candidates are tried. Changing the rule (e.g. to a UUID
|
|
18
|
+
# scheme) is a one-file edit.
|
|
19
|
+
module UniqueGlyphName
|
|
20
|
+
# Separator between the base name and the disambiguating suffix.
|
|
21
|
+
# A literal period so the generated name ("gid110.1") still
|
|
22
|
+
# reads as a variant of the original.
|
|
23
|
+
SUFFIX_SEPARATOR = "."
|
|
24
|
+
|
|
25
|
+
# @param target [Ufo::Font, #glyphs] the target whose +#glyphs+
|
|
26
|
+
# hash is the namespace being deconflicted against. Accepts the
|
|
27
|
+
# UFO font directly so callers don't have to dig out the layer.
|
|
28
|
+
# @param base_name [String, #to_s] the requested name.
|
|
29
|
+
# @return [String] +base_name+ unchanged if no glyph with that
|
|
30
|
+
# name exists in +target.glyphs+; otherwise the first
|
|
31
|
+
# +"base.1"+, +"base.2"+, ... that does not exist.
|
|
32
|
+
def self.in(target, base_name)
|
|
33
|
+
base = base_name.to_s
|
|
34
|
+
return base unless target.glyphs.key?(base)
|
|
35
|
+
|
|
36
|
+
suffix = 1
|
|
37
|
+
loop do
|
|
38
|
+
candidate = "#{base}#{SUFFIX_SEPARATOR}#{suffix}"
|
|
39
|
+
return candidate unless target.glyphs.key?(candidate)
|
|
40
|
+
|
|
41
|
+
suffix += 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -34,6 +34,10 @@ module Fontisan
|
|
|
34
34
|
autoload :SubfontStats, "fontisan/stitcher/collection_result"
|
|
35
35
|
autoload :FormatMetadata, "fontisan/stitcher/format_metadata"
|
|
36
36
|
autoload :PartitionStrategy, "fontisan/stitcher/partition_strategy"
|
|
37
|
+
autoload :CbdtPropagator, "fontisan/stitcher/cbdt_propagator"
|
|
38
|
+
autoload :GlyphCloner, "fontisan/stitcher/glyph_cloner"
|
|
39
|
+
autoload :GlyphCopier, "fontisan/stitcher/glyph_copier"
|
|
40
|
+
autoload :UniqueGlyphName, "fontisan/stitcher/unique_glyph_name"
|
|
37
41
|
|
|
38
42
|
# Internal: pairs a compiled loaded font with its stats so
|
|
39
43
|
# +write_collection+ can build the collection and the result from a
|
|
@@ -98,7 +102,7 @@ module Fontisan
|
|
|
98
102
|
metadata = FormatMetadata.resolve(format)
|
|
99
103
|
metadata.compiler_class.new(target).compile(output_path: path)
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source, path)
|
|
102
106
|
path
|
|
103
107
|
end
|
|
104
108
|
|
|
@@ -129,25 +133,6 @@ module Fontisan
|
|
|
129
133
|
end
|
|
130
134
|
end
|
|
131
135
|
|
|
132
|
-
def cbdt_source
|
|
133
|
-
cbdts = @sources.values.select { |s| s.bitmap_mode == :cbdt }
|
|
134
|
-
if cbdts.size > 1
|
|
135
|
-
raise MultipleCbdtSourcesError,
|
|
136
|
-
"multiple CBDT sources not supported (found #{cbdts.size})"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
cbdts.first
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Safe variant of #cbdt_source: returns nil instead of raising when
|
|
143
|
-
# the project has multiple CBDT sources. Used inside the per-subfont
|
|
144
|
-
# glyph-copy pass where raising mid-compile would leave partial state.
|
|
145
|
-
def safe_cbdt_source
|
|
146
|
-
cbdt_source
|
|
147
|
-
rescue MultipleCbdtSourcesError
|
|
148
|
-
nil
|
|
149
|
-
end
|
|
150
|
-
|
|
151
136
|
def build_target_for(subfont_name)
|
|
152
137
|
bindings = @subfonts[subfont_name] || []
|
|
153
138
|
target = Ufo::Font.new
|
|
@@ -165,7 +150,7 @@ module Fontisan
|
|
|
165
150
|
Dir.mktmpdir do |dir|
|
|
166
151
|
sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
|
|
167
152
|
metadata.compiler_class.new(target).compile(output_path: sub_path)
|
|
168
|
-
|
|
153
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source, sub_path)
|
|
169
154
|
|
|
170
155
|
loaded = Fontisan::FontLoader.load(sub_path)
|
|
171
156
|
stats = SubfontStats.new(
|
|
@@ -178,152 +163,22 @@ module Fontisan
|
|
|
178
163
|
end
|
|
179
164
|
|
|
180
165
|
def assign_gids_and_copy_glyphs(bindings, target, deduplicator)
|
|
181
|
-
cbdt = safe_cbdt_source
|
|
166
|
+
cbdt = cbdt_propagator.safe_cbdt_source
|
|
167
|
+
copier = GlyphCopier.new(deduplicator)
|
|
182
168
|
|
|
183
169
|
# Glyph ordering matters: Cmap.build uses first-wins semantics
|
|
184
170
|
# (Cmap.build docstring), so outline donors must be stitched
|
|
185
171
|
# before CBDT placeholders. Otherwise the empty CBDT placeholder
|
|
186
172
|
# would land at a lower GID and win the cp→gid mapping for any
|
|
187
173
|
# codepoint covered by both donors, hiding the real outline glyph.
|
|
188
|
-
# CBDT bitmap data still propagates via
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
add_all_cbdt_glyphs(cbdt, target) if cbdt
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def add_outline_glyphs(bindings, target, deduplicator, skip_sources:)
|
|
196
|
-
sorted_bindings(bindings).each do |binding|
|
|
197
|
-
next if binding[:donor_gid].zero?
|
|
198
|
-
next if skip_sources.any? { |s| s.equal?(binding[:source]) }
|
|
199
|
-
|
|
200
|
-
glyph = binding[:source].glyph_for_gid(binding[:donor_gid])
|
|
201
|
-
next unless glyph
|
|
202
|
-
|
|
203
|
-
canonical = deduplicator&.find(glyph)
|
|
204
|
-
if canonical && target.glyphs.key?(canonical)
|
|
205
|
-
add_extra_unicode(target, canonical, binding[:codepoint])
|
|
206
|
-
else
|
|
207
|
-
name = unique_target_name(target, glyph.name)
|
|
208
|
-
copy_glyph_into(target, name: name, source: binding[:source],
|
|
209
|
-
donor_gid: binding[:donor_gid],
|
|
210
|
-
codepoint: binding[:codepoint])
|
|
211
|
-
deduplicator&.register(glyph, name)
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def sorted_bindings(bindings)
|
|
217
|
-
bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def add_notdef_from(bindings, target, deduplicator)
|
|
221
|
-
notdef_binding = bindings.find { |b| b[:donor_gid].zero? }
|
|
222
|
-
if notdef_binding
|
|
223
|
-
copy_glyph_into(target, name: ".notdef",
|
|
224
|
-
source: notdef_binding[:source],
|
|
225
|
-
donor_gid: 0)
|
|
226
|
-
else
|
|
227
|
-
target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
|
|
228
|
-
end
|
|
229
|
-
dedup_target = target.glyphs[".notdef"]
|
|
230
|
-
deduplicator&.register(dedup_target, ".notdef") if dedup_target
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def copy_glyph_into(target_font, name:, source:, donor_gid:, codepoint: nil)
|
|
234
|
-
original = source.glyph_for_gid(donor_gid)
|
|
235
|
-
return unless original
|
|
236
|
-
|
|
237
|
-
copy = clone_glyph(original, name: name)
|
|
238
|
-
copy.add_unicode(codepoint) if codepoint
|
|
239
|
-
target_font.layers.default_layer.add(copy)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def add_extra_unicode(target_font, glyph_name, codepoint)
|
|
243
|
-
return unless codepoint
|
|
244
|
-
|
|
245
|
-
glyph = target_font.glyph(glyph_name)
|
|
246
|
-
glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
|
|
174
|
+
# CBDT bitmap data still propagates via CbdtPropagator.
|
|
175
|
+
copier.inject_notdef(bindings, target)
|
|
176
|
+
copier.copy_outlines(bindings, target, skip_sources: cbdt ? [cbdt] : [])
|
|
177
|
+
cbdt_propagator.add_placeholder_glyphs(cbdt, target) if cbdt
|
|
247
178
|
end
|
|
248
179
|
|
|
249
|
-
def
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
suffix = 1
|
|
253
|
-
loop do
|
|
254
|
-
candidate = "#{base_name}.#{suffix}"
|
|
255
|
-
return candidate unless target_font.glyphs.key?(candidate)
|
|
256
|
-
|
|
257
|
-
suffix += 1
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def clone_glyph(original, name:)
|
|
262
|
-
copy = Ufo::Glyph.new(name: name)
|
|
263
|
-
copy.width = original.width
|
|
264
|
-
copy.height = original.height
|
|
265
|
-
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
266
|
-
original.components.each { |c| copy.add_component(c) }
|
|
267
|
-
original.anchors.each { |a| copy.add_anchor(a) }
|
|
268
|
-
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
269
|
-
copy
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def clone_contour(original)
|
|
273
|
-
points = original.points.map do |p|
|
|
274
|
-
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
275
|
-
end
|
|
276
|
-
Ufo::Contour.new(points)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def propagate_cbdt_tables(path)
|
|
280
|
-
source = cbdt_source
|
|
281
|
-
return unless source
|
|
282
|
-
|
|
283
|
-
compiled = Fontisan::FontLoader.load(path)
|
|
284
|
-
|
|
285
|
-
# Read every table as raw bytes straight from the file's table
|
|
286
|
-
# directory. We deliberately bypass #table (which parses via
|
|
287
|
-
# BinData) because some tables — notably CFF2 — don't yet have
|
|
288
|
-
# round-trippable BinData models; calling #table on them returns
|
|
289
|
-
# nil and would silently drop them from the rewritten font.
|
|
290
|
-
tables = {}
|
|
291
|
-
compiled.table_names.each do |tag|
|
|
292
|
-
raw = compiled.table_data[tag]
|
|
293
|
-
tables[tag] = raw if raw
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
cbdt_bytes = source.raw_table_bytes("CBDT")
|
|
297
|
-
cblc_bytes = source.raw_table_bytes("CBLC")
|
|
298
|
-
tables["CBDT"] = cbdt_bytes if cbdt_bytes
|
|
299
|
-
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
300
|
-
|
|
301
|
-
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
302
|
-
Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
def add_all_cbdt_glyphs(source, target)
|
|
306
|
-
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
307
|
-
if ufo
|
|
308
|
-
ufo.glyphs.each_value { |g| target.layers.default_layer.add(clone_glyph(g, name: g.name)) }
|
|
309
|
-
return
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
maxp = source.font.table("maxp")
|
|
313
|
-
num_glyphs = maxp&.num_glyphs || 0
|
|
314
|
-
cmap = source.font.table("cmap")
|
|
315
|
-
mappings = cmap&.unicode_mappings || {}
|
|
316
|
-
|
|
317
|
-
gid_cps = Hash.new { |h, k| h[k] = [] }
|
|
318
|
-
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
319
|
-
|
|
320
|
-
num_glyphs.times do |gid|
|
|
321
|
-
name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
322
|
-
glyph = Ufo::Glyph.new(name: name)
|
|
323
|
-
glyph.width = 0
|
|
324
|
-
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
325
|
-
target.layers.default_layer.add(glyph)
|
|
326
|
-
end
|
|
180
|
+
def cbdt_propagator
|
|
181
|
+
@cbdt_propagator ||= CbdtPropagator.new(@sources.values)
|
|
327
182
|
end
|
|
328
183
|
end
|
|
329
184
|
end
|