fontisan 0.4.22 → 0.4.24
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 +240 -24
- data/Gemfile +6 -0
- data/Rakefile +9 -6
- data/benchmark/compile_benchmark.rb +13 -5
- data/docs/.vitepress/config.ts +1 -0
- data/docs/STITCHER_GUIDE.adoc +47 -3
- data/docs/api/layer.md +89 -0
- data/docs/cli/subset.md +25 -6
- data/docs/guide/cli/subset.md +31 -0
- data/docs/guide/color-fonts/bitmaps.md +17 -0
- data/lib/fontisan/cli.rb +2 -1
- data/lib/fontisan/collection/writer.rb +2 -1
- data/lib/fontisan/commands/validate_collection_command.rb +7 -1
- data/lib/fontisan/commands.rb +2 -1
- data/lib/fontisan/config/subset_profiles.yml +2 -0
- data/lib/fontisan/converters/collection_converter.rb +6 -1
- data/lib/fontisan/converters/svg_generator.rb +3 -1
- data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
- data/lib/fontisan/stitcher/collection_result.rb +2 -1
- data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +5 -34
- data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
- data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
- data/lib/fontisan/stitcher/source.rb +2 -1
- data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
- data/lib/fontisan/stitcher.rb +4 -1
- 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 +146 -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/svg/standalone_glyph.rb +4 -1
- data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
- data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
- data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
- 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/cff/cff2_charstring_builder.rb +11 -5
- data/lib/fontisan/tables/cff.rb +2 -1
- data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
- data/lib/fontisan/tables.rb +12 -0
- data/lib/fontisan/ufo/cli.rb +4 -1
- data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
- data/lib/fontisan/ufo/compile/cff.rb +6 -2
- data/lib/fontisan/ufo/compile/cmap.rb +11 -5
- data/lib/fontisan/ufo/compile/cpal.rb +2 -1
- data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
- data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
- data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
- data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
- data/lib/fontisan/ufo/compile/fvar.rb +7 -3
- data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
- data/lib/fontisan/ufo/compile/gvar.rb +5 -2
- data/lib/fontisan/ufo/compile/head.rb +2 -1
- data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
- data/lib/fontisan/ufo/compile/name.rb +3 -1
- data/lib/fontisan/ufo/compile/sbix.rb +2 -1
- data/lib/fontisan/ufo/compile/stat.rb +6 -3
- data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
- data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
- data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
- data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
- data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
- data/lib/fontisan/ufo/convert.rb +4 -1
- data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
- data/lib/fontisan/ufo/info.rb +4 -2
- data/lib/fontisan/ufo/layer.rb +42 -2
- data/lib/fontisan/ufo/plist.rb +7 -2
- data/lib/fontisan/ufo/reader.rb +3 -1
- data/lib/fontisan/ufo/writer.rb +13 -5
- data/lib/fontisan/ufo.rb +2 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_decoder.rb +4 -1
- data/lib/fontisan/woff2/collection_encoder.rb +4 -1
- data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
- data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
- data/lib/fontisan.rb +0 -1
- metadata +36 -4
- data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
- data/lib/fontisan/tasks.rb +0 -11
|
@@ -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
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Plan for the subset of color bitmaps: which source strikes have
|
|
7
|
+
# any surviving glyph, and the per-strike runs of surviving new
|
|
8
|
+
# GIDs. Built once by [ColorBitmapSubsetter] from a CBLC + a
|
|
9
|
+
# GlyphMapping, then used to drive both CBDT and CBLC emission.
|
|
10
|
+
class ColorBitmapSubsetPlan
|
|
11
|
+
attr_reader :strikes, :strike_plans, :placements
|
|
12
|
+
|
|
13
|
+
def initialize(mapping)
|
|
14
|
+
@mapping = mapping
|
|
15
|
+
@strikes = []
|
|
16
|
+
@strike_plans = []
|
|
17
|
+
@placements = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Walk every (gid, strike) location in CBLC and capture the
|
|
21
|
+
# ones whose source gid is in the subset. Remaps the gid via
|
|
22
|
+
# the supplied mapping.
|
|
23
|
+
def collect!(cblc)
|
|
24
|
+
cblc.bitmap_sizes.each do |strike|
|
|
25
|
+
strike_plan = ColorBitmapStrikePlan.new(strike)
|
|
26
|
+
|
|
27
|
+
cblc.sub_tables_for(strike).each do |sub|
|
|
28
|
+
sub.locations.each do |loc|
|
|
29
|
+
new_gid = @mapping.new_id(loc.glyph_id)
|
|
30
|
+
next unless new_gid
|
|
31
|
+
|
|
32
|
+
placement = ColorBitmapPlacement.new(
|
|
33
|
+
source_gid: loc.glyph_id,
|
|
34
|
+
new_gid: new_gid,
|
|
35
|
+
source_offset: loc.cbdt_offset,
|
|
36
|
+
byte_length: loc.byte_length,
|
|
37
|
+
image_format: loc.image_format,
|
|
38
|
+
strike_ppem: strike.ppem,
|
|
39
|
+
)
|
|
40
|
+
@placements << placement
|
|
41
|
+
strike_plan.add(placement)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
next if strike_plan.empty?
|
|
46
|
+
|
|
47
|
+
@strikes << strike
|
|
48
|
+
@strike_plans << strike_plan
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def each_placement(&)
|
|
53
|
+
@placements.each(&)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def each_strike_plan(&)
|
|
57
|
+
@strike_plans.each(&)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Paired CBDT + CBLC subsetter.
|
|
7
|
+
#
|
|
8
|
+
# CBDT (Color Bitmap Data) and CBLC (Color Bitmap Location) are
|
|
9
|
+
# tightly coupled: CBLC indexes glyph IDs to CBDT byte offsets,
|
|
10
|
+
# and CBDT is just a blob of bitmap blocks. Subsetting requires
|
|
11
|
+
# walking both at once — filtering to subset GIDs, copying the
|
|
12
|
+
# retained bitmap blocks into a fresh CBDT, and rewriting CBLC's
|
|
13
|
+
# IndexSubTable offsets to match.
|
|
14
|
+
#
|
|
15
|
+
# Both the Cbdt and Cblc strategies call into this collaborator,
|
|
16
|
+
# which performs the work once and caches both byte strings. This
|
|
17
|
+
# keeps the strategies themselves trivial and avoids forcing the
|
|
18
|
+
# TableSubsetter to know about CBDT/CBLC ordering.
|
|
19
|
+
#
|
|
20
|
+
# The output CBLC keeps the source BitmapSize records verbatim
|
|
21
|
+
# (ppem, metrics, glyph_range) and rewrites each IndexSubTable's
|
|
22
|
+
# `imageDataOffset` and offset array to point into the new CBDT.
|
|
23
|
+
# Glyph IDs are remapped via the supplied [GlyphMapping] so the
|
|
24
|
+
# subset's CBLC references subset GIDs, not source GIDs.
|
|
25
|
+
#
|
|
26
|
+
# Output IndexSubTables always use format 1 (uint32 offsets) —
|
|
27
|
+
# it has no alignment requirement, so arbitrary bitmap block
|
|
28
|
+
# sizes from the source work without padding.
|
|
29
|
+
#
|
|
30
|
+
# Reference: OpenType CBDT/CBLC specifications.
|
|
31
|
+
class ColorBitmapSubsetter
|
|
32
|
+
# @return [String] new CBDT bytes (header + retained blocks)
|
|
33
|
+
attr_reader :cbdt_bytes
|
|
34
|
+
|
|
35
|
+
# @return [String] new CBLC bytes (header + BitmapSize + ISTA + IST)
|
|
36
|
+
attr_reader :cblc_bytes
|
|
37
|
+
|
|
38
|
+
# @param font [SfntFont]
|
|
39
|
+
# @param mapping [GlyphMapping] old↔new GID map for the subset
|
|
40
|
+
def initialize(font:, mapping:)
|
|
41
|
+
@font = font
|
|
42
|
+
@mapping = mapping
|
|
43
|
+
@cbdt_bytes = +""
|
|
44
|
+
@cblc_bytes = +""
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Build the subset CBDT + CBLC bytes. Idempotent.
|
|
48
|
+
#
|
|
49
|
+
# @return [self]
|
|
50
|
+
def build
|
|
51
|
+
return self if @built
|
|
52
|
+
|
|
53
|
+
source_cblc = @font.table_data["CBLC"]
|
|
54
|
+
source_cbdt = @font.table_data["CBDT"]
|
|
55
|
+
|
|
56
|
+
if source_cblc.nil? || source_cbdt.nil?
|
|
57
|
+
@built = true
|
|
58
|
+
return self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
cblc = Tables::Cblc.read(source_cblc)
|
|
62
|
+
cbdt = Tables::Cbdt.read(source_cbdt)
|
|
63
|
+
|
|
64
|
+
plan = ColorBitmapSubsetPlan.new(@mapping)
|
|
65
|
+
plan.collect!(cblc)
|
|
66
|
+
|
|
67
|
+
emit_cbdt(cbdt, plan)
|
|
68
|
+
emit_cblc(cblc, plan)
|
|
69
|
+
|
|
70
|
+
@built = true
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Emit CBDT: preserve the source 4-byte header, then concatenate
|
|
77
|
+
# every retained bitmap block in (strike, gid) order. The
|
|
78
|
+
# placement's new_offset is recorded so CBLC emission can
|
|
79
|
+
# reference it.
|
|
80
|
+
def emit_cbdt(cbdt, plan)
|
|
81
|
+
@cbdt_bytes = String.new(encoding: Encoding::BINARY)
|
|
82
|
+
@cbdt_bytes << cbdt.raw_data[0, 4] # majorVersion + minorVersion
|
|
83
|
+
|
|
84
|
+
plan.each_placement do |placement|
|
|
85
|
+
block = cbdt.bitmap_data_at(placement.source_offset,
|
|
86
|
+
placement.byte_length)
|
|
87
|
+
next unless block
|
|
88
|
+
|
|
89
|
+
placement.new_offset = @cbdt_bytes.bytesize
|
|
90
|
+
@cbdt_bytes << block
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Emit CBLC: header + BitmapSize section + IndexSubTableArray
|
|
95
|
+
# section + IndexSubTable records. The IndexSubTableArray entries
|
|
96
|
+
# are grouped per strike; IndexSubTable records are appended
|
|
97
|
+
# after all ISTA entries. Each IndexSubTable is format 1.
|
|
98
|
+
def emit_cblc(cblc, plan)
|
|
99
|
+
output = String.new(encoding: Encoding::BINARY)
|
|
100
|
+
output << [Integer(cblc.version), plan.strikes.size].pack("NN")
|
|
101
|
+
|
|
102
|
+
bitmap_size_section_end = output.bytesize + (plan.strikes.size * 48)
|
|
103
|
+
ista_offsets = build_ista_offsets(plan, bitmap_size_section_end)
|
|
104
|
+
ista_section_end = ista_offsets.last.to_i +
|
|
105
|
+
(plan.strike_plans.sum(&:subtable_count) * 8)
|
|
106
|
+
|
|
107
|
+
# BitmapSize records (patched array_offset + number_of_subtables).
|
|
108
|
+
plan.strike_plans.each_with_index do |strike_plan, idx|
|
|
109
|
+
output << patch_bitmap_size(strike_plan.source_strike,
|
|
110
|
+
ista_offsets[idx],
|
|
111
|
+
strike_plan.subtable_count)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# IndexSubTableArray entries (one per subtable plan).
|
|
115
|
+
ist_cursor = ista_section_end
|
|
116
|
+
plan.strike_plans.each_with_index do |strike_plan, idx|
|
|
117
|
+
strike_ista_offset = ista_offsets[idx]
|
|
118
|
+
strike_plan.subtable_plans.each do |sub|
|
|
119
|
+
additional = ist_cursor - strike_ista_offset
|
|
120
|
+
output << [sub.first_new_gid, sub.last_new_gid,
|
|
121
|
+
additional].pack("nnN")
|
|
122
|
+
ist_cursor += sub.byte_length
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# IndexSubTable records (format 1).
|
|
127
|
+
plan.each_strike_plan do |strike_plan|
|
|
128
|
+
strike_plan.subtable_plans.each do |sub|
|
|
129
|
+
output << build_index_sub_table(sub)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@cblc_bytes = output
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Absolute CBLC offset of each survivor strike's IndexSubTableArray.
|
|
137
|
+
# Each strike's ISTA is laid out right after the BitmapSize section
|
|
138
|
+
# and the previous strikes' ISTA entries (8 bytes × subtable count).
|
|
139
|
+
def build_ista_offsets(plan, bitmap_size_section_end)
|
|
140
|
+
offsets = []
|
|
141
|
+
cursor = bitmap_size_section_end
|
|
142
|
+
plan.strike_plans.each do |sp|
|
|
143
|
+
offsets << cursor
|
|
144
|
+
cursor += sp.subtable_count * 8
|
|
145
|
+
end
|
|
146
|
+
offsets
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Clone a BitmapSize with patched `index_subtable_array_offset`
|
|
150
|
+
# and `number_of_index_subtables`; everything else preserved
|
|
151
|
+
# from the source.
|
|
152
|
+
def patch_bitmap_size(source_strike, new_ista_offset, subtable_count)
|
|
153
|
+
clone = Tables::CblcBitmapSize.new
|
|
154
|
+
clone.index_subtable_array_offset = new_ista_offset
|
|
155
|
+
clone.index_tables_size = source_strike.index_tables_size
|
|
156
|
+
clone.number_of_index_subtables = subtable_count
|
|
157
|
+
clone.color_ref = source_strike.color_ref
|
|
158
|
+
clone.hori = source_strike.hori
|
|
159
|
+
clone.vert = source_strike.vert
|
|
160
|
+
clone.start_glyph_index = source_strike.start_glyph_index
|
|
161
|
+
clone.end_glyph_index = source_strike.end_glyph_index
|
|
162
|
+
clone.ppem_x = source_strike.ppem_x
|
|
163
|
+
clone.ppem_y = source_strike.ppem_y
|
|
164
|
+
clone.bit_depth = source_strike.bit_depth
|
|
165
|
+
clone.flags = source_strike.flags
|
|
166
|
+
clone.to_binary_s
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Format 1 IndexSubTable: 8-byte header + uint32 offsetArray[count+1].
|
|
170
|
+
# imageDataOffset = absolute new CBDT offset of the first glyph's
|
|
171
|
+
# bitmap. offsets[i] = relative offset from imageDataOffset for
|
|
172
|
+
# glyph i.
|
|
173
|
+
def build_index_sub_table(sub)
|
|
174
|
+
bytes = String.new(encoding: Encoding::BINARY)
|
|
175
|
+
bytes << [1, sub.image_format, sub.first_new_offset].pack("nnN")
|
|
176
|
+
bytes << sub.offset_array.pack("N*")
|
|
177
|
+
bytes
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Plan for one IndexSubTable emitted by [ColorBitmapSubsetter]:
|
|
7
|
+
# a contiguous run of new GIDs at a single strike, with the
|
|
8
|
+
# CBDT offsets and lengths needed to emit a format 1 IndexSubTable.
|
|
9
|
+
class ColorBitmapSubtablePlan
|
|
10
|
+
attr_reader :placements, :strike_ppem, :image_format
|
|
11
|
+
|
|
12
|
+
def initialize(strike_ppem:, image_format:)
|
|
13
|
+
@strike_ppem = strike_ppem
|
|
14
|
+
@image_format = image_format
|
|
15
|
+
@placements = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add(placement)
|
|
19
|
+
@placements << placement
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def first_new_gid
|
|
23
|
+
@placements.first.new_gid
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def last_new_gid
|
|
27
|
+
@placements.last.new_gid
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Absolute CBDT offset where this subtable's first glyph's
|
|
31
|
+
# bitmap begins. Assigned during CBDT emission.
|
|
32
|
+
#
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
def first_new_offset
|
|
35
|
+
@placements.first.new_offset
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# IndexSubTable format 1 offset array (relative to
|
|
39
|
+
# imageDataOffset). offsets[0] is 0 (first glyph starts at
|
|
40
|
+
# imageDataOffset); offsets[i+1] = offsets[i] + byte_length.
|
|
41
|
+
#
|
|
42
|
+
# @return [Array<Integer>]
|
|
43
|
+
def offset_array
|
|
44
|
+
arr = [0]
|
|
45
|
+
@placements.each do |p|
|
|
46
|
+
arr << (arr.last + p.byte_length)
|
|
47
|
+
end
|
|
48
|
+
arr
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Byte length of this IndexSubTable in the output CBLC:
|
|
52
|
+
# 8-byte header + (count + 1) × 4-byte offsets.
|
|
53
|
+
def byte_length
|
|
54
|
+
8 + (@placements.size + 1) * 4
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Glyf strategy: extract each subset glyph's bytes from the source
|
|
7
|
+
# glyf table (remapping composite component glyph IDs), build the
|
|
8
|
+
# corresponding loca offsets, and stash both into [SharedState] so
|
|
9
|
+
# the Loca strategy can read them. Also records the union bbox
|
|
10
|
+
# for the Head strategy.
|
|
11
|
+
class Glyf
|
|
12
|
+
# @param context [SubsetContext]
|
|
13
|
+
# @param tag [String] "glyf"
|
|
14
|
+
# @param table [Glyf] parsed glyf table (unused; builder reads
|
|
15
|
+
# the table from `context.font` directly so it sees the same
|
|
16
|
+
# instance)
|
|
17
|
+
# @return [String] binary glyf bytes for the subset
|
|
18
|
+
def self.call(context:, tag:, table:)
|
|
19
|
+
builder = GlyfLocaBuilder.new(font: context.font,
|
|
20
|
+
mapping: context.mapping).build
|
|
21
|
+
context.state.glyf_data = builder.glyf_data
|
|
22
|
+
context.state.loca_offsets = builder.loca_offsets
|
|
23
|
+
context.state.subset_bbox = builder.bbox
|
|
24
|
+
builder.glyf_data
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Glyf + Loca are emitted together because loca is just an offset
|
|
7
|
+
# index into glyf. This builder walks the subset's glyph mapping,
|
|
8
|
+
# extracts each glyph's bytes (remapping composite component refs),
|
|
9
|
+
# and produces the new glyf bytes, the new loca offsets, and the
|
|
10
|
+
# union bounding box over the subset's glyphs.
|
|
11
|
+
#
|
|
12
|
+
# Extracted as a collaborator so both the Glyf and Loca strategies
|
|
13
|
+
# can share a single build pass without coupling to each other or
|
|
14
|
+
# to TableSubsetter internals.
|
|
15
|
+
class GlyfLocaBuilder
|
|
16
|
+
# @param font [SfntFont]
|
|
17
|
+
# @param mapping [GlyphMapping]
|
|
18
|
+
def initialize(font:, mapping:)
|
|
19
|
+
@font = font
|
|
20
|
+
@mapping = mapping
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] new glyf bytes
|
|
24
|
+
attr_reader :glyf_data
|
|
25
|
+
|
|
26
|
+
# @return [Array<Integer>] loca offsets (one per glyph + 1)
|
|
27
|
+
attr_reader :loca_offsets
|
|
28
|
+
|
|
29
|
+
# @return [Array(Integer, Integer, Integer, Integer), nil] union
|
|
30
|
+
# bbox as [x_min, y_min, x_max, y_max], or nil if all glyphs empty
|
|
31
|
+
attr_reader :bbox
|
|
32
|
+
|
|
33
|
+
# Build glyf + loca + bbox. Idempotent — re-calling is a no-op.
|
|
34
|
+
#
|
|
35
|
+
# @return [self]
|
|
36
|
+
def build
|
|
37
|
+
return self if @glyf_data
|
|
38
|
+
|
|
39
|
+
glyf = @font.table("glyf")
|
|
40
|
+
loca = @font.table("loca")
|
|
41
|
+
head = @font.table("head")
|
|
42
|
+
|
|
43
|
+
ensure_loca_parsed!(loca, head)
|
|
44
|
+
|
|
45
|
+
@glyf_data = String.new(encoding: Encoding::BINARY)
|
|
46
|
+
@loca_offsets = []
|
|
47
|
+
current_offset = 0
|
|
48
|
+
|
|
49
|
+
bbox_x_min = 1 << 30
|
|
50
|
+
bbox_y_min = 1 << 30
|
|
51
|
+
bbox_x_max = -(1 << 30)
|
|
52
|
+
bbox_y_max = -(1 << 30)
|
|
53
|
+
|
|
54
|
+
@mapping.old_ids.each do |old_id|
|
|
55
|
+
@loca_offsets << current_offset
|
|
56
|
+
|
|
57
|
+
offset = loca.offset_for(old_id)
|
|
58
|
+
size = loca.size_of(old_id)
|
|
59
|
+
next if size.nil? || size.zero?
|
|
60
|
+
|
|
61
|
+
glyph_data = glyf.raw_data[offset, size]
|
|
62
|
+
|
|
63
|
+
if glyph_data.bytesize >= 10
|
|
64
|
+
_n, gx_min, gy_min, gx_max, gy_max = glyph_data[0,
|
|
65
|
+
10].unpack("n5")
|
|
66
|
+
gx_min = to_signed_16(gx_min)
|
|
67
|
+
gy_min = to_signed_16(gy_min)
|
|
68
|
+
gx_max = to_signed_16(gx_max)
|
|
69
|
+
gy_max = to_signed_16(gy_max)
|
|
70
|
+
bbox_x_min = gx_min if gx_min < bbox_x_min
|
|
71
|
+
bbox_y_min = gy_min if gy_min < bbox_y_min
|
|
72
|
+
bbox_x_max = gx_max if gx_max > bbox_x_max
|
|
73
|
+
bbox_y_max = gy_max if gy_max > bbox_y_max
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
glyph_data = remap_compound!(glyph_data) if compound?(glyph_data)
|
|
77
|
+
|
|
78
|
+
@glyf_data << glyph_data
|
|
79
|
+
current_offset += glyph_data.bytesize
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@loca_offsets << current_offset
|
|
83
|
+
|
|
84
|
+
return if bbox_x_min > bbox_x_max
|
|
85
|
+
|
|
86
|
+
@bbox = [bbox_x_min, bbox_y_min, bbox_x_max, bbox_y_max]
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def ensure_loca_parsed!(loca, head)
|
|
93
|
+
return if loca.parsed?
|
|
94
|
+
|
|
95
|
+
maxp = @font.table("maxp")
|
|
96
|
+
loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def compound?(data)
|
|
100
|
+
return false if data.length < 2
|
|
101
|
+
|
|
102
|
+
num_contours = to_signed_16(data[0, 2].unpack1("n"))
|
|
103
|
+
num_contours == -1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def remap_compound!(data)
|
|
107
|
+
new_data = data.dup
|
|
108
|
+
offset = 10 # skip 10-byte header
|
|
109
|
+
|
|
110
|
+
loop do
|
|
111
|
+
break if offset >= new_data.length - 4
|
|
112
|
+
|
|
113
|
+
flags = new_data[offset, 2].unpack1("n")
|
|
114
|
+
old_glyph_index = new_data[offset + 2, 2].unpack1("n")
|
|
115
|
+
|
|
116
|
+
new_glyph_index = @mapping.new_id(old_glyph_index)
|
|
117
|
+
unless new_glyph_index
|
|
118
|
+
raise Fontisan::SubsettingError,
|
|
119
|
+
"Component glyph #{old_glyph_index} not in subset"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
new_data[offset + 2, 2] = [new_glyph_index].pack("n")
|
|
123
|
+
offset += 4 # flags + glyph_index
|
|
124
|
+
offset += (flags & 0x0001).zero? ? 2 : 4 # args
|
|
125
|
+
|
|
126
|
+
if (flags & 0x0080) != 0
|
|
127
|
+
offset += 8 # 2x2 matrix
|
|
128
|
+
elsif (flags & 0x0040) != 0
|
|
129
|
+
offset += 4 # X and Y scale
|
|
130
|
+
elsif (flags & 0x0008) != 0
|
|
131
|
+
offset += 2 # uniform scale
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
break unless (flags & 0x0020) != 0 # MORE_COMPONENTS
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
new_data
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def to_signed_16(value)
|
|
141
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|