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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +10 -5
  3. data/lib/fontisan/config/subset_profiles.yml +2 -0
  4. data/lib/fontisan/stitcher/cbdt_propagator.rb +173 -0
  5. data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
  6. data/lib/fontisan/stitcher/glyph_copier.rb +109 -0
  7. data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
  8. data/lib/fontisan/stitcher.rb +14 -159
  9. data/lib/fontisan/subset/shared_state.rb +42 -0
  10. data/lib/fontisan/subset/subset_context.rb +20 -0
  11. data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
  12. data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
  13. data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
  14. data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
  15. data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
  16. data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
  17. data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
  18. data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
  19. data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
  20. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +145 -0
  21. data/lib/fontisan/subset/table_strategy/head.rb +42 -0
  22. data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
  23. data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
  24. data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
  25. data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
  26. data/lib/fontisan/subset/table_strategy/name.rb +19 -0
  27. data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
  28. data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
  29. data/lib/fontisan/subset/table_strategy/post.rb +37 -0
  30. data/lib/fontisan/subset/table_strategy.rb +75 -0
  31. data/lib/fontisan/subset/table_subsetter.rb +49 -616
  32. data/lib/fontisan/subset.rb +3 -0
  33. data/lib/fontisan/tables/cbdt.rb +66 -121
  34. data/lib/fontisan/tables/cblc.rb +110 -231
  35. data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
  36. data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
  37. data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
  38. data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
  39. data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
  40. data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
  41. data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
  42. data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
  43. data/lib/fontisan/tables.rb +11 -0
  44. data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
  45. data/lib/fontisan/ufo/layer.rb +42 -2
  46. data/lib/fontisan/ufo.rb +2 -1
  47. data/lib/fontisan/version.rb +1 -1
  48. metadata +37 -2
@@ -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
@@ -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