fontisan 0.2.8 → 0.2.10
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 +17 -101
- data/CHANGELOG.md +116 -0
- data/README.adoc +25 -13
- data/docs/APPLE_LEGACY_FONTS.adoc +173 -0
- data/docs/COLLECTION_VALIDATION.adoc +143 -0
- data/docs/COLOR_FONTS.adoc +127 -0
- data/docs/DOCUMENTATION_SUMMARY.md +141 -0
- data/docs/FONT_HINTING.adoc +9 -1
- data/docs/VALIDATION.adoc +254 -0
- data/docs/WOFF_WOFF2_FORMATS.adoc +94 -0
- data/lib/fontisan/open_type_font.rb +18 -424
- data/lib/fontisan/sfnt_font.rb +690 -0
- data/lib/fontisan/sfnt_table.rb +264 -0
- data/lib/fontisan/tables/cmap_table.rb +231 -0
- data/lib/fontisan/tables/glyf_table.rb +255 -0
- data/lib/fontisan/tables/head_table.rb +111 -0
- data/lib/fontisan/tables/hhea_table.rb +255 -0
- data/lib/fontisan/tables/hmtx_table.rb +191 -0
- data/lib/fontisan/tables/loca_table.rb +212 -0
- data/lib/fontisan/tables/maxp_table.rb +258 -0
- data/lib/fontisan/tables/name_table.rb +176 -0
- data/lib/fontisan/tables/os2_table.rb +329 -0
- data/lib/fontisan/tables/post_table.rb +183 -0
- data/lib/fontisan/true_type_font.rb +12 -463
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff_font.rb +45 -29
- metadata +21 -2
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
require_relative "loading_modes"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
# Base class for SFNT font tables
|
|
8
|
+
#
|
|
9
|
+
# Represents a single table in an SFNT font file, encapsulating:
|
|
10
|
+
# - Table metadata (tag, checksum, offset, length)
|
|
11
|
+
# - Lazy loading of table data
|
|
12
|
+
# - Parsing of table data into structured objects
|
|
13
|
+
# - Table-specific validation
|
|
14
|
+
#
|
|
15
|
+
# This class provides an OOP representation of font tables, replacing
|
|
16
|
+
# the previous separation of TableDirectory (metadata), @table_data (raw bytes),
|
|
17
|
+
# and @parsed_tables (parsed objects) with a single cohesive domain object.
|
|
18
|
+
#
|
|
19
|
+
# @abstract Subclasses should override `parser_class` and `validate_parsed_table?`
|
|
20
|
+
#
|
|
21
|
+
# @example Accessing table metadata
|
|
22
|
+
# table = SfntTable.new(font, entry)
|
|
23
|
+
# puts table.tag # => "head"
|
|
24
|
+
# puts table.checksum # => 0x12345678
|
|
25
|
+
# puts table.offset # => 0x0000012C
|
|
26
|
+
# puts table.length # => 54
|
|
27
|
+
#
|
|
28
|
+
# @example Lazy loading table data
|
|
29
|
+
# table.load_data! # Loads raw bytes from IO
|
|
30
|
+
# puts table.data.bytesize
|
|
31
|
+
#
|
|
32
|
+
# @example Parsing table data
|
|
33
|
+
# head_table = table.parse
|
|
34
|
+
# puts head_table.units_per_em
|
|
35
|
+
#
|
|
36
|
+
# @example Validating table
|
|
37
|
+
# table.validate! # Raises InvalidFontError if invalid
|
|
38
|
+
class SfntTable
|
|
39
|
+
# Table metadata entry (from TableDirectory)
|
|
40
|
+
#
|
|
41
|
+
# @return [TableDirectory] The table directory entry
|
|
42
|
+
attr_reader :entry
|
|
43
|
+
|
|
44
|
+
# Parent font containing this table
|
|
45
|
+
#
|
|
46
|
+
# @return [SfntFont] The font that contains this table
|
|
47
|
+
attr_reader :font
|
|
48
|
+
|
|
49
|
+
# Raw table data (loaded lazily)
|
|
50
|
+
#
|
|
51
|
+
# @return [String, nil] Raw binary table data, or nil if not loaded
|
|
52
|
+
attr_reader :data
|
|
53
|
+
|
|
54
|
+
# Parsed table object (cached)
|
|
55
|
+
#
|
|
56
|
+
# @return [Object, nil] Parsed table object, or nil if not parsed
|
|
57
|
+
attr_reader :parsed
|
|
58
|
+
|
|
59
|
+
# Table tag (4-character string)
|
|
60
|
+
#
|
|
61
|
+
# @return [String] The table tag (e.g., "head", "name", "cmap")
|
|
62
|
+
def tag
|
|
63
|
+
@entry.tag
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Table checksum
|
|
67
|
+
#
|
|
68
|
+
# @return [Integer] The table checksum
|
|
69
|
+
def checksum
|
|
70
|
+
@entry.checksum
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Table offset in font file
|
|
74
|
+
#
|
|
75
|
+
# @return [Integer] Byte offset of table data
|
|
76
|
+
def offset
|
|
77
|
+
@entry.offset
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Table length in bytes
|
|
81
|
+
#
|
|
82
|
+
# @return [Integer] Table data length in bytes
|
|
83
|
+
def length
|
|
84
|
+
@entry.table_length
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Initialize a new SfntTable
|
|
88
|
+
#
|
|
89
|
+
# @param font [SfntFont] The font containing this table
|
|
90
|
+
# @param entry [TableDirectory] The table directory entry
|
|
91
|
+
def initialize(font, entry)
|
|
92
|
+
@font = font
|
|
93
|
+
@entry = entry
|
|
94
|
+
@data = nil
|
|
95
|
+
@parsed = nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Load raw table data from font file
|
|
99
|
+
#
|
|
100
|
+
# Reads the table data from the font's IO source or from cached
|
|
101
|
+
# table data. This method supports lazy loading.
|
|
102
|
+
#
|
|
103
|
+
# @return [self] Returns self for chaining
|
|
104
|
+
# @raise [RuntimeError] if table data cannot be loaded
|
|
105
|
+
def load_data!
|
|
106
|
+
# Check if already loaded
|
|
107
|
+
return self if @data
|
|
108
|
+
|
|
109
|
+
# Try to get from font's table_data cache
|
|
110
|
+
if @font.table_data && @font.table_data[tag]
|
|
111
|
+
@data = @font.table_data[tag]
|
|
112
|
+
return self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Load from IO source if available
|
|
116
|
+
if @font.io_source
|
|
117
|
+
@font.io_source.seek(offset)
|
|
118
|
+
@data = @font.io_source.read(length)
|
|
119
|
+
return self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
raise "Cannot load table '#{tag}': no IO source or cached data"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Check if table data is loaded
|
|
126
|
+
#
|
|
127
|
+
# @return [Boolean] true if table data has been loaded
|
|
128
|
+
def data_loaded?
|
|
129
|
+
!@data.nil?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if table has been parsed
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] true if table has been parsed
|
|
135
|
+
def parsed?
|
|
136
|
+
!@parsed.nil?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Parse table data into structured object
|
|
140
|
+
#
|
|
141
|
+
# Loads data if needed, then parses using the table-specific parser class.
|
|
142
|
+
# Results are cached for subsequent calls.
|
|
143
|
+
#
|
|
144
|
+
# @return [Object, nil] Parsed table object, or nil if no parser available
|
|
145
|
+
# @raise [RuntimeError] if table data cannot be loaded for parsing
|
|
146
|
+
def parse
|
|
147
|
+
return @parsed if parsed?
|
|
148
|
+
|
|
149
|
+
# Load data if not already loaded
|
|
150
|
+
load_data! unless data_loaded?
|
|
151
|
+
|
|
152
|
+
# Get parser class for this table type
|
|
153
|
+
parser = parser_class
|
|
154
|
+
return nil unless parser
|
|
155
|
+
|
|
156
|
+
# Parse and cache
|
|
157
|
+
@parsed = parser.read(@data)
|
|
158
|
+
@parsed
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Validate the table
|
|
162
|
+
#
|
|
163
|
+
# Performs table-specific validation. Subclasses should override
|
|
164
|
+
# `validate_parsed_table?` to provide custom validation logic.
|
|
165
|
+
#
|
|
166
|
+
# @return [Boolean] true if table is valid
|
|
167
|
+
# @raise [Fontisan::InvalidFontError] if table is invalid
|
|
168
|
+
def validate!
|
|
169
|
+
# Ensure data is loaded
|
|
170
|
+
load_data! unless data_loaded?
|
|
171
|
+
|
|
172
|
+
# Basic validation: data size matches expected size
|
|
173
|
+
if @data.bytesize != length
|
|
174
|
+
raise InvalidFontError,
|
|
175
|
+
"Table '#{tag}' data size mismatch: expected #{length} bytes, got #{@data.bytesize}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Validate checksum if not head table (head table checksum is special)
|
|
179
|
+
if tag != Constants::HEAD_TAG
|
|
180
|
+
expected_checksum = calculate_checksum
|
|
181
|
+
if checksum != expected_checksum
|
|
182
|
+
# Checksum mismatch might be OK for some tables, log a warning
|
|
183
|
+
# But don't fail validation for it
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Table-specific validation (if parsed)
|
|
188
|
+
if parsed?
|
|
189
|
+
validate_parsed_table?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
true
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Calculate table checksum
|
|
196
|
+
#
|
|
197
|
+
# @return [Integer] The checksum of the table data
|
|
198
|
+
def calculate_checksum
|
|
199
|
+
load_data! unless data_loaded?
|
|
200
|
+
|
|
201
|
+
require_relative "utilities/checksum_calculator"
|
|
202
|
+
Utilities::ChecksumCalculator.calculate_table_checksum(@data)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Check if table is available in current loading mode
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean] true if table is available
|
|
208
|
+
def available?
|
|
209
|
+
@font.table_available?(tag)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Check if table is required for the font
|
|
213
|
+
#
|
|
214
|
+
# @return [Boolean] true if table is required
|
|
215
|
+
def required?
|
|
216
|
+
Constants::REQUIRED_TABLES.include?(tag)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Get human-readable table name
|
|
220
|
+
#
|
|
221
|
+
# @return [String] Human-readable name
|
|
222
|
+
def human_name
|
|
223
|
+
Constants::TABLE_NAMES[tag] || tag
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# String representation
|
|
227
|
+
#
|
|
228
|
+
# @return [String] Human-readable representation
|
|
229
|
+
def inspect
|
|
230
|
+
"#<#{self.class.name} tag=#{tag.inspect} offset=0x#{offset.to_s(16).upcase} length=#{length}>"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# String representation for display
|
|
234
|
+
#
|
|
235
|
+
# @return [String] Human-readable representation
|
|
236
|
+
def to_s
|
|
237
|
+
"#{tag}: #{human_name} (#{length} bytes @ 0x#{offset.to_s(16).upcase})"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
protected
|
|
241
|
+
|
|
242
|
+
# Get the parser class for this table type
|
|
243
|
+
#
|
|
244
|
+
# Subclasses should override this method to return the appropriate
|
|
245
|
+
# Tables::* class (e.g., Tables::Head, Tables::Name).
|
|
246
|
+
#
|
|
247
|
+
# @return [Class, nil] The parser class, or nil if no parser available
|
|
248
|
+
def parser_class
|
|
249
|
+
# Direct access to TABLE_CLASS_MAP for better performance
|
|
250
|
+
@font.class::TABLE_CLASS_MAP[tag]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Validate the parsed table object
|
|
254
|
+
#
|
|
255
|
+
# Subclasses should override this method to provide table-specific
|
|
256
|
+
# validation logic. The default implementation does nothing.
|
|
257
|
+
#
|
|
258
|
+
# @return [Boolean] true if valid
|
|
259
|
+
# @raise [Fontisan::InvalidFontError] if table is invalid
|
|
260
|
+
def validate_parsed_table?
|
|
261
|
+
true
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "cmap"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'cmap' (Character to Glyph Index Mapping) table
|
|
9
|
+
#
|
|
10
|
+
# The cmap table maps character codes to glyph indices, supporting multiple
|
|
11
|
+
# encoding formats for different character sets and Unicode planes.
|
|
12
|
+
#
|
|
13
|
+
# This class extends SfntTable to provide cmap-specific validation and
|
|
14
|
+
# convenience methods for character-to-glyph mapping.
|
|
15
|
+
#
|
|
16
|
+
# @example Mapping characters to glyphs
|
|
17
|
+
# cmap = font.sfnt_table("cmap")
|
|
18
|
+
# cmap.glyph_for('A') # => 36
|
|
19
|
+
# cmap.glyph_for(0x0041) # => 36 (same as 'A')
|
|
20
|
+
# cmap.has_glyph?('€') # => true
|
|
21
|
+
# cmap.character_count # => 1234
|
|
22
|
+
class CmapTable < SfntTable
|
|
23
|
+
# Get Unicode character to glyph index mappings
|
|
24
|
+
#
|
|
25
|
+
# @return [Hash<Integer, Integer>] Mapping from Unicode codepoints to glyph IDs
|
|
26
|
+
def unicode_mappings
|
|
27
|
+
return {} unless parsed
|
|
28
|
+
|
|
29
|
+
parsed.unicode_mappings || {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get glyph ID for a character
|
|
33
|
+
#
|
|
34
|
+
# @param char [String, Integer] Character (string or Unicode codepoint)
|
|
35
|
+
# @return [Integer, nil] Glyph ID, or nil if character not mapped
|
|
36
|
+
def glyph_for(char)
|
|
37
|
+
codepoint = char.is_a?(String) ? char.ord : char
|
|
38
|
+
unicode_mappings[codepoint]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if a character has a glyph mapping
|
|
42
|
+
#
|
|
43
|
+
# @param char [String, Integer] Character (string or Unicode codepoint)
|
|
44
|
+
# @return [Boolean] true if character is mapped to a glyph
|
|
45
|
+
def has_glyph?(char)
|
|
46
|
+
!glyph_for(char).nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if multiple characters have glyph mappings
|
|
50
|
+
#
|
|
51
|
+
# @param chars [Array<String, Integer>] Characters to check
|
|
52
|
+
# @return [Boolean] true if all characters are mapped
|
|
53
|
+
def has_glyphs?(*chars)
|
|
54
|
+
chars.all? { |char| has_glyph?(char) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get the number of mapped characters
|
|
58
|
+
#
|
|
59
|
+
# @return [Integer] Number of unique character mappings
|
|
60
|
+
def character_count
|
|
61
|
+
unicode_mappings.size
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get all mapped character codes
|
|
65
|
+
#
|
|
66
|
+
# @return [Array<Integer>] Array of Unicode codepoints
|
|
67
|
+
def character_codes
|
|
68
|
+
unicode_mappings.keys.sort
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get all mapped glyphs
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<Integer>] Array of glyph IDs
|
|
74
|
+
def glyph_ids
|
|
75
|
+
unicode_mappings.values.uniq.sort
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if BMP (Basic Multilingual Plane) coverage exists
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean] true if BMP characters (U+0000-U+FFFF) are mapped
|
|
81
|
+
def has_bmp_coverage?
|
|
82
|
+
return false unless parsed
|
|
83
|
+
|
|
84
|
+
parsed.has_bmp_coverage?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if specific required characters are mapped
|
|
88
|
+
#
|
|
89
|
+
# @param chars [Array<Integer>] Unicode codepoints that must be present
|
|
90
|
+
# @return [Boolean] true if all required characters are mapped
|
|
91
|
+
def has_required_characters?(*chars)
|
|
92
|
+
return false unless parsed
|
|
93
|
+
|
|
94
|
+
parsed.has_required_characters?(*chars)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if space character is mapped
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean] true if U+0020 (space) is mapped
|
|
100
|
+
def has_space?
|
|
101
|
+
has_glyph?(0x0020)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if common Latin characters are mapped
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean] true if A-Z, a-z are mapped
|
|
107
|
+
def has_basic_latin?
|
|
108
|
+
# Check uppercase A-Z
|
|
109
|
+
return false unless has_glyphs?(*(0x0041..0x005A).to_a)
|
|
110
|
+
|
|
111
|
+
# Check lowercase a-z
|
|
112
|
+
has_glyphs?(*(0x0061..0x007A).to_a)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if digits are mapped
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean] true if 0-9 are mapped
|
|
118
|
+
def has_digits?
|
|
119
|
+
has_glyphs?(*(0x0030..0x0039).to_a)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if common punctuation is mapped
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean] true if common punctuation marks are mapped
|
|
125
|
+
def has_basic_punctuation?
|
|
126
|
+
required = [0x0020, 0x0021, 0x0022, 0x0027, 0x0028, 0x0029, 0x002C, 0x002E,
|
|
127
|
+
0x003A, 0x003B, 0x003F, 0x005F] # space !"()',.:;?_
|
|
128
|
+
has_required_characters?(*required)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get glyph IDs for a string of characters
|
|
132
|
+
#
|
|
133
|
+
# @param text [String] Text string
|
|
134
|
+
# @return [Array<Integer>] Array of glyph IDs
|
|
135
|
+
def glyphs_for_text(text)
|
|
136
|
+
text.chars.map { |char| glyph_for(char) || 0 }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Create a simple text rendering glyph sequence
|
|
140
|
+
#
|
|
141
|
+
# @param text [String] Text string
|
|
142
|
+
# @return [Array<Integer>] Array of glyph IDs for rendering
|
|
143
|
+
def glyph_sequence_for(text)
|
|
144
|
+
glyphs_for_text(text)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Get the highest Unicode codepoint mapped
|
|
148
|
+
#
|
|
149
|
+
# @return [Integer, nil] Maximum codepoint, or nil if no mappings
|
|
150
|
+
def max_codepoint
|
|
151
|
+
codes = character_codes
|
|
152
|
+
codes.last unless codes.empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get the lowest Unicode codepoint mapped
|
|
156
|
+
#
|
|
157
|
+
# @return [Integer, nil] Minimum codepoint, or nil if no mappings
|
|
158
|
+
def min_codepoint
|
|
159
|
+
codes = character_codes
|
|
160
|
+
codes.first unless codes.empty?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check if font has full Unicode coverage
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean] true if characters beyond BMP are mapped
|
|
166
|
+
def has_full_unicode?
|
|
167
|
+
max_cp = max_codepoint
|
|
168
|
+
!max_cp.nil? && max_cp > 0xFFFF
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get mapping statistics
|
|
172
|
+
#
|
|
173
|
+
# @return [Hash] Statistics about the character mapping
|
|
174
|
+
def statistics
|
|
175
|
+
{
|
|
176
|
+
character_count: character_count,
|
|
177
|
+
glyph_count: glyph_ids.size,
|
|
178
|
+
min_codepoint: min_codepoint,
|
|
179
|
+
max_codepoint: max_codepoint,
|
|
180
|
+
has_bmp: has_bmp_coverage?,
|
|
181
|
+
has_full_unicode: has_full_unicode?,
|
|
182
|
+
has_space: has_space?,
|
|
183
|
+
has_basic_latin: has_basic_latin?,
|
|
184
|
+
has_digits: has_digits?,
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
protected
|
|
189
|
+
|
|
190
|
+
# Validate the parsed cmap table
|
|
191
|
+
#
|
|
192
|
+
# @return [Boolean] true if valid
|
|
193
|
+
# @raise [InvalidFontError] if cmap table is invalid
|
|
194
|
+
def validate_parsed_table?
|
|
195
|
+
return true unless parsed
|
|
196
|
+
|
|
197
|
+
# Validate version
|
|
198
|
+
unless parsed.valid_version?
|
|
199
|
+
raise InvalidFontError,
|
|
200
|
+
"Invalid cmap table version: #{parsed.version} (must be 0)"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Validate subtables exist
|
|
204
|
+
unless parsed.has_subtables?
|
|
205
|
+
raise InvalidFontError,
|
|
206
|
+
"Invalid cmap table: no subtables found (num_tables=#{parsed.num_tables})"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Validate Unicode mapping exists
|
|
210
|
+
unless parsed.has_unicode_mapping?
|
|
211
|
+
raise InvalidFontError,
|
|
212
|
+
"Invalid cmap table: no Unicode mappings found"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Validate BMP coverage (required for fonts)
|
|
216
|
+
unless parsed.has_bmp_coverage?
|
|
217
|
+
raise InvalidFontError,
|
|
218
|
+
"Invalid cmap table: no BMP character coverage found"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Validate required characters (space at minimum)
|
|
222
|
+
unless parsed.has_required_characters?(0x0020)
|
|
223
|
+
raise InvalidFontError,
|
|
224
|
+
"Invalid cmap table: missing required character U+0020 (space)"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
true
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|