fontisan 0.4.21 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +10 -5
- data/lib/fontisan/config/subset_profiles.yml +2 -0
- data/lib/fontisan/stitcher/cbdt_propagator.rb +173 -0
- data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
- data/lib/fontisan/stitcher/glyph_copier.rb +109 -0
- data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
- data/lib/fontisan/stitcher.rb +14 -159
- data/lib/fontisan/subset/shared_state.rb +42 -0
- data/lib/fontisan/subset/subset_context.rb +20 -0
- data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
- data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
- data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
- data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
- data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
- data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +145 -0
- data/lib/fontisan/subset/table_strategy/head.rb +42 -0
- data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
- data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
- data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
- data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
- data/lib/fontisan/subset/table_strategy/name.rb +19 -0
- data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
- data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
- data/lib/fontisan/subset/table_strategy/post.rb +37 -0
- data/lib/fontisan/subset/table_strategy.rb +75 -0
- data/lib/fontisan/subset/table_subsetter.rb +49 -616
- data/lib/fontisan/subset.rb +3 -0
- data/lib/fontisan/tables/cbdt.rb +66 -121
- data/lib/fontisan/tables/cblc.rb +110 -231
- data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
- data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
- data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
- data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
- data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
- data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
- data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
- data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
- data/lib/fontisan/tables.rb +11 -0
- data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
- data/lib/fontisan/ufo/layer.rb +42 -2
- data/lib/fontisan/ufo.rb +2 -1
- data/lib/fontisan/version.rb +1 -1
- metadata +37 -2
|
@@ -0,0 +1,145 @@
|
|
|
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, 10].unpack("n5")
|
|
65
|
+
gx_min = to_signed_16(gx_min)
|
|
66
|
+
gy_min = to_signed_16(gy_min)
|
|
67
|
+
gx_max = to_signed_16(gx_max)
|
|
68
|
+
gy_max = to_signed_16(gy_max)
|
|
69
|
+
bbox_x_min = gx_min if gx_min < bbox_x_min
|
|
70
|
+
bbox_y_min = gy_min if gy_min < bbox_y_min
|
|
71
|
+
bbox_x_max = gx_max if gx_max > bbox_x_max
|
|
72
|
+
bbox_y_max = gy_max if gy_max > bbox_y_max
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
glyph_data = remap_compound!(glyph_data) if compound?(glyph_data)
|
|
76
|
+
|
|
77
|
+
@glyf_data << glyph_data
|
|
78
|
+
current_offset += glyph_data.bytesize
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@loca_offsets << current_offset
|
|
82
|
+
|
|
83
|
+
return if bbox_x_min > bbox_x_max
|
|
84
|
+
|
|
85
|
+
@bbox = [bbox_x_min, bbox_y_min, bbox_x_max, bbox_y_max]
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def ensure_loca_parsed!(loca, head)
|
|
92
|
+
return if loca.parsed?
|
|
93
|
+
|
|
94
|
+
maxp = @font.table("maxp")
|
|
95
|
+
loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def compound?(data)
|
|
99
|
+
return false if data.length < 2
|
|
100
|
+
|
|
101
|
+
num_contours = to_signed_16(data[0, 2].unpack1("n"))
|
|
102
|
+
num_contours == -1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def remap_compound!(data)
|
|
106
|
+
new_data = data.dup
|
|
107
|
+
offset = 10 # skip 10-byte header
|
|
108
|
+
|
|
109
|
+
loop do
|
|
110
|
+
break if offset >= new_data.length - 4
|
|
111
|
+
|
|
112
|
+
flags = new_data[offset, 2].unpack1("n")
|
|
113
|
+
old_glyph_index = new_data[offset + 2, 2].unpack1("n")
|
|
114
|
+
|
|
115
|
+
new_glyph_index = @mapping.new_id(old_glyph_index)
|
|
116
|
+
unless new_glyph_index
|
|
117
|
+
raise Fontisan::SubsettingError,
|
|
118
|
+
"Component glyph #{old_glyph_index} not in subset"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
new_data[offset + 2, 2] = [new_glyph_index].pack("n")
|
|
122
|
+
offset += 4 # flags + glyph_index
|
|
123
|
+
offset += (flags & 0x0001).zero? ? 2 : 4 # args
|
|
124
|
+
|
|
125
|
+
if (flags & 0x0080) != 0
|
|
126
|
+
offset += 8 # 2x2 matrix
|
|
127
|
+
elsif (flags & 0x0040) != 0
|
|
128
|
+
offset += 4 # X and Y scale
|
|
129
|
+
elsif (flags & 0x0008) != 0
|
|
130
|
+
offset += 2 # uniform scale
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
break unless (flags & 0x0020) != 0 # MORE_COMPONENTS
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
new_data
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def to_signed_16(value)
|
|
140
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Head strategy: recompute xMin/yMin/xMax/yMax (offsets 36–43) from
|
|
7
|
+
# the subset's actual glyphs. The source TTC's bbox covers every
|
|
8
|
+
# donor font in the collection and is far larger than any per-block
|
|
9
|
+
# subset needs; an inflated bbox makes browsers render glyphs at
|
|
10
|
+
# the wrong visual size. The remaining head fields pass through
|
|
11
|
+
# unchanged — checksumAdjustment is rewritten by FontWriter.
|
|
12
|
+
class Head
|
|
13
|
+
# @param context [SubsetContext]
|
|
14
|
+
# @param tag [String] "head"
|
|
15
|
+
# @param table [Head] parsed head table (unused)
|
|
16
|
+
# @return [String] binary head bytes for the subset
|
|
17
|
+
def self.call(context:, tag:, table:)
|
|
18
|
+
ensure_glyf_built!(context)
|
|
19
|
+
|
|
20
|
+
data = context.font.table_data["head"].dup
|
|
21
|
+
bbox = context.state.subset_bbox
|
|
22
|
+
return data unless bbox
|
|
23
|
+
|
|
24
|
+
x_min, y_min, x_max, y_max = bbox
|
|
25
|
+
data[36, 8] = [x_min, y_min, x_max, y_max].pack("n4")
|
|
26
|
+
data
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.ensure_glyf_built!(context)
|
|
30
|
+
return if context.state.glyf_data
|
|
31
|
+
|
|
32
|
+
builder = GlyfLocaBuilder.new(font: context.font,
|
|
33
|
+
mapping: context.mapping).build
|
|
34
|
+
context.state.glyf_data = builder.glyf_data
|
|
35
|
+
context.state.loca_offsets = builder.loca_offsets
|
|
36
|
+
context.state.subset_bbox = builder.bbox
|
|
37
|
+
end
|
|
38
|
+
private_class_method :ensure_glyf_built!
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Hhea strategy: rewrite numberOfHMetrics (offset 34, uint16) and
|
|
7
|
+
# advanceWidthMax (offset 10, uint16). The source TTC's
|
|
8
|
+
# advanceWidthMax covers every donor font and can be far larger
|
|
9
|
+
# than any per-block subset needs; recomputing from the subset's
|
|
10
|
+
# actual hmtx keeps the value honest.
|
|
11
|
+
#
|
|
12
|
+
# Hhea runs alphabetically before Hmtx, so the Hmtx strategy's
|
|
13
|
+
# SharedState cache hasn't been populated yet — we walk the source
|
|
14
|
+
# hmtx directly to find the max.
|
|
15
|
+
class Hhea
|
|
16
|
+
# @param context [SubsetContext]
|
|
17
|
+
# @param tag [String] "hhea"
|
|
18
|
+
# @param table [Hhea] parsed hhea table
|
|
19
|
+
# @return [String] binary hhea bytes for the subset
|
|
20
|
+
def self.call(context:, tag:, table:)
|
|
21
|
+
data = table.to_binary_s.dup
|
|
22
|
+
|
|
23
|
+
new_num_h_metrics = calculate_number_of_h_metrics(context, table)
|
|
24
|
+
data[34, 2] = [new_num_h_metrics].pack("n")
|
|
25
|
+
|
|
26
|
+
new_max = compute_subset_max_advance(context)
|
|
27
|
+
data[10, 2] = [new_max].pack("n") if new_max.positive?
|
|
28
|
+
|
|
29
|
+
data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.calculate_number_of_h_metrics(context, _table)
|
|
33
|
+
hmtx = context.font.table("hmtx")
|
|
34
|
+
if hmtx&.parsed? && hmtx.h_metrics
|
|
35
|
+
hmtx.h_metrics.size
|
|
36
|
+
else
|
|
37
|
+
context.mapping.size
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.compute_subset_max_advance(context)
|
|
42
|
+
hmtx = context.font.table("hmtx")
|
|
43
|
+
return 0 unless hmtx
|
|
44
|
+
|
|
45
|
+
unless hmtx.parsed?
|
|
46
|
+
hhea = context.font.table("hhea")
|
|
47
|
+
maxp = context.font.table("maxp")
|
|
48
|
+
hmtx.parse_with_context(hhea.number_of_h_metrics,
|
|
49
|
+
maxp.num_glyphs)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
max_advance = 0
|
|
53
|
+
context.mapping.old_ids.each do |old_id|
|
|
54
|
+
metric = hmtx.metric_for(old_id)
|
|
55
|
+
next unless metric
|
|
56
|
+
|
|
57
|
+
advance = metric[:advance_width]
|
|
58
|
+
max_advance = advance if advance && advance > max_advance
|
|
59
|
+
end
|
|
60
|
+
max_advance
|
|
61
|
+
end
|
|
62
|
+
private_class_method :calculate_number_of_h_metrics,
|
|
63
|
+
:compute_subset_max_advance
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Hmtx strategy: emit one LongHorMetric per glyph in the subset's
|
|
7
|
+
# glyph mapping, in the same order as the mapping. Also records
|
|
8
|
+
# the maximum advanceWidth into [SharedState] so Hhea (which is
|
|
9
|
+
# processed alphabetically before Hmtx) can read it.
|
|
10
|
+
#
|
|
11
|
+
# Tables are processed in profile order — Hhea comes before Hmtx
|
|
12
|
+
# alphabetically — so Hhea reads its max advance via a private
|
|
13
|
+
# helper that walks the source hmtx directly. Hmtx then stashes
|
|
14
|
+
# the value into SharedState for any later consumer.
|
|
15
|
+
class Hmtx
|
|
16
|
+
# @param context [SubsetContext]
|
|
17
|
+
# @param tag [String] "hmtx"
|
|
18
|
+
# @param table [Hmtx] parsed hmtx table
|
|
19
|
+
# @return [String] binary hmtx bytes for the subset
|
|
20
|
+
def self.call(context:, tag:, table:)
|
|
21
|
+
ensure_parsed!(context, table)
|
|
22
|
+
|
|
23
|
+
data = String.new(encoding: Encoding::BINARY)
|
|
24
|
+
max_advance = 0
|
|
25
|
+
|
|
26
|
+
context.mapping.old_ids.each do |old_id|
|
|
27
|
+
metric = table.metric_for(old_id)
|
|
28
|
+
next unless metric
|
|
29
|
+
|
|
30
|
+
advance = metric[:advance_width]
|
|
31
|
+
max_advance = advance if advance && advance > max_advance
|
|
32
|
+
data << [advance].pack("n")
|
|
33
|
+
data << [metric[:lsb]].pack("n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context.state.subset_max_advance = max_advance
|
|
37
|
+
data
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.ensure_parsed!(context, table)
|
|
41
|
+
return if table.parsed?
|
|
42
|
+
|
|
43
|
+
hhea = context.font.table("hhea")
|
|
44
|
+
maxp = context.font.table("maxp")
|
|
45
|
+
table.parse_with_context(hhea.number_of_h_metrics,
|
|
46
|
+
maxp.num_glyphs)
|
|
47
|
+
end
|
|
48
|
+
private_class_method :ensure_parsed!
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Loca strategy: emit one entry per subset glyph + a final offset,
|
|
7
|
+
# using short or long format based on the source head.index_to_loc_format.
|
|
8
|
+
# Glyph offsets come from [SharedState], populated earlier by the
|
|
9
|
+
# Glyf strategy. If Glyf hasn't run yet (e.g. direct invocation
|
|
10
|
+
# from a spec), this strategy triggers the build lazily.
|
|
11
|
+
class Loca
|
|
12
|
+
# @param context [SubsetContext]
|
|
13
|
+
# @param tag [String] "loca"
|
|
14
|
+
# @param table [Loca] parsed loca table (unused)
|
|
15
|
+
# @return [String] binary loca bytes for the subset
|
|
16
|
+
def self.call(context:, tag:, table:)
|
|
17
|
+
offsets = context.state.loca_offsets
|
|
18
|
+
if offsets.nil?
|
|
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
|
+
offsets = builder.loca_offsets
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
head = context.font.table("head")
|
|
28
|
+
data = String.new(encoding: Encoding::BINARY)
|
|
29
|
+
|
|
30
|
+
if head.index_to_loc_format.zero?
|
|
31
|
+
offsets.each { |o| data << [o / 2].pack("n") }
|
|
32
|
+
else
|
|
33
|
+
offsets.each { |o| data << [o].pack("N") }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
data
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Maxp strategy: rewrite numGlyphs (offset 4, uint16) to the
|
|
7
|
+
# subset's glyph count. The remaining maxp fields describe worst-
|
|
8
|
+
# case outline/instruction complexity and can be left as-is —
|
|
9
|
+
# a smaller subset cannot exceed the source's maxima.
|
|
10
|
+
class Maxp
|
|
11
|
+
# @param context [SubsetContext]
|
|
12
|
+
# @param tag [String] "maxp"
|
|
13
|
+
# @param table [Maxp] parsed maxp table
|
|
14
|
+
# @return [String] binary maxp bytes for the subset
|
|
15
|
+
def self.call(context:, tag:, table:)
|
|
16
|
+
data = table.to_binary_s.dup
|
|
17
|
+
data[4, 2] = [context.mapping.size].pack("n")
|
|
18
|
+
data
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Name strategy: name records don't reference glyph IDs, so the
|
|
7
|
+
# full source table is preserved.
|
|
8
|
+
class Name
|
|
9
|
+
# @param context [SubsetContext]
|
|
10
|
+
# @param tag [String] "name"
|
|
11
|
+
# @param table [Name] parsed name table (unused)
|
|
12
|
+
# @return [String] source name bytes verbatim
|
|
13
|
+
def self.call(context:, tag:, table:)
|
|
14
|
+
context.font.table_data[tag]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# OS/2 strategy: OS/2 currently passes through unchanged. Unicode
|
|
7
|
+
# range pruning is left as future work — the source OS/2 ranges
|
|
8
|
+
# remain valid (they describe the source character coverage, which
|
|
9
|
+
# is a superset of the subset's).
|
|
10
|
+
class Os2
|
|
11
|
+
# @param context [SubsetContext]
|
|
12
|
+
# @param tag [String] "OS/2"
|
|
13
|
+
# @param table [Os2] parsed OS/2 table (unused)
|
|
14
|
+
# @return [String] source OS/2 bytes verbatim
|
|
15
|
+
def self.call(context:, tag:, table:)
|
|
16
|
+
context.font.table_data[tag]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Default fallback for tags that have no dedicated strategy.
|
|
7
|
+
# Returns the source table bytes verbatim — used for tables that
|
|
8
|
+
# don't need any glyph-aware subsetting (e.g. name, OS/2).
|
|
9
|
+
class PassThrough
|
|
10
|
+
# @param context [SubsetContext]
|
|
11
|
+
# @param tag [String] table tag
|
|
12
|
+
# @param table [Object, nil] parsed table (unused)
|
|
13
|
+
# @return [String, nil] source binary bytes for `tag`, or nil
|
|
14
|
+
def self.call(context:, tag:, table:)
|
|
15
|
+
context.font.table_data[tag]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Subset
|
|
5
|
+
module TableStrategy
|
|
6
|
+
# Post strategy: if the `drop_names` option is set, rewrite as a
|
|
7
|
+
# version 3.0 post table (no glyph names). Otherwise pass through
|
|
8
|
+
# the source post bytes.
|
|
9
|
+
class Post
|
|
10
|
+
# @param context [SubsetContext]
|
|
11
|
+
# @param tag [String] "post"
|
|
12
|
+
# @param table [Post] parsed post table
|
|
13
|
+
# @return [String] binary post bytes for the subset
|
|
14
|
+
def self.call(context:, tag:, table:)
|
|
15
|
+
return build_v3(context) if context.options.drop_names
|
|
16
|
+
|
|
17
|
+
context.font.table_data["post"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.build_v3(context)
|
|
21
|
+
data = String.new(encoding: Encoding::BINARY)
|
|
22
|
+
data << [0x00030000].pack("N") # version 3.0
|
|
23
|
+
|
|
24
|
+
original = context.font.table_data["post"]
|
|
25
|
+
data << if original&.length && original.length >= 32
|
|
26
|
+
original[4, 28]
|
|
27
|
+
else
|
|
28
|
+
[0, 0, 0, 0, 0, 0, 0].pack("N7")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
data
|
|
32
|
+
end
|
|
33
|
+
private_class_method :build_v3
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Autoload hub for the Fontisan::Subset::TableStrategy namespace.
|
|
4
|
+
#
|
|
5
|
+
# Each table tag handled by the subsetter has a corresponding strategy
|
|
6
|
+
# class (e.g. `TableStrategy::Maxp` for the "maxp" tag). Strategies are
|
|
7
|
+
# stateless — they receive a [SubsetContext] + tag + table and return
|
|
8
|
+
# the subset table's binary bytes. Unknown tags fall back to
|
|
9
|
+
# [TableStrategy::PassThrough], which preserves the source bytes.
|
|
10
|
+
#
|
|
11
|
+
# Registering a new tag is a one-line change: add the autoload here and
|
|
12
|
+
# an entry in REGISTRY. No edits to TableSubsetter required (Open/Closed).
|
|
13
|
+
|
|
14
|
+
module Fontisan
|
|
15
|
+
module Subset
|
|
16
|
+
module TableStrategy
|
|
17
|
+
autoload :Cblc, "fontisan/subset/table_strategy/cblc"
|
|
18
|
+
autoload :Cbdt, "fontisan/subset/table_strategy/cbdt"
|
|
19
|
+
autoload :Cmap, "fontisan/subset/table_strategy/cmap"
|
|
20
|
+
autoload :ColorBitmapPlacement,
|
|
21
|
+
"fontisan/subset/table_strategy/color_bitmap_placement"
|
|
22
|
+
autoload :ColorBitmapStrikePlan,
|
|
23
|
+
"fontisan/subset/table_strategy/color_bitmap_strike_plan"
|
|
24
|
+
autoload :ColorBitmapSubsetPlan,
|
|
25
|
+
"fontisan/subset/table_strategy/color_bitmap_subset_plan"
|
|
26
|
+
autoload :ColorBitmapSubsetter,
|
|
27
|
+
"fontisan/subset/table_strategy/color_bitmap_subsetter"
|
|
28
|
+
autoload :ColorBitmapSubtablePlan,
|
|
29
|
+
"fontisan/subset/table_strategy/color_bitmap_subtable_plan"
|
|
30
|
+
autoload :Glyf, "fontisan/subset/table_strategy/glyf"
|
|
31
|
+
autoload :GlyfLocaBuilder,
|
|
32
|
+
"fontisan/subset/table_strategy/glyf_loca_builder"
|
|
33
|
+
autoload :Head, "fontisan/subset/table_strategy/head"
|
|
34
|
+
autoload :Hhea, "fontisan/subset/table_strategy/hhea"
|
|
35
|
+
autoload :Hmtx, "fontisan/subset/table_strategy/hmtx"
|
|
36
|
+
autoload :Loca, "fontisan/subset/table_strategy/loca"
|
|
37
|
+
autoload :Maxp, "fontisan/subset/table_strategy/maxp"
|
|
38
|
+
autoload :Name, "fontisan/subset/table_strategy/name"
|
|
39
|
+
autoload :Os2, "fontisan/subset/table_strategy/os2"
|
|
40
|
+
autoload :PassThrough, "fontisan/subset/table_strategy/pass_through"
|
|
41
|
+
autoload :Post, "fontisan/subset/table_strategy/post"
|
|
42
|
+
|
|
43
|
+
# Tag → strategy class name (resolved lazily via const_get to keep
|
|
44
|
+
# this file loadable in any order). Add new subsetting logic by
|
|
45
|
+
# adding one autoload + one entry here, plus a strategy file under
|
|
46
|
+
# `lib/fontisan/subset/table_strategy/`.
|
|
47
|
+
REGISTRY = {
|
|
48
|
+
"maxp" => :Maxp,
|
|
49
|
+
"hhea" => :Hhea,
|
|
50
|
+
"hmtx" => :Hmtx,
|
|
51
|
+
"loca" => :Loca,
|
|
52
|
+
"glyf" => :Glyf,
|
|
53
|
+
"cmap" => :Cmap,
|
|
54
|
+
"post" => :Post,
|
|
55
|
+
"name" => :Name,
|
|
56
|
+
"head" => :Head,
|
|
57
|
+
"OS/2" => :Os2,
|
|
58
|
+
"CBDT" => :Cbdt,
|
|
59
|
+
"CBLC" => :Cblc,
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# Resolve a table tag to its strategy class. Returns PassThrough
|
|
63
|
+
# for any tag without an explicit registration.
|
|
64
|
+
#
|
|
65
|
+
# @param tag [String] OpenType table tag (e.g. "maxp", "CBDT")
|
|
66
|
+
# @return [Class] a strategy class responding to `.call`
|
|
67
|
+
def self.for(tag)
|
|
68
|
+
const_name = REGISTRY[tag]
|
|
69
|
+
return PassThrough unless const_name
|
|
70
|
+
|
|
71
|
+
const_get(const_name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|