fontisan 0.2.3 → 0.2.5
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 +221 -49
- data/README.adoc +519 -5
- data/Rakefile +20 -7
- data/lib/fontisan/cli.rb +67 -6
- data/lib/fontisan/commands/base_command.rb +2 -19
- data/lib/fontisan/commands/convert_command.rb +16 -13
- data/lib/fontisan/commands/info_command.rb +88 -0
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +58 -20
- data/lib/fontisan/converters/outline_converter.rb +6 -3
- data/lib/fontisan/converters/svg_generator.rb +45 -0
- data/lib/fontisan/converters/woff2_encoder.rb +84 -13
- data/lib/fontisan/models/bitmap_glyph.rb +123 -0
- data/lib/fontisan/models/bitmap_strike.rb +94 -0
- data/lib/fontisan/models/color_glyph.rb +57 -0
- data/lib/fontisan/models/color_layer.rb +53 -0
- data/lib/fontisan/models/color_palette.rb +60 -0
- data/lib/fontisan/models/font_info.rb +26 -0
- data/lib/fontisan/models/svg_glyph.rb +89 -0
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/open_type_font.rb +6 -0
- data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
- data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
- data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
- data/lib/fontisan/pipeline/output_writer.rb +2 -2
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cbdt.rb +169 -0
- data/lib/fontisan/tables/cblc.rb +290 -0
- data/lib/fontisan/tables/cff.rb +6 -12
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/colr.rb +291 -0
- data/lib/fontisan/tables/cpal.rb +281 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/tables/sbix.rb +379 -0
- data/lib/fontisan/tables/svg.rb +301 -0
- data/lib/fontisan/true_type_font.rb +6 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +40 -11
- data/lib/fontisan/woff2/table_transformer.rb +506 -73
- data/lib/fontisan/woff2_font.rb +29 -9
- data/lib/fontisan/woff_font.rb +17 -4
- data/lib/fontisan.rb +90 -6
- metadata +20 -9
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../binary/base_record"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# CBLC (Color Bitmap Location) table parser
|
|
9
|
+
#
|
|
10
|
+
# The CBLC table contains location information for bitmap glyphs at various
|
|
11
|
+
# sizes (strikes). It works together with the CBDT table which contains the
|
|
12
|
+
# actual bitmap data.
|
|
13
|
+
#
|
|
14
|
+
# CBLC Table Structure:
|
|
15
|
+
# ```
|
|
16
|
+
# CBLC Table = Header (8 bytes)
|
|
17
|
+
# + BitmapSize Records (48 bytes each)
|
|
18
|
+
# ```
|
|
19
|
+
#
|
|
20
|
+
# Header (8 bytes):
|
|
21
|
+
# - version (uint32): Table version (0x00020000 or 0x00030000)
|
|
22
|
+
# - numSizes (uint32): Number of BitmapSize records
|
|
23
|
+
#
|
|
24
|
+
# Each BitmapSize record (48 bytes) contains:
|
|
25
|
+
# - indexSubTableArrayOffset (uint32): Offset to index subtable array
|
|
26
|
+
# - indexTablesSize (uint32): Size of index subtables
|
|
27
|
+
# - numberOfIndexSubTables (uint32): Number of index subtables
|
|
28
|
+
# - colorRef (uint32): Not used, set to 0
|
|
29
|
+
# - hori (SbitLineMetrics, 12 bytes): Horizontal line metrics
|
|
30
|
+
# - vert (SbitLineMetrics, 12 bytes): Vertical line metrics
|
|
31
|
+
# - startGlyphIndex (uint16): First glyph ID in strike
|
|
32
|
+
# - endGlyphIndex (uint16): Last glyph ID in strike
|
|
33
|
+
# - ppemX (uint8): Horizontal pixels per em
|
|
34
|
+
# - ppemY (uint8): Vertical pixels per em
|
|
35
|
+
# - bitDepth (uint8): Bit depth (1, 2, 4, 8, 32)
|
|
36
|
+
# - flags (int8): Flags
|
|
37
|
+
#
|
|
38
|
+
# Reference: OpenType CBLC specification
|
|
39
|
+
# https://docs.microsoft.com/en-us/typography/opentype/spec/cblc
|
|
40
|
+
#
|
|
41
|
+
# @example Reading a CBLC table
|
|
42
|
+
# data = font.table_data['CBLC']
|
|
43
|
+
# cblc = Fontisan::Tables::Cblc.read(data)
|
|
44
|
+
# strikes = cblc.strikes
|
|
45
|
+
# puts "Font has #{strikes.length} bitmap strikes"
|
|
46
|
+
class Cblc < Binary::BaseRecord
|
|
47
|
+
# OpenType table tag for CBLC
|
|
48
|
+
TAG = "CBLC"
|
|
49
|
+
|
|
50
|
+
# Supported CBLC versions
|
|
51
|
+
VERSION_2_0 = 0x00020000
|
|
52
|
+
VERSION_3_0 = 0x00030000
|
|
53
|
+
|
|
54
|
+
# SbitLineMetrics structure (12 bytes)
|
|
55
|
+
#
|
|
56
|
+
# Contains metrics for horizontal or vertical layout
|
|
57
|
+
class SbitLineMetrics < Binary::BaseRecord
|
|
58
|
+
endian :big
|
|
59
|
+
int8 :ascender
|
|
60
|
+
int8 :descender
|
|
61
|
+
uint8 :width_max
|
|
62
|
+
int8 :caret_slope_numerator
|
|
63
|
+
int8 :caret_slope_denominator
|
|
64
|
+
int8 :caret_offset
|
|
65
|
+
int8 :min_origin_sb
|
|
66
|
+
int8 :min_advance_sb
|
|
67
|
+
int8 :max_before_bl
|
|
68
|
+
int8 :min_after_bl
|
|
69
|
+
int8 :pad1
|
|
70
|
+
int8 :pad2
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# BitmapSize record structure (48 bytes)
|
|
74
|
+
#
|
|
75
|
+
# Describes a bitmap strike at a specific ppem size
|
|
76
|
+
class BitmapSize < Binary::BaseRecord
|
|
77
|
+
endian :big
|
|
78
|
+
uint32 :index_subtable_array_offset
|
|
79
|
+
uint32 :index_tables_size
|
|
80
|
+
uint32 :number_of_index_subtables
|
|
81
|
+
uint32 :color_ref
|
|
82
|
+
|
|
83
|
+
# Read the SbitLineMetrics structures manually
|
|
84
|
+
def self.read(io)
|
|
85
|
+
data = io.is_a?(String) ? io : io.read
|
|
86
|
+
size = new
|
|
87
|
+
|
|
88
|
+
io = StringIO.new(data)
|
|
89
|
+
size.instance_variable_set(:@index_subtable_array_offset, io.read(4).unpack1("N"))
|
|
90
|
+
size.instance_variable_set(:@index_tables_size, io.read(4).unpack1("N"))
|
|
91
|
+
size.instance_variable_set(:@number_of_index_subtables, io.read(4).unpack1("N"))
|
|
92
|
+
size.instance_variable_set(:@color_ref, io.read(4).unpack1("N"))
|
|
93
|
+
|
|
94
|
+
# Parse hori and vert metrics (12 bytes each)
|
|
95
|
+
hori_data = io.read(12)
|
|
96
|
+
vert_data = io.read(12)
|
|
97
|
+
size.instance_variable_set(:@hori, SbitLineMetrics.read(hori_data))
|
|
98
|
+
size.instance_variable_set(:@vert, SbitLineMetrics.read(vert_data))
|
|
99
|
+
|
|
100
|
+
# Parse remaining fields
|
|
101
|
+
size.instance_variable_set(:@start_glyph_index, io.read(2).unpack1("n"))
|
|
102
|
+
size.instance_variable_set(:@end_glyph_index, io.read(2).unpack1("n"))
|
|
103
|
+
size.instance_variable_set(:@ppem_x, io.read(1).unpack1("C"))
|
|
104
|
+
size.instance_variable_set(:@ppem_y, io.read(1).unpack1("C"))
|
|
105
|
+
size.instance_variable_set(:@bit_depth, io.read(1).unpack1("C"))
|
|
106
|
+
size.instance_variable_set(:@flags, io.read(1).unpack1("c"))
|
|
107
|
+
|
|
108
|
+
size
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
attr_reader :index_subtable_array_offset, :index_tables_size,
|
|
112
|
+
:number_of_index_subtables, :color_ref, :hori, :vert,
|
|
113
|
+
:start_glyph_index, :end_glyph_index, :ppem_x, :ppem_y,
|
|
114
|
+
:bit_depth, :flags
|
|
115
|
+
|
|
116
|
+
# Get ppem size (assumes square pixels)
|
|
117
|
+
#
|
|
118
|
+
# @return [Integer] Pixels per em
|
|
119
|
+
def ppem
|
|
120
|
+
ppem_x
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get glyph range for this strike
|
|
124
|
+
#
|
|
125
|
+
# @return [Range] Range of glyph IDs
|
|
126
|
+
def glyph_range
|
|
127
|
+
start_glyph_index..end_glyph_index
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check if this strike includes a specific glyph ID
|
|
131
|
+
#
|
|
132
|
+
# @param glyph_id [Integer] Glyph ID to check
|
|
133
|
+
# @return [Boolean] True if glyph is in range
|
|
134
|
+
def includes_glyph?(glyph_id)
|
|
135
|
+
glyph_range.include?(glyph_id)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @return [Integer] CBLC version
|
|
140
|
+
attr_reader :version
|
|
141
|
+
|
|
142
|
+
# @return [Integer] Number of bitmap size records
|
|
143
|
+
attr_reader :num_sizes
|
|
144
|
+
|
|
145
|
+
# @return [Array<BitmapSize>] Parsed bitmap size records
|
|
146
|
+
attr_reader :bitmap_sizes
|
|
147
|
+
|
|
148
|
+
# @return [String] Raw binary data for the entire CBLC table
|
|
149
|
+
attr_reader :raw_data
|
|
150
|
+
|
|
151
|
+
# Override read to parse CBLC structure
|
|
152
|
+
#
|
|
153
|
+
# @param io [IO, String] Binary data to read
|
|
154
|
+
# @return [Cblc] Parsed CBLC table
|
|
155
|
+
def self.read(io)
|
|
156
|
+
cblc = new
|
|
157
|
+
return cblc if io.nil?
|
|
158
|
+
|
|
159
|
+
data = io.is_a?(String) ? io : io.read
|
|
160
|
+
cblc.parse!(data)
|
|
161
|
+
cblc
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Parse the CBLC table structure
|
|
165
|
+
#
|
|
166
|
+
# @param data [String] Binary data for the CBLC table
|
|
167
|
+
# @raise [CorruptedTableError] If CBLC structure is invalid
|
|
168
|
+
def parse!(data)
|
|
169
|
+
@raw_data = data
|
|
170
|
+
io = StringIO.new(data)
|
|
171
|
+
|
|
172
|
+
# Parse CBLC header (8 bytes)
|
|
173
|
+
parse_header(io)
|
|
174
|
+
validate_header!
|
|
175
|
+
|
|
176
|
+
# Parse bitmap size records
|
|
177
|
+
parse_bitmap_sizes(io)
|
|
178
|
+
rescue StandardError => e
|
|
179
|
+
raise CorruptedTableError, "Failed to parse CBLC table: #{e.message}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get bitmap strikes (sizes)
|
|
183
|
+
#
|
|
184
|
+
# @return [Array<BitmapSize>] Array of bitmap strikes
|
|
185
|
+
def strikes
|
|
186
|
+
bitmap_sizes || []
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get strikes for specific ppem size
|
|
190
|
+
#
|
|
191
|
+
# @param ppem [Integer] Pixels per em
|
|
192
|
+
# @return [Array<BitmapSize>] Strikes matching ppem
|
|
193
|
+
def strikes_for_ppem(ppem)
|
|
194
|
+
strikes.select { |size| size.ppem == ppem }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Check if glyph has bitmap at ppem size
|
|
198
|
+
#
|
|
199
|
+
# @param glyph_id [Integer] Glyph ID
|
|
200
|
+
# @param ppem [Integer] Pixels per em
|
|
201
|
+
# @return [Boolean] True if glyph has bitmap
|
|
202
|
+
def has_bitmap_for_glyph?(glyph_id, ppem)
|
|
203
|
+
strikes_for_ppem(ppem).any? do |strike|
|
|
204
|
+
strike.includes_glyph?(glyph_id)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Get all available ppem sizes
|
|
209
|
+
#
|
|
210
|
+
# @return [Array<Integer>] Sorted array of ppem sizes
|
|
211
|
+
def ppem_sizes
|
|
212
|
+
strikes.map(&:ppem).uniq.sort
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Get all glyph IDs that have bitmaps across all strikes
|
|
216
|
+
#
|
|
217
|
+
# @return [Array<Integer>] Array of glyph IDs
|
|
218
|
+
def glyph_ids_with_bitmaps
|
|
219
|
+
strikes.flat_map { |strike| strike.glyph_range.to_a }.uniq.sort
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Get strikes that include a specific glyph ID
|
|
223
|
+
#
|
|
224
|
+
# @param glyph_id [Integer] Glyph ID
|
|
225
|
+
# @return [Array<BitmapSize>] Strikes containing glyph
|
|
226
|
+
def strikes_for_glyph(glyph_id)
|
|
227
|
+
strikes.select { |strike| strike.includes_glyph?(glyph_id) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Get the number of bitmap strikes
|
|
231
|
+
#
|
|
232
|
+
# @return [Integer] Number of strikes
|
|
233
|
+
def num_strikes
|
|
234
|
+
num_sizes || 0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Validate the CBLC table structure
|
|
238
|
+
#
|
|
239
|
+
# @return [Boolean] True if valid
|
|
240
|
+
def valid?
|
|
241
|
+
return false if version.nil?
|
|
242
|
+
return false unless [VERSION_2_0, VERSION_3_0].include?(version)
|
|
243
|
+
return false if num_sizes.nil? || num_sizes.negative?
|
|
244
|
+
return false unless bitmap_sizes
|
|
245
|
+
|
|
246
|
+
true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
# Parse CBLC header (8 bytes)
|
|
252
|
+
#
|
|
253
|
+
# @param io [StringIO] Input stream
|
|
254
|
+
def parse_header(io)
|
|
255
|
+
@version = io.read(4).unpack1("N")
|
|
256
|
+
@num_sizes = io.read(4).unpack1("N")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Validate header values
|
|
260
|
+
#
|
|
261
|
+
# @raise [CorruptedTableError] If validation fails
|
|
262
|
+
def validate_header!
|
|
263
|
+
unless [VERSION_2_0, VERSION_3_0].include?(version)
|
|
264
|
+
raise CorruptedTableError,
|
|
265
|
+
"Unsupported CBLC version: 0x#{version.to_s(16).upcase} " \
|
|
266
|
+
"(only versions 2.0 and 3.0 supported)"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
if num_sizes.negative?
|
|
270
|
+
raise CorruptedTableError,
|
|
271
|
+
"Invalid numSizes: #{num_sizes}"
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Parse bitmap size records
|
|
276
|
+
#
|
|
277
|
+
# @param io [StringIO] Input stream
|
|
278
|
+
def parse_bitmap_sizes(io)
|
|
279
|
+
@bitmap_sizes = []
|
|
280
|
+
return if num_sizes.zero?
|
|
281
|
+
|
|
282
|
+
# Each BitmapSize record is 48 bytes
|
|
283
|
+
num_sizes.times do
|
|
284
|
+
size_data = io.read(48)
|
|
285
|
+
@bitmap_sizes << BitmapSize.read(size_data)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
data/lib/fontisan/tables/cff.rb
CHANGED
|
@@ -268,8 +268,7 @@ module Fontisan
|
|
|
268
268
|
|
|
269
269
|
PrivateDict.new(private_data)
|
|
270
270
|
rescue StandardError => e
|
|
271
|
-
|
|
272
|
-
nil
|
|
271
|
+
raise CorruptedTableError, "Failed to parse Private DICT: #{e.message}"
|
|
273
272
|
end
|
|
274
273
|
|
|
275
274
|
# Get the Local Subr INDEX for a specific font
|
|
@@ -300,8 +299,7 @@ module Fontisan
|
|
|
300
299
|
io.seek(absolute_offset)
|
|
301
300
|
Index.new(io, start_offset: absolute_offset)
|
|
302
301
|
rescue StandardError => e
|
|
303
|
-
|
|
304
|
-
nil
|
|
302
|
+
raise CorruptedTableError, "Failed to parse Local Subr INDEX: #{e.message}"
|
|
305
303
|
end
|
|
306
304
|
|
|
307
305
|
# Get the CharStrings INDEX for a specific font
|
|
@@ -322,8 +320,7 @@ module Fontisan
|
|
|
322
320
|
io.seek(charstrings_offset)
|
|
323
321
|
CharstringsIndex.new(io, start_offset: charstrings_offset)
|
|
324
322
|
rescue StandardError => e
|
|
325
|
-
|
|
326
|
-
nil
|
|
323
|
+
raise CorruptedTableError, "Failed to parse CharStrings INDEX: #{e.message}"
|
|
327
324
|
end
|
|
328
325
|
|
|
329
326
|
# Get a CharString for a specific glyph
|
|
@@ -358,8 +355,7 @@ module Fontisan
|
|
|
358
355
|
local_subr_index,
|
|
359
356
|
)
|
|
360
357
|
rescue StandardError => e
|
|
361
|
-
|
|
362
|
-
nil
|
|
358
|
+
raise CorruptedTableError, "Failed to get CharString for glyph #{glyph_index}: #{e.message}"
|
|
363
359
|
end
|
|
364
360
|
|
|
365
361
|
# Get the number of glyphs in a font
|
|
@@ -437,8 +433,7 @@ module Fontisan
|
|
|
437
433
|
num_glyphs = glyph_count(index)
|
|
438
434
|
Charset.new(charset_data, num_glyphs, self)
|
|
439
435
|
rescue StandardError => e
|
|
440
|
-
|
|
441
|
-
nil
|
|
436
|
+
raise CorruptedTableError, "Failed to parse Charset: #{e.message}"
|
|
442
437
|
end
|
|
443
438
|
|
|
444
439
|
# Get the Encoding for a specific font
|
|
@@ -467,8 +462,7 @@ module Fontisan
|
|
|
467
462
|
num_glyphs = glyph_count(index)
|
|
468
463
|
Encoding.new(encoding_data, num_glyphs)
|
|
469
464
|
rescue StandardError => e
|
|
470
|
-
|
|
471
|
-
nil
|
|
465
|
+
raise CorruptedTableError, "Failed to parse Encoding: #{e.message}"
|
|
472
466
|
end
|
|
473
467
|
end
|
|
474
468
|
|
data/lib/fontisan/tables/cmap.rb
CHANGED
|
@@ -36,8 +36,6 @@ module Fontisan
|
|
|
36
36
|
@unicode_mappings ||= parse_mappings
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
private
|
|
40
|
-
|
|
41
39
|
# Parse all encoding records and extract Unicode mappings
|
|
42
40
|
def parse_mappings
|
|
43
41
|
mappings = {}
|
|
@@ -279,6 +277,88 @@ module Fontisan
|
|
|
279
277
|
mappings[code] = glyph_index if glyph_index != 0
|
|
280
278
|
end
|
|
281
279
|
end
|
|
280
|
+
|
|
281
|
+
public
|
|
282
|
+
|
|
283
|
+
# Validation helper: Check if version is valid
|
|
284
|
+
#
|
|
285
|
+
# cmap version should be 0
|
|
286
|
+
#
|
|
287
|
+
# @return [Boolean] True if version is 0
|
|
288
|
+
def valid_version?
|
|
289
|
+
version == 0
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Validation helper: Check if at least one subtable exists
|
|
293
|
+
#
|
|
294
|
+
# @return [Boolean] True if num_tables > 0
|
|
295
|
+
def has_subtables?
|
|
296
|
+
num_tables && num_tables > 0
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Validation helper: Check if Unicode mapping exists
|
|
300
|
+
#
|
|
301
|
+
# @return [Boolean] True if Unicode mappings are present
|
|
302
|
+
def has_unicode_mapping?
|
|
303
|
+
!unicode_mappings.nil? && !unicode_mappings.empty?
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Validation helper: Check if BMP coverage exists
|
|
307
|
+
#
|
|
308
|
+
# Checks if the Basic Multilingual Plane (U+0000-U+FFFF) has mappings
|
|
309
|
+
#
|
|
310
|
+
# @return [Boolean] True if BMP characters are mapped
|
|
311
|
+
def has_bmp_coverage?
|
|
312
|
+
mappings = unicode_mappings
|
|
313
|
+
return false if mappings.nil? || mappings.empty?
|
|
314
|
+
|
|
315
|
+
# Check if any BMP characters (0x0000-0xFFFF) are mapped
|
|
316
|
+
mappings.keys.any? { |code| code.between?(0x0000, 0xFFFF) }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Validation helper: Check if required characters are mapped
|
|
320
|
+
#
|
|
321
|
+
# Checks for essential characters like space (U+0020)
|
|
322
|
+
#
|
|
323
|
+
# @param required_chars [Array<Integer>] Character codes that must be present
|
|
324
|
+
# @return [Boolean] True if all required characters are mapped
|
|
325
|
+
def has_required_characters?(*required_chars)
|
|
326
|
+
mappings = unicode_mappings
|
|
327
|
+
return false if mappings.nil?
|
|
328
|
+
|
|
329
|
+
required_chars.all? { |code| mappings.key?(code) }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Validation helper: Check if format 4 subtable exists
|
|
333
|
+
#
|
|
334
|
+
# Format 4 is the minimum requirement for Unicode BMP support
|
|
335
|
+
#
|
|
336
|
+
# @return [Boolean] True if format 4 subtable is found
|
|
337
|
+
def has_format_4_subtable?
|
|
338
|
+
data = to_binary_s
|
|
339
|
+
records = read_encoding_records(data)
|
|
340
|
+
|
|
341
|
+
records.any? do |record|
|
|
342
|
+
subtable_data = extract_subtable_data(record, data)
|
|
343
|
+
next false unless subtable_data && subtable_data.length >= 2
|
|
344
|
+
|
|
345
|
+
format = subtable_data[0, 2].unpack1("n")
|
|
346
|
+
format == 4
|
|
347
|
+
end
|
|
348
|
+
rescue StandardError
|
|
349
|
+
false
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Validation helper: Check if glyph indices are within bounds
|
|
353
|
+
#
|
|
354
|
+
# @param max_glyph_id [Integer] Maximum valid glyph ID from maxp table
|
|
355
|
+
# @return [Boolean] True if all mapped glyph IDs are valid
|
|
356
|
+
def valid_glyph_indices?(max_glyph_id)
|
|
357
|
+
mappings = unicode_mappings
|
|
358
|
+
return true if mappings.nil? || mappings.empty?
|
|
359
|
+
|
|
360
|
+
mappings.values.all? { |glyph_id| glyph_id >= 0 && glyph_id < max_glyph_id }
|
|
361
|
+
end
|
|
282
362
|
end
|
|
283
363
|
end
|
|
284
364
|
end
|