fontisan 0.4.20 → 0.4.22
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 +7 -7
- data/lib/fontisan/stitcher/cbdt_propagator.rb +151 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +139 -0
- data/lib/fontisan/stitcher.rb +20 -158
- data/lib/fontisan/ufo/compile/cmap.rb +8 -0
- data/lib/fontisan/ufo/compile/gvar.rb +1 -1
- 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: 208fa8e2c44bdea2e800ecb13639d50e06e6914406822f6e3c92152d21414fd7
|
|
4
|
+
data.tar.gz: cdce57d3a157d5a5549b45104f93de38263697989bae6a66572907bc54b58bb0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c87e9ec10e9cadd168dda76b38ef225c1e286bc63501b75268a7cd29fcdfbef3d86abacac211052d529d153aebf1f8db29aaab7445c51a5a83e3eb091f752ab4
|
|
7
|
+
data.tar.gz: 4632d882fe8ab4897ed32722ec17efb942819ded33e2005bf51c3032384466164d2c2c09a1a8f4388f1889b5c169f42b9dc14ae5cafce72ef5680c94d2333c02
|
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-
|
|
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
|
|
@@ -95,7 +95,7 @@ Lint/UselessConstantScoping:
|
|
|
95
95
|
- 'lib/fontisan/conversion_options.rb'
|
|
96
96
|
- 'lib/fontisan/type1/charstrings.rb'
|
|
97
97
|
|
|
98
|
-
# Offense count:
|
|
98
|
+
# Offense count: 705
|
|
99
99
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
100
100
|
Metrics/AbcSize:
|
|
101
101
|
Enabled: false
|
|
@@ -123,7 +123,7 @@ Metrics/CollectionLiteralLength:
|
|
|
123
123
|
Metrics/CyclomaticComplexity:
|
|
124
124
|
Enabled: false
|
|
125
125
|
|
|
126
|
-
# Offense count:
|
|
126
|
+
# Offense count: 978
|
|
127
127
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
128
128
|
Metrics/MethodLength:
|
|
129
129
|
Max: 135
|
|
@@ -221,7 +221,7 @@ RSpec/DescribeMethod:
|
|
|
221
221
|
- 'spec/fontisan/type1/seac_expander_spec.rb'
|
|
222
222
|
- 'spec/fontisan/type1_real_fonts_spec.rb'
|
|
223
223
|
|
|
224
|
-
# Offense count:
|
|
224
|
+
# Offense count: 1673
|
|
225
225
|
# Configuration parameters: CountAsOne.
|
|
226
226
|
RSpec/ExampleLength:
|
|
227
227
|
Max: 66
|
|
@@ -298,7 +298,7 @@ RSpec/MultipleDescribes:
|
|
|
298
298
|
- 'spec/fontisan/utils/thread_pool_spec.rb'
|
|
299
299
|
- 'spec/fontisan/variation/cache_spec.rb'
|
|
300
300
|
|
|
301
|
-
# Offense count:
|
|
301
|
+
# Offense count: 2105
|
|
302
302
|
RSpec/MultipleExpectations:
|
|
303
303
|
Max: 19
|
|
304
304
|
|
|
@@ -330,7 +330,7 @@ RSpec/RepeatedExample:
|
|
|
330
330
|
- 'spec/fontisan/metrics_calculator_spec.rb'
|
|
331
331
|
- 'spec/fontisan/tables/os2_spec.rb'
|
|
332
332
|
|
|
333
|
-
# Offense count:
|
|
333
|
+
# Offense count: 48
|
|
334
334
|
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
|
335
335
|
# SupportedInflectors: default, active_support
|
|
336
336
|
RSpec/SpecFilePathFormat:
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
# Glyphs are NOT registered with the deduplicator: each CBDT
|
|
64
|
+
# glyph is unique to its source, and deduplication would
|
|
65
|
+
# incorrectly collapse distinct bitmaps.
|
|
66
|
+
#
|
|
67
|
+
# @param source [Stitcher::Source] the CBDT source
|
|
68
|
+
# @param target [Ufo::Font] target UFO font to receive placeholders
|
|
69
|
+
def add_placeholder_glyphs(source, target)
|
|
70
|
+
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
71
|
+
if ufo
|
|
72
|
+
ufo.glyphs.each_value { |g| target.layers.default_layer.add(clone_glyph(g, name: g.name)) }
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
maxp = source.font.table("maxp")
|
|
77
|
+
num_glyphs = maxp&.num_glyphs || 0
|
|
78
|
+
cmap = source.font.table("cmap")
|
|
79
|
+
mappings = cmap&.unicode_mappings || {}
|
|
80
|
+
|
|
81
|
+
gid_cps = Hash.new { |h, k| h[k] = [] }
|
|
82
|
+
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
83
|
+
|
|
84
|
+
num_glyphs.times do |gid|
|
|
85
|
+
name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
86
|
+
glyph = Ufo::Glyph.new(name: name)
|
|
87
|
+
glyph.width = 0
|
|
88
|
+
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
89
|
+
target.layers.default_layer.add(glyph)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Propagate the raw CBDT/CBLC tables from the CBDT source into
|
|
94
|
+
# the compiled font at `path`, rewriting the file in place.
|
|
95
|
+
#
|
|
96
|
+
# Reads every table from the compiled font as raw bytes (bypassing
|
|
97
|
+
# BinData #table because some tables — notably CFF2 — don't yet
|
|
98
|
+
# have round-trippable BinData models), splices in CBDT/CBLC,
|
|
99
|
+
# and rewrites the file.
|
|
100
|
+
#
|
|
101
|
+
# No-op when called with a non-CBDT source or nil.
|
|
102
|
+
#
|
|
103
|
+
# @param source [Stitcher::Source, nil] the CBDT source
|
|
104
|
+
# @param path [String] compiled font file to rewrite
|
|
105
|
+
def propagate_tables_into(source, path)
|
|
106
|
+
return unless source
|
|
107
|
+
|
|
108
|
+
compiled = FontLoader.load(path)
|
|
109
|
+
|
|
110
|
+
# Read every table as raw bytes straight from the file's table
|
|
111
|
+
# directory. We deliberately bypass #table (which parses via
|
|
112
|
+
# BinData) because some tables — notably CFF2 — don't yet have
|
|
113
|
+
# round-trippable BinData models; calling #table on them returns
|
|
114
|
+
# nil and would silently drop them from the rewritten font.
|
|
115
|
+
tables = {}
|
|
116
|
+
compiled.table_names.each do |tag|
|
|
117
|
+
raw = compiled.table_data[tag]
|
|
118
|
+
tables[tag] = raw if raw
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
cbdt_bytes = source.raw_table_bytes("CBDT")
|
|
122
|
+
cblc_bytes = source.raw_table_bytes("CBLC")
|
|
123
|
+
tables["CBDT"] = cbdt_bytes if cbdt_bytes
|
|
124
|
+
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
125
|
+
|
|
126
|
+
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
127
|
+
FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def clone_glyph(original, name:)
|
|
133
|
+
copy = Ufo::Glyph.new(name: name)
|
|
134
|
+
copy.width = original.width
|
|
135
|
+
copy.height = original.height
|
|
136
|
+
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
137
|
+
original.components.each { |c| copy.add_component(c) }
|
|
138
|
+
original.anchors.each { |a| copy.add_anchor(a) }
|
|
139
|
+
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
140
|
+
copy
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def clone_contour(original)
|
|
144
|
+
points = original.points.map do |p|
|
|
145
|
+
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
146
|
+
end
|
|
147
|
+
Ufo::Contour.new(points)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string: 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 = unique_target_name(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 = clone_glyph(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
|
+
|
|
108
|
+
def unique_target_name(target_font, base_name)
|
|
109
|
+
return base_name unless target_font.glyphs.key?(base_name)
|
|
110
|
+
|
|
111
|
+
suffix = 1
|
|
112
|
+
loop do
|
|
113
|
+
candidate = "#{base_name}.#{suffix}"
|
|
114
|
+
return candidate unless target_font.glyphs.key?(candidate)
|
|
115
|
+
|
|
116
|
+
suffix += 1
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def clone_glyph(original, name:)
|
|
121
|
+
copy = Ufo::Glyph.new(name: name)
|
|
122
|
+
copy.width = original.width
|
|
123
|
+
copy.height = original.height
|
|
124
|
+
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
125
|
+
original.components.each { |c| copy.add_component(c) }
|
|
126
|
+
original.anchors.each { |a| copy.add_anchor(a) }
|
|
127
|
+
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
128
|
+
copy
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def clone_contour(original)
|
|
132
|
+
points = original.points.map do |p|
|
|
133
|
+
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
134
|
+
end
|
|
135
|
+
Ufo::Contour.new(points)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/fontisan/stitcher.rb
CHANGED
|
@@ -34,6 +34,8 @@ 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 :GlyphCopier, "fontisan/stitcher/glyph_copier"
|
|
37
39
|
|
|
38
40
|
# Internal: pairs a compiled loaded font with its stats so
|
|
39
41
|
# +write_collection+ can build the collection and the result from a
|
|
@@ -98,7 +100,7 @@ module Fontisan
|
|
|
98
100
|
metadata = FormatMetadata.resolve(format)
|
|
99
101
|
metadata.compiler_class.new(target).compile(output_path: path)
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source, path)
|
|
102
104
|
path
|
|
103
105
|
end
|
|
104
106
|
|
|
@@ -129,16 +131,6 @@ module Fontisan
|
|
|
129
131
|
end
|
|
130
132
|
end
|
|
131
133
|
|
|
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
134
|
def build_target_for(subfont_name)
|
|
143
135
|
bindings = @subfonts[subfont_name] || []
|
|
144
136
|
target = Ufo::Font.new
|
|
@@ -156,7 +148,7 @@ module Fontisan
|
|
|
156
148
|
Dir.mktmpdir do |dir|
|
|
157
149
|
sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
|
|
158
150
|
metadata.compiler_class.new(target).compile(output_path: sub_path)
|
|
159
|
-
|
|
151
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source, sub_path)
|
|
160
152
|
|
|
161
153
|
loaded = Fontisan::FontLoader.load(sub_path)
|
|
162
154
|
stats = SubfontStats.new(
|
|
@@ -169,152 +161,22 @@ module Fontisan
|
|
|
169
161
|
end
|
|
170
162
|
|
|
171
163
|
def assign_gids_and_copy_glyphs(bindings, target, deduplicator)
|
|
172
|
-
cbdt = safe_cbdt_source
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if canonical && target.glyphs.key?(canonical)
|
|
189
|
-
add_extra_unicode(target, canonical, binding[:codepoint])
|
|
190
|
-
else
|
|
191
|
-
name = unique_target_name(target, glyph.name)
|
|
192
|
-
copy_glyph_into(target, name: name, source: binding[:source],
|
|
193
|
-
donor_gid: binding[:donor_gid],
|
|
194
|
-
codepoint: binding[:codepoint])
|
|
195
|
-
deduplicator&.register(glyph, name)
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def safe_cbdt_source
|
|
201
|
-
cbdts = @sources.values.select { |s| s.bitmap_mode == :cbdt }
|
|
202
|
-
cbdts.size == 1 ? cbdts.first : nil
|
|
203
|
-
rescue MultipleCbdtSourcesError
|
|
204
|
-
nil
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def sorted_bindings(bindings)
|
|
208
|
-
bindings.sort_by { |b| [b[:codepoint] || Float::INFINITY, b[:donor_gid]] }
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def add_notdef_from(bindings, target, deduplicator)
|
|
212
|
-
notdef_binding = bindings.find { |b| b[:donor_gid].zero? }
|
|
213
|
-
if notdef_binding
|
|
214
|
-
copy_glyph_into(target, name: ".notdef",
|
|
215
|
-
source: notdef_binding[:source],
|
|
216
|
-
donor_gid: 0)
|
|
217
|
-
else
|
|
218
|
-
target.layers.default_layer.add(Ufo::Glyph.new(name: ".notdef"))
|
|
219
|
-
end
|
|
220
|
-
dedup_target = target.glyphs[".notdef"]
|
|
221
|
-
deduplicator&.register(dedup_target, ".notdef") if dedup_target
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def copy_glyph_into(target_font, name:, source:, donor_gid:, codepoint: nil)
|
|
225
|
-
original = source.glyph_for_gid(donor_gid)
|
|
226
|
-
return unless original
|
|
227
|
-
|
|
228
|
-
copy = clone_glyph(original, name: name)
|
|
229
|
-
copy.add_unicode(codepoint) if codepoint
|
|
230
|
-
target_font.layers.default_layer.add(copy)
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def add_extra_unicode(target_font, glyph_name, codepoint)
|
|
234
|
-
return unless codepoint
|
|
235
|
-
|
|
236
|
-
glyph = target_font.glyph(glyph_name)
|
|
237
|
-
glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def unique_target_name(target_font, base_name)
|
|
241
|
-
return base_name unless target_font.glyphs.key?(base_name)
|
|
242
|
-
|
|
243
|
-
suffix = 1
|
|
244
|
-
loop do
|
|
245
|
-
candidate = "#{base_name}.#{suffix}"
|
|
246
|
-
return candidate unless target_font.glyphs.key?(candidate)
|
|
247
|
-
|
|
248
|
-
suffix += 1
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def clone_glyph(original, name:)
|
|
253
|
-
copy = Ufo::Glyph.new(name: name)
|
|
254
|
-
copy.width = original.width
|
|
255
|
-
copy.height = original.height
|
|
256
|
-
original.contours.each { |c| copy.add_contour(clone_contour(c)) }
|
|
257
|
-
original.components.each { |c| copy.add_component(c) }
|
|
258
|
-
original.anchors.each { |a| copy.add_anchor(a) }
|
|
259
|
-
original.guidelines.each { |g| copy.add_guideline(g) }
|
|
260
|
-
copy
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def clone_contour(original)
|
|
264
|
-
points = original.points.map do |p|
|
|
265
|
-
Ufo::Point.new(x: p.x, y: p.y, type: p.type, smooth: p.smooth)
|
|
266
|
-
end
|
|
267
|
-
Ufo::Contour.new(points)
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def propagate_cbdt_tables(path)
|
|
271
|
-
source = cbdt_source
|
|
272
|
-
return unless source
|
|
273
|
-
|
|
274
|
-
compiled = Fontisan::FontLoader.load(path)
|
|
275
|
-
|
|
276
|
-
# Read every table as raw bytes straight from the file's table
|
|
277
|
-
# directory. We deliberately bypass #table (which parses via
|
|
278
|
-
# BinData) because some tables — notably CFF2 — don't yet have
|
|
279
|
-
# round-trippable BinData models; calling #table on them returns
|
|
280
|
-
# nil and would silently drop them from the rewritten font.
|
|
281
|
-
tables = {}
|
|
282
|
-
compiled.table_names.each do |tag|
|
|
283
|
-
raw = compiled.table_data[tag]
|
|
284
|
-
tables[tag] = raw if raw
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
cbdt_bytes = source.raw_table_bytes("CBDT")
|
|
288
|
-
cblc_bytes = source.raw_table_bytes("CBLC")
|
|
289
|
-
tables["CBDT"] = cbdt_bytes if cbdt_bytes
|
|
290
|
-
tables["CBLC"] = cblc_bytes if cblc_bytes
|
|
291
|
-
|
|
292
|
-
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
293
|
-
Fontisan::FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def add_all_cbdt_glyphs(source, target)
|
|
297
|
-
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
298
|
-
if ufo
|
|
299
|
-
ufo.glyphs.each_value { |g| target.layers.default_layer.add(clone_glyph(g, name: g.name)) }
|
|
300
|
-
return
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
maxp = source.font.table("maxp")
|
|
304
|
-
num_glyphs = maxp&.num_glyphs || 0
|
|
305
|
-
cmap = source.font.table("cmap")
|
|
306
|
-
mappings = cmap&.unicode_mappings || {}
|
|
307
|
-
|
|
308
|
-
gid_cps = Hash.new { |h, k| h[k] = [] }
|
|
309
|
-
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
310
|
-
|
|
311
|
-
num_glyphs.times do |gid|
|
|
312
|
-
name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
313
|
-
glyph = Ufo::Glyph.new(name: name)
|
|
314
|
-
glyph.width = 0
|
|
315
|
-
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
316
|
-
target.layers.default_layer.add(glyph)
|
|
317
|
-
end
|
|
164
|
+
cbdt = cbdt_propagator.safe_cbdt_source
|
|
165
|
+
copier = GlyphCopier.new(deduplicator)
|
|
166
|
+
|
|
167
|
+
# Glyph ordering matters: Cmap.build uses first-wins semantics
|
|
168
|
+
# (Cmap.build docstring), so outline donors must be stitched
|
|
169
|
+
# before CBDT placeholders. Otherwise the empty CBDT placeholder
|
|
170
|
+
# would land at a lower GID and win the cp→gid mapping for any
|
|
171
|
+
# codepoint covered by both donors, hiding the real outline glyph.
|
|
172
|
+
# CBDT bitmap data still propagates via CbdtPropagator.
|
|
173
|
+
copier.inject_notdef(bindings, target)
|
|
174
|
+
copier.copy_outlines(bindings, target, skip_sources: cbdt ? [cbdt] : [])
|
|
175
|
+
cbdt_propagator.add_placeholder_glyphs(cbdt, target) if cbdt
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def cbdt_propagator
|
|
179
|
+
@cbdt_propagator ||= CbdtPropagator.new(@sources.values)
|
|
318
180
|
end
|
|
319
181
|
end
|
|
320
182
|
end
|
|
@@ -11,6 +11,14 @@ module Fontisan
|
|
|
11
11
|
# Both subtables share the same segment list (capped to BMP for
|
|
12
12
|
# format 4); format 4 is required by Windows even though format
|
|
13
13
|
# 12 is more capable.
|
|
14
|
+
#
|
|
15
|
+
# Contract — first-wins: when multiple glyphs declare the same
|
|
16
|
+
# codepoint, the glyph at the LOWER GID wins the cp→gid mapping.
|
|
17
|
+
# Callers that want one donor to outrank another for a shared
|
|
18
|
+
# codepoint (e.g. outline donor vs. CBDT placeholder) MUST order
|
|
19
|
+
# the higher-priority glyphs earlier in the `glyphs:` array.
|
|
20
|
+
# Stitcher relies on this for outline-first stitching.
|
|
21
|
+
#
|
|
14
22
|
# @see https://learn.microsoft.com/en-us/typography/opentype/spec/cmap
|
|
15
23
|
module Cmap
|
|
16
24
|
PLATFORM_WINDOWS = 3
|
|
@@ -195,7 +195,7 @@ module Fontisan
|
|
|
195
195
|
|
|
196
196
|
def self.build_empty(glyph_count)
|
|
197
197
|
# Minimal gvar with no variation data
|
|
198
|
-
header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1, glyph_count
|
|
198
|
+
header = [VERSION_MAJOR, VERSION_MINOR, 0, 0, 0, glyph_count, 1, glyph_count + 16].pack("nnnnnnNN")
|
|
199
199
|
offsets = Array.new(glyph_count + 1, 0).pack("C*")
|
|
200
200
|
header + offsets
|
|
201
201
|
end
|
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.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -436,9 +436,11 @@ files:
|
|
|
436
436
|
- lib/fontisan/sfnt_font.rb
|
|
437
437
|
- lib/fontisan/sfnt_table.rb
|
|
438
438
|
- lib/fontisan/stitcher.rb
|
|
439
|
+
- lib/fontisan/stitcher/cbdt_propagator.rb
|
|
439
440
|
- lib/fontisan/stitcher/collection_result.rb
|
|
440
441
|
- lib/fontisan/stitcher/deduplicator.rb
|
|
441
442
|
- lib/fontisan/stitcher/format_metadata.rb
|
|
443
|
+
- lib/fontisan/stitcher/glyph_copier.rb
|
|
442
444
|
- lib/fontisan/stitcher/glyph_limit.rb
|
|
443
445
|
- lib/fontisan/stitcher/glyph_signature.rb
|
|
444
446
|
- lib/fontisan/stitcher/partition_strategy.rb
|