fontisan 0.4.21 → 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 +5 -5
- data/lib/fontisan/stitcher/cbdt_propagator.rb +151 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +139 -0
- data/lib/fontisan/stitcher.rb +12 -159
- 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-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
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -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,25 +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
|
-
# 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
134
|
def build_target_for(subfont_name)
|
|
152
135
|
bindings = @subfonts[subfont_name] || []
|
|
153
136
|
target = Ufo::Font.new
|
|
@@ -165,7 +148,7 @@ module Fontisan
|
|
|
165
148
|
Dir.mktmpdir do |dir|
|
|
166
149
|
sub_path = File.join(dir, "sub#{subfont_name}#{metadata.extension}")
|
|
167
150
|
metadata.compiler_class.new(target).compile(output_path: sub_path)
|
|
168
|
-
|
|
151
|
+
cbdt_propagator.propagate_tables_into(cbdt_propagator.cbdt_source, sub_path)
|
|
169
152
|
|
|
170
153
|
loaded = Fontisan::FontLoader.load(sub_path)
|
|
171
154
|
stats = SubfontStats.new(
|
|
@@ -178,152 +161,22 @@ module Fontisan
|
|
|
178
161
|
end
|
|
179
162
|
|
|
180
163
|
def assign_gids_and_copy_glyphs(bindings, target, deduplicator)
|
|
181
|
-
cbdt = safe_cbdt_source
|
|
164
|
+
cbdt = cbdt_propagator.safe_cbdt_source
|
|
165
|
+
copier = GlyphCopier.new(deduplicator)
|
|
182
166
|
|
|
183
167
|
# Glyph ordering matters: Cmap.build uses first-wins semantics
|
|
184
168
|
# (Cmap.build docstring), so outline donors must be stitched
|
|
185
169
|
# before CBDT placeholders. Otherwise the empty CBDT placeholder
|
|
186
170
|
# would land at a lower GID and win the cp→gid mapping for any
|
|
187
171
|
# 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)
|
|
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
|
|
247
176
|
end
|
|
248
177
|
|
|
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
|
|
178
|
+
def cbdt_propagator
|
|
179
|
+
@cbdt_propagator ||= CbdtPropagator.new(@sources.values)
|
|
327
180
|
end
|
|
328
181
|
end
|
|
329
182
|
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
|