fontisan 0.4.22 → 0.4.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +5 -0
  3. data/lib/fontisan/config/subset_profiles.yml +2 -0
  4. data/lib/fontisan/stitcher/cbdt_propagator.rb +44 -22
  5. data/lib/fontisan/stitcher/glyph_cloner.rb +41 -0
  6. data/lib/fontisan/stitcher/glyph_copier.rb +3 -33
  7. data/lib/fontisan/stitcher/unique_glyph_name.rb +46 -0
  8. data/lib/fontisan/stitcher.rb +2 -0
  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 +35 -2
@@ -8,6 +8,9 @@ module Fontisan
8
8
  autoload :GlyphMapping, "fontisan/subset/glyph_mapping"
9
9
  autoload :Options, "fontisan/subset/options"
10
10
  autoload :Profile, "fontisan/subset/profile"
11
+ autoload :SharedState, "fontisan/subset/shared_state"
12
+ autoload :SubsetContext, "fontisan/subset/subset_context"
13
+ autoload :TableStrategy, "fontisan/subset/table_strategy"
11
14
  autoload :TableSubsetter, "fontisan/subset/table_subsetter"
12
15
  end
13
16
  end
@@ -1,167 +1,112 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "stringio"
4
-
5
3
  module Fontisan
6
4
  module Tables
7
- # CBDT (Color Bitmap Data) table parser
8
- #
9
- # The CBDT table contains the actual bitmap data for color glyphs. It works
10
- # together with the CBLC table which provides the location information for
11
- # finding bitmaps in this table.
12
- #
13
- # CBDT Table Structure:
14
- # ```
15
- # CBDT Table = Header (8 bytes)
16
- # + Bitmap Data (variable length)
17
- # ```
18
- #
19
- # Header (8 bytes):
20
- # - majorVersion (uint16): Major version (2 or 3)
21
- # - minorVersion (uint16): Minor version (0)
22
- # - reserved (uint32): Reserved, set to 0
5
+ # CBDT (Color Bitmap Data) table.
23
6
  #
24
- # The bitmap data format depends on the index subtable format in CBLC.
25
- # Common formats include:
26
- # - Format 17: Small metrics, PNG data
27
- # - Format 18: Big metrics, PNG data
28
- # - Format 19: Metrics in CBLC, PNG data
7
+ # CBDT stores the raw bitmap data blocks for color glyphs. CBLC indexes
8
+ # into CBDT via `imageDataOffset` and per-glyph offset arrays. CBDT
9
+ # itself is essentially a blob: the 4-byte header (majorVersion +
10
+ # minorVersion) followed by an opaque sequence of bitmap blocks.
29
11
  #
30
- # This parser provides low-level access to bitmap data. For proper bitmap
31
- # extraction, use together with CBLC table which contains the index.
12
+ # This model exposes the header fields via BinData and preserves the
13
+ # original bytes for subsetting and direct slicing. A Cbdt constructed
14
+ # via `Cbdt.new` (no `read`) has no captured bytes — `raw_data` is nil
15
+ # and `data_size` returns 0.
32
16
  #
33
- # Reference: OpenType CBDT specification
34
- # https://docs.microsoft.com/en-us/typography/opentype/spec/cbdt
17
+ # Reference: OpenType CBDT specification.
18
+ # https://learn.microsoft.com/en-us/typography/opentype/spec/cbdt
35
19
  #
36
- # @example Reading a CBDT table
37
- # data = font.table_data['CBDT']
38
- # cbdt = Fontisan::Tables::Cbdt.read(data)
39
- # bitmap_data = cbdt.bitmap_data_at(offset, length)
20
+ # @example Reading CBDT and slicing a bitmap block
21
+ # cbdt = Fontisan::Tables::Cbdt.read(font.table_data["CBDT"])
22
+ # bytes = cbdt.bitmap_data_at(120, 4096)
40
23
  class Cbdt < Binary::BaseRecord
41
- # OpenType table tag for CBDT
42
24
  TAG = "CBDT"
43
25
 
44
- # Supported CBDT versions
45
- VERSION_2_0 = 0x0002_0000
46
- VERSION_3_0 = 0x0003_0000
26
+ # CBDT v2.0 — original release
27
+ VERSION_2_0 = 0x00020000
28
+ # CBDT v3.0 — adds PNG image format support
29
+ VERSION_3_0 = 0x00030000
47
30
 
48
- # @return [Integer] Major version (2 or 3)
49
- attr_reader :major_version
31
+ uint16 :major_version
32
+ uint16 :minor_version
50
33
 
51
- # @return [Integer] Minor version (0)
52
- attr_reader :minor_version
53
-
54
- # @return [String] Raw binary data for the entire CBDT table
55
- attr_reader :raw_data
56
-
57
- # Override read to parse CBDT structure
34
+ # Whether this instance was populated from real table bytes.
58
35
  #
59
- # @param io [IO, String] Binary data to read
60
- # @return [Cbdt] Parsed CBDT table
61
- def self.read(io)
62
- cbdt = new
63
- return cbdt if io.nil?
64
-
65
- data = io.is_a?(String) ? io : io.read
66
- cbdt.parse!(data)
67
- cbdt
36
+ # @return [Boolean]
37
+ def has_raw_data?
38
+ defined?(@raw_data) && !@raw_data.nil? ? true : false
68
39
  end
69
40
 
70
- # Parse the CBDT table structure
41
+ # Raw bytes captured at read time, or nil for a fresh instance.
71
42
  #
72
- # @param data [String] Binary data for the CBDT table
73
- # @raise [CorruptedTableError] If CBDT structure is invalid
74
- def parse!(data)
75
- @raw_data = data
76
- io = StringIO.new(data)
77
-
78
- # Parse CBDT header (8 bytes)
79
- parse_header(io)
80
- validate_header!
81
- rescue StandardError => e
82
- raise CorruptedTableError, "Failed to parse CBDT table: #{e.message}"
83
- end
43
+ # @return [String, nil]
44
+ def raw_data
45
+ return @raw_data if defined?(@raw_data)
84
46
 
85
- # Get bitmap data at specific offset and length
86
- #
87
- # Used together with CBLC index to extract bitmap data.
88
- #
89
- # @param offset [Integer] Offset from start of table
90
- # @param length [Integer] Length of bitmap data
91
- # @return [String, nil] Binary bitmap data or nil
92
- def bitmap_data_at(offset, length)
93
- return nil if offset.nil? || length.nil?
94
- return nil if offset.negative? || length.negative?
95
- return nil if offset + length > raw_data.length
96
-
97
- raw_data[offset, length]
47
+ nil
98
48
  end
99
49
 
100
- # Get combined version number
50
+ # Combined version number (e.g. 0x00030000 for v3.0).
101
51
  #
102
- # @return [Integer] Combined version (e.g., 0x00020000 for v2.0)
52
+ # @return [Integer]
103
53
  def version
104
- return nil if major_version.nil? || minor_version.nil?
105
-
106
54
  (major_version << 16) | minor_version
107
55
  end
108
56
 
109
- # Get table data size
57
+ # Total byte size of the captured CBDT bytes, or 0 if none.
110
58
  #
111
- # @return [Integer] Size of CBDT table in bytes
59
+ # @return [Integer]
112
60
  def data_size
113
- raw_data&.length || 0
61
+ raw_data&.bytesize || 0
114
62
  end
115
63
 
116
- # Check if offset is valid for this table
64
+ # Whether `offset` falls within the captured CBDT byte range.
117
65
  #
118
- # @param offset [Integer] Offset to check
119
- # @return [Boolean] True if offset is within table bounds
66
+ # @param offset [Integer, nil]
67
+ # @return [Boolean]
120
68
  def valid_offset?(offset)
121
69
  return false if offset.nil? || offset.negative?
122
- return false if raw_data.nil?
123
70
 
124
- offset < raw_data.length
71
+ offset < data_size
125
72
  end
126
73
 
127
- # Validate the CBDT table structure
74
+ # Slice `length` bytes starting at `offset` from the CBDT blob.
75
+ # Returns nil for out-of-range offsets/lengths so callers can use a
76
+ # nil check instead of rescuing IndexError.
128
77
  #
129
- # @return [Boolean] True if valid
130
- def valid?
131
- return false if major_version.nil? || minor_version.nil?
132
- return false unless [2, 3].include?(major_version)
133
- return false unless minor_version.zero?
134
- return false unless raw_data
78
+ # @param offset [Integer, nil] start byte
79
+ # @param length [Integer, nil] number of bytes
80
+ # @return [String, nil]
81
+ def bitmap_data_at(offset, length)
82
+ return nil if offset.nil? || length.nil?
83
+ return nil if offset.negative? || length.negative?
84
+ return nil if offset + length > data_size
135
85
 
136
- true
86
+ raw_data[offset, length]
137
87
  end
138
88
 
139
- private
140
-
141
- # Parse CBDT header (8 bytes)
89
+ # Serialize the captured bytes back to binary. Falls back to BinData's
90
+ # default serialization (4-byte header) if no bytes were captured,
91
+ # so consumers can construct a CBDT from scratch.
142
92
  #
143
- # @param io [StringIO] Input stream
144
- def parse_header(io)
145
- @major_version = io.read(2).unpack1("n")
146
- @minor_version = io.read(2).unpack1("n")
147
- @reserved = io.read(4).unpack1("N")
93
+ # @return [String]
94
+ def to_binary_s
95
+ return raw_data if has_raw_data?
96
+
97
+ super
148
98
  end
149
99
 
150
- # Validate header values
100
+ # Whether the header declares a CBDT version fontisan understands
101
+ # (v2.0 or v3.0) and any raw bytes were captured.
151
102
  #
152
- # @raise [CorruptedTableError] If validation fails
153
- def validate_header!
154
- unless [2, 3].include?(major_version)
155
- raise CorruptedTableError,
156
- "Unsupported CBDT major version: #{major_version} " \
157
- "(only versions 2 and 3 supported)"
158
- end
159
-
160
- unless minor_version.zero?
161
- raise CorruptedTableError,
162
- "Unsupported CBDT minor version: #{minor_version} " \
163
- "(only version 0 supported)"
164
- end
103
+ # @return [Boolean]
104
+ def valid?
105
+ return false unless [2, 3].include?(major_version)
106
+ return false unless minor_version.zero?
107
+ return false unless has_raw_data?
108
+
109
+ true
165
110
  end
166
111
  end
167
112
  end
@@ -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