fontisan 0.4.22 → 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 +5 -0
- data/lib/fontisan/config/subset_profiles.yml +2 -0
- data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
- data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +3 -33
- data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
- data/lib/fontisan/stitcher.rb +2 -0
- 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 +35 -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
|
@@ -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:
|
|
@@ -60,6 +60,19 @@ module Fontisan
|
|
|
60
60
|
# Outline data is intentionally absent — the bitmap lives in
|
|
61
61
|
# the CBDT table that #propagate_tables_into will copy later.
|
|
62
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
|
+
#
|
|
63
76
|
# Glyphs are NOT registered with the deduplicator: each CBDT
|
|
64
77
|
# glyph is unique to its source, and deduplication would
|
|
65
78
|
# incorrectly collapse distinct bitmaps.
|
|
@@ -69,7 +82,10 @@ module Fontisan
|
|
|
69
82
|
def add_placeholder_glyphs(source, target)
|
|
70
83
|
ufo = source.font.is_a?(Ufo::Font) ? source.font : nil
|
|
71
84
|
if ufo
|
|
72
|
-
ufo.glyphs.each_value
|
|
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
|
|
73
89
|
return
|
|
74
90
|
end
|
|
75
91
|
|
|
@@ -82,7 +98,8 @@ module Fontisan
|
|
|
82
98
|
mappings.each { |cp, gid| gid_cps[gid] << cp }
|
|
83
99
|
|
|
84
100
|
num_glyphs.times do |gid|
|
|
85
|
-
|
|
101
|
+
base_name = gid.zero? ? ".notdef" : "gid#{gid}"
|
|
102
|
+
name = UniqueGlyphName.in(target, base_name)
|
|
86
103
|
glyph = Ufo::Glyph.new(name: name)
|
|
87
104
|
glyph.width = 0
|
|
88
105
|
gid_cps[gid].each { |cp| glyph.add_unicode(cp) }
|
|
@@ -100,6 +117,31 @@ module Fontisan
|
|
|
100
117
|
#
|
|
101
118
|
# No-op when called with a non-CBDT source or nil.
|
|
102
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
|
+
#
|
|
103
145
|
# @param source [Stitcher::Source, nil] the CBDT source
|
|
104
146
|
# @param path [String] compiled font file to rewrite
|
|
105
147
|
def propagate_tables_into(source, path)
|
|
@@ -126,26 +168,6 @@ module Fontisan
|
|
|
126
168
|
sfnt = tables.key?("CFF ") || tables.key?("CFF2") ? 0x4F54544F : 0x00010000
|
|
127
169
|
FontWriter.write_to_file(tables, path, sfnt_version: sfnt)
|
|
128
170
|
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
171
|
end
|
|
150
172
|
end
|
|
151
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Fontisan
|
|
4
4
|
class Stitcher
|
|
@@ -74,7 +74,7 @@ module Fontisan
|
|
|
74
74
|
if canonical && target.glyphs.key?(canonical)
|
|
75
75
|
add_extra_unicode(target, canonical, binding[:codepoint])
|
|
76
76
|
else
|
|
77
|
-
name =
|
|
77
|
+
name = UniqueGlyphName.in(target, glyph.name)
|
|
78
78
|
copy_glyph_into(target, name: name, source: binding[:source],
|
|
79
79
|
donor_gid: binding[:donor_gid],
|
|
80
80
|
codepoint: binding[:codepoint])
|
|
@@ -93,7 +93,7 @@ module Fontisan
|
|
|
93
93
|
original = source.glyph_for_gid(donor_gid)
|
|
94
94
|
return unless original
|
|
95
95
|
|
|
96
|
-
copy =
|
|
96
|
+
copy = GlyphCloner.clone(original, name: name)
|
|
97
97
|
copy.add_unicode(codepoint) if codepoint
|
|
98
98
|
target_font.layers.default_layer.add(copy)
|
|
99
99
|
end
|
|
@@ -104,36 +104,6 @@ module Fontisan
|
|
|
104
104
|
glyph = target_font.glyph(glyph_name)
|
|
105
105
|
glyph.add_unicode(codepoint) unless glyph.unicodes.include?(codepoint)
|
|
106
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
107
|
end
|
|
138
108
|
end
|
|
139
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
|
@@ -35,7 +35,9 @@ module Fontisan
|
|
|
35
35
|
autoload :FormatMetadata, "fontisan/stitcher/format_metadata"
|
|
36
36
|
autoload :PartitionStrategy, "fontisan/stitcher/partition_strategy"
|
|
37
37
|
autoload :CbdtPropagator, "fontisan/stitcher/cbdt_propagator"
|
|
38
|
+
autoload :GlyphCloner, "fontisan/stitcher/glyph_cloner"
|
|
38
39
|
autoload :GlyphCopier, "fontisan/stitcher/glyph_copier"
|
|
40
|
+
autoload :UniqueGlyphName, "fontisan/stitcher/unique_glyph_name"
|
|
39
41
|
|
|
40
42
|
# Internal: pairs a compiled loaded font with its stats so
|
|
41
43
|
# +write_collection+ can build the collection and the result from a
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
# Per-subsetting shared state.
|
|
6
|
+
#
|
|
7
|
+
# Several subset strategies depend on values computed by earlier
|
|
8
|
+
# strategies during the same subsetting run:
|
|
9
|
+
#
|
|
10
|
+
# - `subset_max_advance` is computed by the Hmtx strategy and read
|
|
11
|
+
# by Hhea (because hhea is processed alphabetically before hmtx).
|
|
12
|
+
# - `glyf_data`, `loca_offsets`, and `subset_bbox` are computed by
|
|
13
|
+
# the Glyf strategy and consumed by Loca and Head.
|
|
14
|
+
#
|
|
15
|
+
# Rather than giving each TableStrategy access to the entire
|
|
16
|
+
# TableSubsetter (which would couple them to internals), the
|
|
17
|
+
# cross-strategy state lives here. Each strategy receives a Context
|
|
18
|
+
# exposing [font], [mapping], [options], and this [SharedState].
|
|
19
|
+
class SharedState
|
|
20
|
+
# @return [String, nil] binary glyf data built by the Glyf strategy
|
|
21
|
+
attr_accessor :glyf_data
|
|
22
|
+
|
|
23
|
+
# @return [Array<Integer>, nil] loca offsets produced by the Glyf strategy
|
|
24
|
+
attr_accessor :loca_offsets
|
|
25
|
+
|
|
26
|
+
# @return [Array(Integer, Integer, Integer, Integer), nil] union
|
|
27
|
+
# xMin/yMin/xMax/yMax over the subset's glyphs
|
|
28
|
+
attr_accessor :subset_bbox
|
|
29
|
+
|
|
30
|
+
# @return [Integer] largest advanceWidth seen while subsetting hmtx
|
|
31
|
+
attr_accessor :subset_max_advance
|
|
32
|
+
|
|
33
|
+
# @return [TableStrategy::ColorBitmapSubsetter, nil] cached paired
|
|
34
|
+
# CBDT+CBLC subsetter so both strategies reuse the same pass
|
|
35
|
+
attr_accessor :color_bitmap_subsetter
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@subset_max_advance = 0
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
# Value object handed to every TableStrategy. Encapsulates the inputs
|
|
6
|
+
# (font, mapping, options) plus the cross-strategy [SharedState] so
|
|
7
|
+
# strategies don't need a back-reference to the TableSubsetter.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute font
|
|
10
|
+
# @return [SfntFont]
|
|
11
|
+
# @!attribute mapping
|
|
12
|
+
# @return [GlyphMapping]
|
|
13
|
+
# @!attribute options
|
|
14
|
+
# @return [Options]
|
|
15
|
+
# @!attribute state
|
|
16
|
+
# @return [SharedState]
|
|
17
|
+
SubsetContext = Struct.new(:font, :mapping, :options, :state,
|
|
18
|
+
keyword_init: true)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# CBDT strategy: delegate to [ColorBitmapSubsetter], which produces
|
|
7
|
+
# both CBDT and CBLC bytes in a single pass. The Cblc strategy
|
|
8
|
+
# reads from the same collaborator so the two tables stay
|
|
9
|
+
# consistent.
|
|
10
|
+
class Cbdt
|
|
11
|
+
# @param context [SubsetContext]
|
|
12
|
+
# @param tag [String] "CBDT"
|
|
13
|
+
# @param table [Cbdt] parsed CBDT table (unused)
|
|
14
|
+
# @return [String] subset CBDT bytes
|
|
15
|
+
def self.call(context:, tag:, table:)
|
|
16
|
+
color_bitmap_subsetter(context).cbdt_bytes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param context [SubsetContext]
|
|
20
|
+
# @return [ColorBitmapSubsetter] cached on the shared state so the
|
|
21
|
+
# Cblc strategy reuses the same pass
|
|
22
|
+
def self.color_bitmap_subsetter(context)
|
|
23
|
+
context.state.color_bitmap_subsetter ||= ColorBitmapSubsetter.new(font: context.font,
|
|
24
|
+
mapping: context.mapping).build
|
|
25
|
+
end
|
|
26
|
+
private_class_method :color_bitmap_subsetter
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# CBLC strategy: delegate to [ColorBitmapSubsetter] (the same
|
|
7
|
+
# collaborator used by the Cbdt strategy) so both tables stay
|
|
8
|
+
# consistent. The subsetter is built lazily on first access and
|
|
9
|
+
# cached on [SharedState].
|
|
10
|
+
class Cblc
|
|
11
|
+
# @param context [SubsetContext]
|
|
12
|
+
# @param tag [String] "CBLC"
|
|
13
|
+
# @param table [Cblc] parsed CBLC table (unused)
|
|
14
|
+
# @return [String] subset CBLC bytes
|
|
15
|
+
def self.call(context:, tag:, table:)
|
|
16
|
+
color_bitmap_subsetter(context).cblc_bytes
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.color_bitmap_subsetter(context)
|
|
20
|
+
context.state.color_bitmap_subsetter ||= ColorBitmapSubsetter.new(font: context.font,
|
|
21
|
+
mapping: context.mapping).build
|
|
22
|
+
end
|
|
23
|
+
private_class_method :color_bitmap_subsetter
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Cmap strategy: rebuild cmap containing only the (codepoint →
|
|
7
|
+
# new GID) mappings for codepoints whose original GID is retained.
|
|
8
|
+
# Emits a format-4 subtable for BMP codepoints and a format-12
|
|
9
|
+
# subtable for supplementary-plane codepoints.
|
|
10
|
+
class Cmap
|
|
11
|
+
# @param context [SubsetContext]
|
|
12
|
+
# @param tag [String] "cmap"
|
|
13
|
+
# @param table [Cmap] parsed cmap table
|
|
14
|
+
# @return [String] binary cmap bytes for the subset
|
|
15
|
+
def self.call(context:, tag:, table:)
|
|
16
|
+
new_mappings = {}
|
|
17
|
+
table.unicode_mappings.each do |char_code, old_gid|
|
|
18
|
+
new_gid = context.mapping.new_id(old_gid)
|
|
19
|
+
new_mappings[char_code] = new_gid if new_gid
|
|
20
|
+
end
|
|
21
|
+
Builder.new(new_mappings).build
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Builds a minimal valid cmap table from a {codepoint => gid} map.
|
|
25
|
+
# Extracted as a sibling class so [Cmap] stays focused on
|
|
26
|
+
# subsetting dispatch while the binary arithmetic lives
|
|
27
|
+
# separately.
|
|
28
|
+
class Builder
|
|
29
|
+
def initialize(mappings)
|
|
30
|
+
@mappings = mappings
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build
|
|
34
|
+
@mappings = { 0 => 0 } if @mappings.empty?
|
|
35
|
+
|
|
36
|
+
bmp = @mappings.select { |cp, _| cp <= 0xFFFF }
|
|
37
|
+
supp = @mappings.select { |cp, _| cp > 0xFFFF }
|
|
38
|
+
|
|
39
|
+
subtables = []
|
|
40
|
+
records = []
|
|
41
|
+
|
|
42
|
+
unless bmp.empty?
|
|
43
|
+
subtables << build_format_4(bmp)
|
|
44
|
+
idx = subtables.size - 1
|
|
45
|
+
records << [3, 1, idx] # Windows BMP
|
|
46
|
+
records << [0, 3, idx] # Unicode BMP
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
unless supp.empty?
|
|
50
|
+
subtables << build_format_12(@mappings)
|
|
51
|
+
idx = subtables.size - 1
|
|
52
|
+
records << [3, 10, idx] # Windows UCS-4
|
|
53
|
+
records << [0, 4, idx] # Unicode full
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
assemble(records, subtables)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def assemble(records, subtables)
|
|
62
|
+
num_tables = records.size
|
|
63
|
+
header = [0, num_tables].pack("nn")
|
|
64
|
+
subtable_base = 4 + (8 * num_tables)
|
|
65
|
+
|
|
66
|
+
offsets = []
|
|
67
|
+
running = subtable_base
|
|
68
|
+
subtables.each do |st|
|
|
69
|
+
offsets << running
|
|
70
|
+
running += st.bytesize
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
record_bytes = +""
|
|
74
|
+
records.each do |pid, eid, idx|
|
|
75
|
+
record_bytes << [pid, eid, offsets[idx]].pack("nnN")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
header + record_bytes + subtables.join
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_format_4(bmp_mappings)
|
|
82
|
+
segments = coalesce_segments(bmp_mappings)
|
|
83
|
+
segments << { start_cp: 0xFFFF, end_cp: 0xFFFF, start_gid: 0 }
|
|
84
|
+
|
|
85
|
+
seg_count = segments.size
|
|
86
|
+
seg_count_x2 = seg_count * 2
|
|
87
|
+
search_range = 2**Math.log2(seg_count).floor * 2
|
|
88
|
+
search_range = 2 if search_range < 2
|
|
89
|
+
entry_selector = Math.log2(search_range / 2).to_i
|
|
90
|
+
range_shift = seg_count_x2 - search_range
|
|
91
|
+
|
|
92
|
+
end_codes = segments.map { |s| s[:end_cp] }
|
|
93
|
+
start_codes = segments.map { |s| s[:start_cp] }
|
|
94
|
+
id_deltas = segments.map do |s|
|
|
95
|
+
(s[:start_gid] - s[:start_cp]) & 0xFFFF
|
|
96
|
+
end
|
|
97
|
+
id_range_offsets = [0] * seg_count
|
|
98
|
+
|
|
99
|
+
subtable = +""
|
|
100
|
+
subtable << [4, 0, 0, seg_count_x2,
|
|
101
|
+
search_range, entry_selector, range_shift].pack("n*")
|
|
102
|
+
subtable << end_codes.pack("n*")
|
|
103
|
+
subtable << [0].pack("n") # reservedPad
|
|
104
|
+
subtable << start_codes.pack("n*")
|
|
105
|
+
subtable << id_deltas.pack("n*")
|
|
106
|
+
subtable << id_range_offsets.pack("n*")
|
|
107
|
+
|
|
108
|
+
subtable[2, 2] = [subtable.bytesize].pack("n")
|
|
109
|
+
subtable
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_format_12(all_mappings)
|
|
113
|
+
groups = coalesce_segments(all_mappings)
|
|
114
|
+
num_groups = groups.size
|
|
115
|
+
|
|
116
|
+
subtable = +""
|
|
117
|
+
subtable << [12, 0, 0, 0, num_groups].pack("nnNNN")
|
|
118
|
+
groups.each do |g|
|
|
119
|
+
subtable << [g[:start_cp], g[:end_cp], g[:start_gid]].pack("NNN")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
subtable[4, 4] = [subtable.bytesize].pack("N")
|
|
123
|
+
subtable
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def coalesce_segments(mappings)
|
|
127
|
+
sorted = mappings.sort_by { |cp, _| cp }
|
|
128
|
+
segments = []
|
|
129
|
+
current = nil
|
|
130
|
+
sorted.each do |cp, gid|
|
|
131
|
+
if current && cp == current[:end_cp] + 1 &&
|
|
132
|
+
gid == current[:start_gid] + (cp - current[:start_cp])
|
|
133
|
+
current[:end_cp] = cp
|
|
134
|
+
else
|
|
135
|
+
segments << current if current
|
|
136
|
+
current = { start_cp: cp, end_cp: cp, start_gid: gid }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
segments << current if current
|
|
140
|
+
segments
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Per-survivor placement: where the source bitmap lives and where it
|
|
4
|
+
# ends up in the subset CBDT.
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Subset
|
|
7
|
+
module TableStrategy
|
|
8
|
+
ColorBitmapPlacement = Struct.new(:source_gid, :new_gid,
|
|
9
|
+
:source_offset, :byte_length,
|
|
10
|
+
:image_format, :strike_ppem,
|
|
11
|
+
:new_offset,
|
|
12
|
+
keyword_init: true) do
|
|
13
|
+
def initialize(*)
|
|
14
|
+
super
|
|
15
|
+
self.new_offset ||= 0
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Plan for one strike emitted by [ColorBitmapSubsetter]: the source
|
|
7
|
+
# BitmapSize plus the list of contiguous new-gid runs (each run
|
|
8
|
+
# becomes one IndexSubTable in the output CBLC).
|
|
9
|
+
class ColorBitmapStrikePlan
|
|
10
|
+
attr_reader :source_strike, :subtable_plans
|
|
11
|
+
|
|
12
|
+
def initialize(source_strike)
|
|
13
|
+
@source_strike = source_strike
|
|
14
|
+
@subtable_plans = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Add a placement, grouping into contiguous new-gid runs. A new
|
|
18
|
+
# run starts when the new gid skips, the ppem changes, or the
|
|
19
|
+
# image format changes.
|
|
20
|
+
def add(placement)
|
|
21
|
+
current = @subtable_plans.last
|
|
22
|
+
|
|
23
|
+
if current && placement.new_gid == current.last_new_gid + 1
|
|
24
|
+
current.add(placement)
|
|
25
|
+
else
|
|
26
|
+
sub = ColorBitmapSubtablePlan.new(
|
|
27
|
+
strike_ppem: placement.strike_ppem,
|
|
28
|
+
image_format: placement.image_format,
|
|
29
|
+
)
|
|
30
|
+
sub.add(placement)
|
|
31
|
+
@subtable_plans << sub
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def empty?
|
|
36
|
+
@subtable_plans.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def subtable_count
|
|
40
|
+
@subtable_plans.size
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|