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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +240 -24
  3. data/Gemfile +6 -0
  4. data/Rakefile +9 -6
  5. data/benchmark/compile_benchmark.rb +13 -5
  6. data/docs/.vitepress/config.ts +1 -0
  7. data/docs/STITCHER_GUIDE.adoc +47 -3
  8. data/docs/api/layer.md +89 -0
  9. data/docs/cli/subset.md +25 -6
  10. data/docs/guide/cli/subset.md +31 -0
  11. data/docs/guide/color-fonts/bitmaps.md +17 -0
  12. data/lib/fontisan/cli.rb +2 -1
  13. data/lib/fontisan/collection/writer.rb +2 -1
  14. data/lib/fontisan/commands/validate_collection_command.rb +7 -1
  15. data/lib/fontisan/commands.rb +2 -1
  16. data/lib/fontisan/config/subset_profiles.yml +2 -0
  17. data/lib/fontisan/converters/collection_converter.rb +6 -1
  18. data/lib/fontisan/converters/svg_generator.rb +3 -1
  19. data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
  20. data/lib/fontisan/stitcher/collection_result.rb +2 -1
  21. data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
  22. data/lib/fontisan/stitcher/glyph_copier.rb +5 -34
  23. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
  24. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
  25. data/lib/fontisan/stitcher/source.rb +2 -1
  26. data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
  27. data/lib/fontisan/stitcher.rb +4 -1
  28. data/lib/fontisan/subset/shared_state.rb +42 -0
  29. data/lib/fontisan/subset/subset_context.rb +20 -0
  30. data/lib/fontisan/subset/table_strategy/cbdt.rb +30 -0
  31. data/lib/fontisan/subset/table_strategy/cblc.rb +27 -0
  32. data/lib/fontisan/subset/table_strategy/cmap.rb +146 -0
  33. data/lib/fontisan/subset/table_strategy/color_bitmap_placement.rb +20 -0
  34. data/lib/fontisan/subset/table_strategy/color_bitmap_strike_plan.rb +45 -0
  35. data/lib/fontisan/subset/table_strategy/color_bitmap_subset_plan.rb +62 -0
  36. data/lib/fontisan/subset/table_strategy/color_bitmap_subsetter.rb +182 -0
  37. data/lib/fontisan/subset/table_strategy/color_bitmap_subtable_plan.rb +59 -0
  38. data/lib/fontisan/subset/table_strategy/glyf.rb +29 -0
  39. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +146 -0
  40. data/lib/fontisan/subset/table_strategy/head.rb +42 -0
  41. data/lib/fontisan/subset/table_strategy/hhea.rb +67 -0
  42. data/lib/fontisan/subset/table_strategy/hmtx.rb +52 -0
  43. data/lib/fontisan/subset/table_strategy/loca.rb +41 -0
  44. data/lib/fontisan/subset/table_strategy/maxp.rb +23 -0
  45. data/lib/fontisan/subset/table_strategy/name.rb +19 -0
  46. data/lib/fontisan/subset/table_strategy/os2.rb +21 -0
  47. data/lib/fontisan/subset/table_strategy/pass_through.rb +20 -0
  48. data/lib/fontisan/subset/table_strategy/post.rb +37 -0
  49. data/lib/fontisan/subset/table_strategy.rb +75 -0
  50. data/lib/fontisan/subset/table_subsetter.rb +49 -616
  51. data/lib/fontisan/subset.rb +3 -0
  52. data/lib/fontisan/svg/standalone_glyph.rb +4 -1
  53. data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
  54. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
  55. data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
  56. data/lib/fontisan/tables/cbdt.rb +66 -121
  57. data/lib/fontisan/tables/cblc.rb +110 -231
  58. data/lib/fontisan/tables/cblc_big_glyph_metrics.rb +28 -0
  59. data/lib/fontisan/tables/cblc_bitmap_size.rb +67 -0
  60. data/lib/fontisan/tables/cblc_glyph_bitmap_location.rb +28 -0
  61. data/lib/fontisan/tables/cblc_index_subtable.rb +104 -0
  62. data/lib/fontisan/tables/cblc_index_subtable_array_entry.rb +20 -0
  63. data/lib/fontisan/tables/cblc_index_subtable_format_parser.rb +150 -0
  64. data/lib/fontisan/tables/cblc_index_subtable_header.rb +18 -0
  65. data/lib/fontisan/tables/cblc_sbit_line_metrics.rb +28 -0
  66. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
  67. data/lib/fontisan/tables/cff.rb +2 -1
  68. data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
  69. data/lib/fontisan/tables.rb +12 -0
  70. data/lib/fontisan/ufo/cli.rb +4 -1
  71. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
  72. data/lib/fontisan/ufo/compile/cff.rb +6 -2
  73. data/lib/fontisan/ufo/compile/cmap.rb +11 -5
  74. data/lib/fontisan/ufo/compile/cpal.rb +2 -1
  75. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
  76. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
  77. data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
  78. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
  79. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
  80. data/lib/fontisan/ufo/compile/fvar.rb +7 -3
  81. data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
  82. data/lib/fontisan/ufo/compile/gvar.rb +5 -2
  83. data/lib/fontisan/ufo/compile/head.rb +2 -1
  84. data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
  85. data/lib/fontisan/ufo/compile/name.rb +3 -1
  86. data/lib/fontisan/ufo/compile/sbix.rb +2 -1
  87. data/lib/fontisan/ufo/compile/stat.rb +6 -3
  88. data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
  89. data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
  90. data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
  91. data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
  92. data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
  93. data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
  94. data/lib/fontisan/ufo/convert.rb +4 -1
  95. data/lib/fontisan/ufo/glyph_exists_error.rb +23 -0
  96. data/lib/fontisan/ufo/info.rb +4 -2
  97. data/lib/fontisan/ufo/layer.rb +42 -2
  98. data/lib/fontisan/ufo/plist.rb +7 -2
  99. data/lib/fontisan/ufo/reader.rb +3 -1
  100. data/lib/fontisan/ufo/writer.rb +13 -5
  101. data/lib/fontisan/ufo.rb +2 -1
  102. data/lib/fontisan/version.rb +1 -1
  103. data/lib/fontisan/woff2/collection_decoder.rb +4 -1
  104. data/lib/fontisan/woff2/collection_encoder.rb +4 -1
  105. data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
  106. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
  107. data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
  108. data/lib/fontisan.rb +0 -1
  109. metadata +36 -4
  110. data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
  111. data/lib/fontisan/tasks.rb +0 -11
@@ -1,291 +1,170 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
4
-
5
3
  module Fontisan
6
4
  module Tables
7
- # CBLC (Color Bitmap Location) table parser
8
- #
9
- # The CBLC table contains location information for bitmap glyphs at various
10
- # sizes (strikes). It works together with the CBDT table which contains the
11
- # actual bitmap data.
5
+ # CBLC (Color Bitmap Location) table.
12
6
  #
13
- # CBLC Table Structure:
14
- # ```
15
- # CBLC Table = Header (8 bytes)
16
- # + BitmapSize Records (48 bytes each)
17
- # ```
7
+ # CBLC indexes glyph IDs into CBDT byte offsets across one or more
8
+ # bitmap strikes (each strike = a ppem + bit depth combination). For
9
+ # each strike, a list of IndexSubTable records describes how to locate
10
+ # the bitmap of each glyph in a contiguous glyph range.
18
11
  #
19
- # Header (8 bytes):
20
- # - version (uint32): Table version (0x00020000 or 0x00030000)
21
- # - numSizes (uint32): Number of BitmapSize records
12
+ # Layout:
13
+ # uint32 version (0x00020000 or 0x00030000)
14
+ # uint32 num_sizes (number of BitmapSize records)
15
+ # CblcBitmapSize[num_sizes] (48 bytes each)
16
+ # IndexSubTableArray entries (8 bytes each, per BitmapSize)
17
+ # IndexSubTable records (variable, format-specific)
22
18
  #
23
- # Each BitmapSize record (48 bytes) contains:
24
- # - indexSubTableArrayOffset (uint32): Offset to index subtable array
25
- # - indexTablesSize (uint32): Size of index subtables
26
- # - numberOfIndexSubTables (uint32): Number of index subtables
27
- # - colorRef (uint32): Not used, set to 0
28
- # - hori (SbitLineMetrics, 12 bytes): Horizontal line metrics
29
- # - vert (SbitLineMetrics, 12 bytes): Vertical line metrics
30
- # - startGlyphIndex (uint16): First glyph ID in strike
31
- # - endGlyphIndex (uint16): Last glyph ID in strike
32
- # - ppemX (uint8): Horizontal pixels per em
33
- # - ppemY (uint8): Vertical pixels per em
34
- # - bitDepth (uint8): Bit depth (1, 2, 4, 8, 32)
35
- # - flags (int8): Flags
19
+ # This model parses the header + BitmapSize records via BinData, then
20
+ # lazily walks the IndexSubTableArray / IndexSubTable structures on
21
+ # demand so callers can iterate [(gid, strike) CBDT location] pairs.
36
22
  #
37
- # Reference: OpenType CBLC specification
38
- # https://docs.microsoft.com/en-us/typography/opentype/spec/cblc
23
+ # Reference: OpenType CBLC specification.
24
+ # https://learn.microsoft.com/en-us/typography/opentype/spec/cblc
39
25
  #
40
- # @example Reading a CBLC table
41
- # data = font.table_data['CBLC']
42
- # cblc = Fontisan::Tables::Cblc.read(data)
43
- # strikes = cblc.strikes
44
- # puts "Font has #{strikes.length} bitmap strikes"
26
+ # @example Enumerate every glyph's CBDT bitmap location
27
+ # cblc = Fontisan::Tables::Cblc.read(font.table_data["CBLC"])
28
+ # cblc.each_glyph_location do |loc|
29
+ # puts "gid=#{loc.glyph_id} cbdt_offset=#{loc.cbdt_offset}"
30
+ # end
45
31
  class Cblc < Binary::BaseRecord
46
- # OpenType table tag for CBLC
47
32
  TAG = "CBLC"
48
33
 
49
- # Supported CBLC versions
34
+ # CBLC v2.0 — original release
50
35
  VERSION_2_0 = 0x00020000
36
+ # CBLC v3.0 — adds PNG image format support
51
37
  VERSION_3_0 = 0x00030000
52
38
 
53
- # SbitLineMetrics structure (12 bytes)
54
- #
55
- # Contains metrics for horizontal or vertical layout
56
- class SbitLineMetrics < Binary::BaseRecord
57
- endian :big
58
- int8 :ascender
59
- int8 :descender
60
- uint8 :width_max
61
- int8 :caret_slope_numerator
62
- int8 :caret_slope_denominator
63
- int8 :caret_offset
64
- int8 :min_origin_sb
65
- int8 :min_advance_sb
66
- int8 :max_before_bl
67
- int8 :min_after_bl
68
- int8 :pad1
69
- int8 :pad2
70
- end
39
+ uint32 :version
40
+ uint32 :num_sizes
41
+ array :bitmap_sizes,
42
+ type: Fontisan::Tables::CblcBitmapSize,
43
+ initial_length: :num_sizes
71
44
 
72
- # BitmapSize record structure (48 bytes)
45
+ # Iterate every (glyph_id, strike) → CblcGlyphBitmapLocation pair
46
+ # across all strikes. Each strike's IndexSubTables are walked in
47
+ # turn so the caller sees every glyph's CBDT location exactly once
48
+ # per strike.
73
49
  #
74
- # Describes a bitmap strike at a specific ppem size
75
- class BitmapSize < Binary::BaseRecord
76
- endian :big
77
- uint32 :index_subtable_array_offset
78
- uint32 :index_tables_size
79
- uint32 :number_of_index_subtables
80
- uint32 :color_ref
81
-
82
- # Read the SbitLineMetrics structures manually
83
- def self.read(io)
84
- data = io.is_a?(String) ? io : io.read
85
- size = new
86
-
87
- io = StringIO.new(data)
88
- size.instance_variable_set(:@index_subtable_array_offset,
89
- io.read(4).unpack1("N"))
90
- size.instance_variable_set(:@index_tables_size,
91
- io.read(4).unpack1("N"))
92
- size.instance_variable_set(:@number_of_index_subtables,
93
- io.read(4).unpack1("N"))
94
- size.instance_variable_set(:@color_ref, io.read(4).unpack1("N"))
95
-
96
- # Parse hori and vert metrics (12 bytes each)
97
- hori_data = io.read(12)
98
- vert_data = io.read(12)
99
- size.instance_variable_set(:@hori, SbitLineMetrics.read(hori_data))
100
- size.instance_variable_set(:@vert, SbitLineMetrics.read(vert_data))
101
-
102
- # Parse remaining fields
103
- size.instance_variable_set(:@start_glyph_index,
104
- io.read(2).unpack1("n"))
105
- size.instance_variable_set(:@end_glyph_index, io.read(2).unpack1("n"))
106
- size.instance_variable_set(:@ppem_x, io.read(1).unpack1("C"))
107
- size.instance_variable_set(:@ppem_y, io.read(1).unpack1("C"))
108
- size.instance_variable_set(:@bit_depth, io.read(1).unpack1("C"))
109
- size.instance_variable_set(:@flags, io.read(1).unpack1("c"))
110
-
111
- size
112
- end
113
-
114
- attr_reader :index_subtable_array_offset, :index_tables_size,
115
- :number_of_index_subtables, :color_ref, :hori, :vert,
116
- :start_glyph_index, :end_glyph_index, :ppem_x, :ppem_y,
117
- :bit_depth, :flags
118
-
119
- # Get ppem size (assumes square pixels)
120
- #
121
- # @return [Integer] Pixels per em
122
- def ppem
123
- ppem_x
124
- end
125
-
126
- # Get glyph range for this strike
127
- #
128
- # @return [Range] Range of glyph IDs
129
- def glyph_range
130
- start_glyph_index..end_glyph_index
131
- end
50
+ # @yield [CblcGlyphBitmapLocation]
51
+ # @return [Enumerator] if no block given
52
+ def each_glyph_location(&block)
53
+ return enum_for(:each_glyph_location) unless block
132
54
 
133
- # Check if this strike includes a specific glyph ID
134
- #
135
- # @param glyph_id [Integer] Glyph ID to check
136
- # @return [Boolean] True if glyph is in range
137
- def includes_glyph?(glyph_id)
138
- glyph_range.include?(glyph_id)
55
+ index_sub_tables.each do |sub|
56
+ sub.locations.each(&block)
139
57
  end
140
58
  end
141
59
 
142
- # @return [Integer] CBLC version
143
- attr_reader :version
144
-
145
- # @return [Integer] Number of bitmap size records
146
- attr_reader :num_sizes
147
-
148
- # @return [Array<BitmapSize>] Parsed bitmap size records
149
- attr_reader :bitmap_sizes
150
-
151
- # @return [String] Raw binary data for the entire CBLC table
152
- attr_reader :raw_data
153
-
154
- # Override read to parse CBLC structure
155
- #
156
- # @param io [IO, String] Binary data to read
157
- # @return [Cblc] Parsed CBLC table
158
- def self.read(io)
159
- cblc = new
160
- return cblc if io.nil?
161
-
162
- data = io.is_a?(String) ? io : io.read
163
- cblc.parse!(data)
164
- cblc
165
- end
166
-
167
- # Parse the CBLC table structure
60
+ # All IndexSubTable records across every strike.
168
61
  #
169
- # @param data [String] Binary data for the CBLC table
170
- # @raise [CorruptedTableError] If CBLC structure is invalid
171
- def parse!(data)
172
- @raw_data = data
173
- io = StringIO.new(data)
174
-
175
- # Parse CBLC header (8 bytes)
176
- parse_header(io)
177
- validate_header!
178
-
179
- # Parse bitmap size records
180
- parse_bitmap_sizes(io)
181
- rescue StandardError => e
182
- raise CorruptedTableError, "Failed to parse CBLC table: #{e.message}"
62
+ # @return [Array<CblcIndexSubTable>]
63
+ def index_sub_tables
64
+ @index_sub_tables ||= bitmap_sizes.flat_map { |s| sub_tables_for(s) }
183
65
  end
184
66
 
185
- # Get bitmap strikes (sizes)
67
+ # Locate a glyph's CBDT bitmap at a specific ppem.
186
68
  #
187
- # @return [Array<BitmapSize>] Array of bitmap strikes
188
- def strikes
189
- bitmap_sizes || []
190
- end
69
+ # @param glyph_id [Integer] source glyph ID
70
+ # @param ppem [Integer] strike ppem
71
+ # @return [CblcGlyphBitmapLocation, nil]
72
+ def bitmap_offset_for_gid(glyph_id, ppem)
73
+ strike = bitmap_sizes.find do |s|
74
+ s.ppem == ppem && s.includes_glyph?(glyph_id)
75
+ end
76
+ return nil unless strike
191
77
 
192
- # Get strikes for specific ppem size
193
- #
194
- # @param ppem [Integer] Pixels per em
195
- # @return [Array<BitmapSize>] Strikes matching ppem
196
- def strikes_for_ppem(ppem)
197
- strikes.select { |size| size.ppem == ppem }
78
+ sub_tables_for(strike).each do |sub|
79
+ loc = sub.locations.find { |l| l.glyph_id == glyph_id }
80
+ return loc if loc
81
+ end
82
+ nil
198
83
  end
199
84
 
200
- # Check if glyph has bitmap at ppem size
85
+ # Iterate every IndexSubTable in a single strike.
201
86
  #
202
- # @param glyph_id [Integer] Glyph ID
203
- # @param ppem [Integer] Pixels per em
204
- # @return [Boolean] True if glyph has bitmap
205
- def has_bitmap_for_glyph?(glyph_id, ppem)
206
- strikes_for_ppem(ppem).any? do |strike|
207
- strike.includes_glyph?(glyph_id)
87
+ # @param strike [CblcBitmapSize]
88
+ # @return [Array<CblcIndexSubTable>]
89
+ def sub_tables_for(strike)
90
+ return [] if strike.number_of_index_subtables.zero?
91
+
92
+ array_offset = strike.index_subtable_array_offset
93
+ entries = read_index_subtable_array(strike, array_offset)
94
+
95
+ entries.map do |entry|
96
+ absolute = array_offset + entry.additional_offset_to_index_subtable
97
+ CblcIndexSubTable.parse(
98
+ raw_data,
99
+ index_subtable_offset: absolute,
100
+ first_glyph_index: entry.first_glyph_index,
101
+ last_glyph_index: entry.last_glyph_index,
102
+ )
208
103
  end
209
104
  end
210
105
 
211
- # Get all available ppem sizes
106
+ # All available ppem sizes across every strike.
212
107
  #
213
- # @return [Array<Integer>] Sorted array of ppem sizes
108
+ # @return [Array<Integer>]
214
109
  def ppem_sizes
215
- strikes.map(&:ppem).uniq.sort
110
+ bitmap_sizes.map(&:ppem).uniq.sort
216
111
  end
217
112
 
218
- # Get all glyph IDs that have bitmaps across all strikes
113
+ # Number of bitmap strikes.
219
114
  #
220
- # @return [Array<Integer>] Array of glyph IDs
221
- def glyph_ids_with_bitmaps
222
- strikes.flat_map { |strike| strike.glyph_range.to_a }.uniq.sort
115
+ # @return [Integer]
116
+ def num_strikes
117
+ num_sizes
223
118
  end
224
119
 
225
- # Get strikes that include a specific glyph ID
120
+ # Strikes that cover a given glyph ID.
226
121
  #
227
- # @param glyph_id [Integer] Glyph ID
228
- # @return [Array<BitmapSize>] Strikes containing glyph
122
+ # @param glyph_id [Integer]
123
+ # @return [Array<CblcBitmapSize>]
229
124
  def strikes_for_glyph(glyph_id)
230
- strikes.select { |strike| strike.includes_glyph?(glyph_id) }
125
+ bitmap_sizes.select { |s| s.includes_glyph?(glyph_id) }
231
126
  end
232
127
 
233
- # Get the number of bitmap strikes
128
+ # Strikes that match a specific ppem.
234
129
  #
235
- # @return [Integer] Number of strikes
236
- def num_strikes
237
- num_sizes || 0
130
+ # @param ppem [Integer]
131
+ # @return [Array<CblcBitmapSize>]
132
+ def strikes_for_ppem(ppem)
133
+ bitmap_sizes.select { |s| s.ppem == ppem }
134
+ end
135
+
136
+ # All glyph IDs that have any bitmap across every strike.
137
+ #
138
+ # @return [Array<Integer>]
139
+ def glyph_ids_with_bitmaps
140
+ bitmap_sizes.flat_map { |s| s.glyph_range.to_a }.uniq.sort
238
141
  end
239
142
 
240
- # Validate the CBLC table structure
143
+ # Whether the CBLC header is one fontisan understands and at least
144
+ # the header fields parsed successfully.
241
145
  #
242
- # @return [Boolean] True if valid
146
+ # @return [Boolean]
243
147
  def valid?
244
148
  return false if version.nil?
245
149
  return false unless [VERSION_2_0, VERSION_3_0].include?(version)
246
- return false if num_sizes.nil? || num_sizes.negative?
247
- return false unless bitmap_sizes
248
150
 
249
151
  true
250
152
  end
251
153
 
252
154
  private
253
155
 
254
- # Parse CBLC header (8 bytes)
255
- #
256
- # @param io [StringIO] Input stream
257
- def parse_header(io)
258
- @version = io.read(4).unpack1("N")
259
- @num_sizes = io.read(4).unpack1("N")
260
- end
261
-
262
- # Validate header values
263
- #
264
- # @raise [CorruptedTableError] If validation fails
265
- def validate_header!
266
- unless [VERSION_2_0, VERSION_3_0].include?(version)
267
- raise CorruptedTableError,
268
- "Unsupported CBLC version: 0x#{version.to_s(16).upcase} " \
269
- "(only versions 2.0 and 3.0 supported)"
270
- end
271
-
272
- if num_sizes.negative?
273
- raise CorruptedTableError,
274
- "Invalid numSizes: #{num_sizes}"
275
- end
276
- end
277
-
278
- # Parse bitmap size records
156
+ # Read the IndexSubTableArray entries for one strike.
279
157
  #
280
- # @param io [StringIO] Input stream
281
- def parse_bitmap_sizes(io)
282
- @bitmap_sizes = []
283
- return if num_sizes.zero?
284
-
285
- # Each BitmapSize record is 48 bytes
286
- num_sizes.times do
287
- size_data = io.read(48)
288
- @bitmap_sizes << BitmapSize.read(size_data)
158
+ # @param strike [CblcBitmapSize]
159
+ # @param array_offset [Integer] absolute offset within CBLC bytes
160
+ # @return [Array<CblcIndexSubTableArrayEntry>]
161
+ def read_index_subtable_array(strike, array_offset)
162
+ count = strike.number_of_index_subtables
163
+ bytes = raw_data[array_offset, count * 8]
164
+ return [] unless bytes
165
+
166
+ Array.new(count) do |i|
167
+ CblcIndexSubTableArrayEntry.read(bytes[i * 8, 8])
289
168
  end
290
169
  end
291
170
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # 8-byte bigGlyphMetrics record embedded in CBLC IndexSubTable formats
6
+ # 2 and 5 (constant-metrics strikes). Layout is smallGlyphMetrics (5
7
+ # bytes) plus four additional signed advance fields.
8
+ #
9
+ # uint8 height
10
+ # uint8 width
11
+ # int8 bearing_x
12
+ # int8 bearing_y
13
+ # int8 advance
14
+ # int8 reserved
15
+ # int8 advance_lsb
16
+ # int8 advance_rsb
17
+ class CblcBigGlyphMetrics < Binary::BaseRecord
18
+ uint8 :height
19
+ uint8 :width
20
+ int8 :bearing_x
21
+ int8 :bearing_y
22
+ int8 :advance
23
+ int8 :reserved
24
+ int8 :advance_lsb
25
+ int8 :advance_rsb
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Fontisan
6
+ module Tables
7
+ # BitmapSize record embedded in a CBLC table (48 bytes).
8
+ #
9
+ # Each BitmapSize describes one bitmap strike: a glyph range rendered
10
+ # at a specific ppem with a specific bit depth. It points (via
11
+ # `index_subtable_array_offset`) at a sequence of IndexSubTableArray
12
+ # records that further describe how to find the bitmap data for each
13
+ # glyph in CBDT.
14
+ #
15
+ # Layout (48 bytes):
16
+ # uint32 index_subtable_array_offset
17
+ # uint32 index_tables_size
18
+ # uint32 number_of_index_subtables
19
+ # uint32 color_ref
20
+ # CblcSbitLineMetrics hori (12 bytes)
21
+ # CblcSbitLineMetrics vert (12 bytes)
22
+ # uint16 start_glyph_index
23
+ # uint16 end_glyph_index
24
+ # uint8 ppem_x
25
+ # uint8 ppem_y
26
+ # uint8 bit_depth
27
+ # int8 flags
28
+ #
29
+ # Reference: OpenType CBLC spec, "BitmapSize" record.
30
+ class CblcBitmapSize < Binary::BaseRecord
31
+ uint32 :index_subtable_array_offset
32
+ uint32 :index_tables_size
33
+ uint32 :number_of_index_subtables
34
+ uint32 :color_ref
35
+ CblcSbitLineMetrics :hori, type: Fontisan::Tables::CblcSbitLineMetrics
36
+ CblcSbitLineMetrics :vert, type: Fontisan::Tables::CblcSbitLineMetrics
37
+ uint16 :start_glyph_index
38
+ uint16 :end_glyph_index
39
+ uint8 :ppem_x
40
+ uint8 :ppem_y
41
+ uint8 :bit_depth
42
+ int8 :flags
43
+
44
+ # Pixels per em, assuming square pixels (the common case).
45
+ #
46
+ # @return [Integer] ppem value
47
+ def ppem
48
+ ppem_x
49
+ end
50
+
51
+ # Glyph ID range covered by this strike.
52
+ #
53
+ # @return [Range<Integer>] inclusive glyph ID range
54
+ def glyph_range
55
+ start_glyph_index..end_glyph_index
56
+ end
57
+
58
+ # Whether the strike covers a specific glyph.
59
+ #
60
+ # @param glyph_id [Integer] source glyph ID
61
+ # @return [Boolean]
62
+ def includes_glyph?(glyph_id)
63
+ glyph_range.include?(glyph_id)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Immutable value object locating one glyph's bitmap inside a CBDT table.
6
+ #
7
+ # A CblcIndexSubTable produces one of these per glyph in its range.
8
+ # The subsetting pipeline reads them to know which CBDT byte range to
9
+ # retain, then computes fresh offsets for the subset output.
10
+ #
11
+ # @!attribute glyph_id
12
+ # @return [Integer] source glyph ID
13
+ # @!attribute image_format
14
+ # @return [Integer] CBDT image format (e.g. 17, 18, 19)
15
+ # @!attribute cbdt_offset
16
+ # @return [Integer] absolute byte offset within the source CBDT
17
+ # @!attribute byte_length
18
+ # @return [Integer] length of the bitmap block in CBDT
19
+ CblcGlyphBitmapLocation = Struct.new(:glyph_id, :image_format,
20
+ :cbdt_offset, :byte_length,
21
+ keyword_init: true) do
22
+ def initialize(*)
23
+ super
24
+ freeze
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Parsed IndexSubTable from a CBLC table.
6
+ #
7
+ # The IndexSubTable is the format-specific directory describing how to
8
+ # locate each glyph's bitmap inside CBDT. Five formats are defined by
9
+ # the OpenType spec:
10
+ #
11
+ # 1 — variable metrics, variable size (uint32 offset array)
12
+ # 2 — constant metrics, constant size (single imageSize for the range)
13
+ # 3 — variable metrics, variable size, 4-byte-aligned (uint16 offsets × 4)
14
+ # 4 — variable metrics, variable size, bit-packed offsets (rare)
15
+ # 5 — constant metrics, constant size, with explicit uint32 offsetArray
16
+ #
17
+ # Each parsed IndexSubTable exposes one [CblcGlyphBitmapLocation] per
18
+ # glyph in its range, plus the raw bytes so a subsetter can preserve
19
+ # unchanged subtables verbatim if needed.
20
+ #
21
+ # Format-specific parsing is delegated to
22
+ # [CblcIndexSubTableFormatParser]; this class only owns the data model.
23
+ #
24
+ # Reference: OpenType CBLC spec, IndexSubTable formats 1–5.
25
+ class CblcIndexSubTable
26
+ # @return [Integer] indexFormat field (1, 2, 3, or 5)
27
+ attr_reader :index_format
28
+
29
+ # @return [Integer] imageFormat field (e.g. 17 = small metrics + PNG)
30
+ attr_reader :image_format
31
+
32
+ # @return [Integer] absolute imageDataOffset from start of CBDT
33
+ attr_reader :image_data_offset
34
+
35
+ # @return [Integer] first glyph ID covered by this subtable
36
+ attr_reader :first_glyph_index
37
+
38
+ # @return [Integer] last glyph ID covered by this subtable
39
+ attr_reader :last_glyph_index
40
+
41
+ # @return [Array<CblcGlyphBitmapLocation>] one per glyph in range
42
+ attr_reader :locations
43
+
44
+ # @return [String] raw bytes of this IndexSubTable in the source CBLC
45
+ attr_reader :raw_bytes
46
+
47
+ def initialize(index_format:, image_format:, image_data_offset:,
48
+ first_glyph_index:, last_glyph_index:, locations:,
49
+ raw_bytes:)
50
+ @index_format = index_format
51
+ @image_format = image_format
52
+ @image_data_offset = image_data_offset
53
+ @first_glyph_index = first_glyph_index
54
+ @last_glyph_index = last_glyph_index
55
+ @locations = locations
56
+ @raw_bytes = raw_bytes
57
+ end
58
+
59
+ # Parse an IndexSubTable from a CBLC binary blob.
60
+ #
61
+ # @param cblc_bytes [String] the full CBLC table bytes
62
+ # @param index_subtable_offset [Integer] absolute byte offset within
63
+ # `cblc_bytes` where this IndexSubTable begins
64
+ # @param first_glyph_index [Integer] first glyph ID covered
65
+ # @param last_glyph_index [Integer] last glyph ID covered
66
+ # @return [CblcIndexSubTable]
67
+ def self.parse(cblc_bytes, index_subtable_offset:, first_glyph_index:,
68
+ last_glyph_index:)
69
+ header = CblcIndexSubTableHeader.read(
70
+ cblc_bytes[index_subtable_offset, 8],
71
+ )
72
+
73
+ new(
74
+ index_format: header.index_format,
75
+ image_format: header.image_format,
76
+ image_data_offset: header.image_data_offset,
77
+ first_glyph_index: first_glyph_index,
78
+ last_glyph_index: last_glyph_index,
79
+ locations: CblcIndexSubTableFormatParser.locations(
80
+ header: header,
81
+ bytes: cblc_bytes,
82
+ base: index_subtable_offset,
83
+ first: first_glyph_index,
84
+ last: last_glyph_index,
85
+ ),
86
+ raw_bytes: CblcIndexSubTableFormatParser.raw_bytes(
87
+ header: header,
88
+ bytes: cblc_bytes,
89
+ base: index_subtable_offset,
90
+ first: first_glyph_index,
91
+ last: last_glyph_index,
92
+ ),
93
+ )
94
+ end
95
+
96
+ # Number of glyphs covered by this subtable.
97
+ #
98
+ # @return [Integer]
99
+ def glyph_count
100
+ last_glyph_index - first_glyph_index + 1
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fontisan
4
+ module Tables
5
+ # Entry in a CBLC IndexSubTableArray (8 bytes).
6
+ #
7
+ # The IndexSubTableArray sits at `BitmapSize#index_subtable_array_offset`
8
+ # (relative to the start of CBLC) and contains `number_of_index_subtables`
9
+ # of these entries. Each entry points (via `additional_offset_to_index_subtable`,
10
+ # relative to the IndexSubTableArray's own location) at an IndexSubTable
11
+ # record describing how to find the bitmaps for a glyph range.
12
+ #
13
+ # Reference: OpenType CBLC spec, IndexSubTableArray record.
14
+ class CblcIndexSubTableArrayEntry < Binary::BaseRecord
15
+ uint16 :first_glyph_index
16
+ uint16 :last_glyph_index
17
+ uint32 :additional_offset_to_index_subtable
18
+ end
19
+ end
20
+ end