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,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "glyf"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'glyf' (Glyph Data) table
|
|
9
|
+
#
|
|
10
|
+
# The glyf table contains TrueType glyph outline data. Each glyph is
|
|
11
|
+
# described by either a simple glyph (with contours and points) or a
|
|
12
|
+
# compound glyph (composed of other glyphs with transformations).
|
|
13
|
+
#
|
|
14
|
+
# This class extends SfntTable to provide glyf-specific convenience
|
|
15
|
+
# methods for accessing glyph data. The glyf table requires context
|
|
16
|
+
# from the loca and head tables to function properly.
|
|
17
|
+
#
|
|
18
|
+
# @example Accessing glyphs
|
|
19
|
+
# glyf = font.sfnt_table("glyf")
|
|
20
|
+
# loca = font.table("loca")
|
|
21
|
+
# head = font.table("head")
|
|
22
|
+
#
|
|
23
|
+
# glyf.parse_with_context(loca, head)
|
|
24
|
+
# glyph = glyf.glyph_for(42) # Get glyph by ID
|
|
25
|
+
# glyph.simple? # => true or false
|
|
26
|
+
# glyph.bounding_box # => [xMin, yMin, xMax, yMax]
|
|
27
|
+
class GlyfTable < SfntTable
|
|
28
|
+
# Cache for context tables
|
|
29
|
+
attr_reader :loca_table, :head_table
|
|
30
|
+
|
|
31
|
+
# Parse the glyf table with required context
|
|
32
|
+
#
|
|
33
|
+
# The glyf table cannot be used without context from loca and head tables.
|
|
34
|
+
# This method must be called before accessing glyph data.
|
|
35
|
+
#
|
|
36
|
+
# @param loca [Tables::Loca] Parsed loca table
|
|
37
|
+
# @param head [Tables::Head] Parsed head table
|
|
38
|
+
# @return [self] Returns self for chaining
|
|
39
|
+
# @raise [ArgumentError] if context is invalid
|
|
40
|
+
def parse_with_context(loca, head)
|
|
41
|
+
unless loca && head
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"glyf table requires both loca and head tables as context"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@loca_table = loca
|
|
47
|
+
@head_table = head
|
|
48
|
+
|
|
49
|
+
# Ensure parsed data is loaded
|
|
50
|
+
parse
|
|
51
|
+
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if table has been parsed with context
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean] true if context is available
|
|
58
|
+
def has_context?
|
|
59
|
+
!@loca_table.nil? && !@head_table.nil?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get a glyph by ID
|
|
63
|
+
#
|
|
64
|
+
# @param glyph_id [Integer] Glyph ID (0-based, 0 is .notdef)
|
|
65
|
+
# @return [SimpleGlyph, CompoundGlyph, nil] Parsed glyph or nil if empty
|
|
66
|
+
# @raise [ArgumentError] if context not set
|
|
67
|
+
def glyph_for(glyph_id)
|
|
68
|
+
ensure_context!
|
|
69
|
+
return nil unless parsed
|
|
70
|
+
|
|
71
|
+
parsed.glyph_for(glyph_id, @loca_table, @head_table)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if a glyph is simple (has contours)
|
|
75
|
+
#
|
|
76
|
+
# @param glyph_id [Integer] Glyph ID
|
|
77
|
+
# @return [Boolean] true if glyph is simple
|
|
78
|
+
def simple_glyph?(glyph_id)
|
|
79
|
+
glyph = glyph_for(glyph_id)
|
|
80
|
+
return false if glyph.nil?
|
|
81
|
+
|
|
82
|
+
glyph.respond_to?(:num_contours) && glyph.num_contours >= 0
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if a glyph is compound
|
|
86
|
+
#
|
|
87
|
+
# @param glyph_id [Integer] Glyph ID
|
|
88
|
+
# @return [Boolean] true if glyph is compound
|
|
89
|
+
def compound_glyph?(glyph_id)
|
|
90
|
+
glyph = glyph_for(glyph_id)
|
|
91
|
+
return false if glyph.nil?
|
|
92
|
+
|
|
93
|
+
glyph.respond_to?(:num_contours) && glyph.num_contours == -1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if a glyph is empty
|
|
97
|
+
#
|
|
98
|
+
# @param glyph_id [Integer] Glyph ID
|
|
99
|
+
# @return [Boolean] true if glyph has no data
|
|
100
|
+
def empty_glyph?(glyph_id)
|
|
101
|
+
glyph_for(glyph_id).nil?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get glyph bounding box
|
|
105
|
+
#
|
|
106
|
+
# @param glyph_id [Integer] Glyph ID
|
|
107
|
+
# @return [Array<Integer>, nil] [xMin, yMin, xMax, yMax] or nil
|
|
108
|
+
def glyph_bounding_box(glyph_id)
|
|
109
|
+
glyph = glyph_for(glyph_id)
|
|
110
|
+
return nil if glyph.nil?
|
|
111
|
+
|
|
112
|
+
[glyph.x_min, glyph.y_min, glyph.x_max, glyph.y_max]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get number of contours for a glyph
|
|
116
|
+
#
|
|
117
|
+
# @param glyph_id [Integer] Glyph ID
|
|
118
|
+
# @return [Integer, nil] Number of contours, or nil
|
|
119
|
+
def glyph_contour_count(glyph_id)
|
|
120
|
+
glyph = glyph_for(glyph_id)
|
|
121
|
+
return nil if glyph.nil?
|
|
122
|
+
|
|
123
|
+
glyph.num_contours if glyph.respond_to?(:num_contours)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get number of points for a simple glyph
|
|
127
|
+
#
|
|
128
|
+
# @param glyph_id [Integer] Glyph ID
|
|
129
|
+
# @return [Integer, nil] Number of points, or nil
|
|
130
|
+
def glyph_point_count(glyph_id)
|
|
131
|
+
glyph = glyph_for(glyph_id)
|
|
132
|
+
return nil if glyph.nil? || !glyph.respond_to?(:num_points)
|
|
133
|
+
|
|
134
|
+
glyph.num_points
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get glyph cache size
|
|
138
|
+
#
|
|
139
|
+
# @return [Integer] Number of cached glyphs
|
|
140
|
+
def cache_size
|
|
141
|
+
return 0 unless parsed
|
|
142
|
+
|
|
143
|
+
parsed.cache_size
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Clear glyph cache
|
|
147
|
+
#
|
|
148
|
+
# @return [void]
|
|
149
|
+
def clear_cache
|
|
150
|
+
return unless parsed
|
|
151
|
+
|
|
152
|
+
parsed.clear_cache
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Validate all glyphs are accessible
|
|
156
|
+
#
|
|
157
|
+
# @param num_glyphs [Integer] Total number of glyphs to check
|
|
158
|
+
# @return [Boolean] true if all glyphs can be accessed
|
|
159
|
+
def all_glyphs_accessible?(num_glyphs)
|
|
160
|
+
ensure_context!
|
|
161
|
+
return false unless parsed
|
|
162
|
+
|
|
163
|
+
parsed.all_glyphs_accessible?(@loca_table, @head_table, num_glyphs)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Validate no glyphs are clipped
|
|
167
|
+
#
|
|
168
|
+
# @param num_glyphs [Integer] Total number of glyphs to check
|
|
169
|
+
# @return [Boolean] true if no glyphs exceed font bounds
|
|
170
|
+
def no_clipped_glyphs?(num_glyphs)
|
|
171
|
+
ensure_context!
|
|
172
|
+
return false unless parsed
|
|
173
|
+
|
|
174
|
+
parsed.no_clipped_glyphs?(@loca_table, @head_table, num_glyphs)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Validate all glyphs have valid contour counts
|
|
178
|
+
#
|
|
179
|
+
# @param glyph_id [Integer] Glyph ID to check
|
|
180
|
+
# @return [Boolean] true if contour count is valid
|
|
181
|
+
def valid_contour_count?(glyph_id)
|
|
182
|
+
ensure_context!
|
|
183
|
+
return false unless parsed
|
|
184
|
+
|
|
185
|
+
parsed.valid_contour_count?(glyph_id, @loca_table, @head_table)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Validate glyphs have sound instructions
|
|
189
|
+
#
|
|
190
|
+
# @param num_glyphs [Integer] Total number of glyphs to check
|
|
191
|
+
# @return [Boolean] true if all instructions are valid
|
|
192
|
+
def instructions_sound?(num_glyphs)
|
|
193
|
+
ensure_context!
|
|
194
|
+
return false unless parsed
|
|
195
|
+
|
|
196
|
+
parsed.instructions_sound?(@loca_table, @head_table, num_glyphs)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Get glyph statistics
|
|
200
|
+
#
|
|
201
|
+
# @param num_glyphs [Integer] Total number of glyphs
|
|
202
|
+
# @return [Hash] Statistics about the glyphs
|
|
203
|
+
def statistics(num_glyphs)
|
|
204
|
+
ensure_context!
|
|
205
|
+
return {} unless parsed
|
|
206
|
+
|
|
207
|
+
simple_count = 0
|
|
208
|
+
compound_count = 0
|
|
209
|
+
empty_count = 0
|
|
210
|
+
|
|
211
|
+
(0...num_glyphs).each do |gid|
|
|
212
|
+
if empty_glyph?(gid)
|
|
213
|
+
empty_count += 1
|
|
214
|
+
elsif simple_glyph?(gid)
|
|
215
|
+
simple_count += 1
|
|
216
|
+
elsif compound_glyph?(gid)
|
|
217
|
+
compound_count += 1
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
{
|
|
222
|
+
total_glyphs: num_glyphs,
|
|
223
|
+
simple_glyphs: simple_count,
|
|
224
|
+
compound_glyphs: compound_count,
|
|
225
|
+
empty_glyphs: empty_count,
|
|
226
|
+
cached_glyphs: cache_size,
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# Ensure context tables are set
|
|
233
|
+
#
|
|
234
|
+
# @raise [ArgumentError] if context not set
|
|
235
|
+
def ensure_context!
|
|
236
|
+
unless has_context?
|
|
237
|
+
raise ArgumentError,
|
|
238
|
+
"glyf table requires context. Call parse_with_context(loca, head) first"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
protected
|
|
243
|
+
|
|
244
|
+
# Validate the parsed glyf table
|
|
245
|
+
#
|
|
246
|
+
# @return [Boolean] true if valid
|
|
247
|
+
# @raise [InvalidFontError] if glyf table is invalid
|
|
248
|
+
def validate_parsed_table?
|
|
249
|
+
# Glyf table validation requires context, so we can't validate here
|
|
250
|
+
# Validation is deferred until context is provided
|
|
251
|
+
true
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "head"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'head' (Font Header) table
|
|
9
|
+
#
|
|
10
|
+
# The head table contains global information about the font, including
|
|
11
|
+
# metadata about the font file, bounding box, and indexing information.
|
|
12
|
+
#
|
|
13
|
+
# This class extends SfntTable to provide head-specific validation and
|
|
14
|
+
# convenience methods for accessing common head table fields.
|
|
15
|
+
#
|
|
16
|
+
# @example Accessing head table data
|
|
17
|
+
# head = font.table("head") # Returns SfntTable instance
|
|
18
|
+
# head.magic_number_valid? # => true
|
|
19
|
+
# head.units_per_em # => 2048
|
|
20
|
+
# head.bounding_box # => {x_min: -123, y_min: -456, ...}
|
|
21
|
+
class HeadTable < SfntTable
|
|
22
|
+
# Check if magic number is valid
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean] true if magic number is 0x5F0F3CF5
|
|
25
|
+
def magic_number_valid?
|
|
26
|
+
parsed && parsed.magic_number == Tables::Head::MAGIC_NUMBER
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get units per em
|
|
30
|
+
#
|
|
31
|
+
# @return [Integer, nil] Units per em value, or nil if not parsed
|
|
32
|
+
def units_per_em
|
|
33
|
+
parsed&.units_per_em
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get font bounding box
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash, nil] Bounding box hash, or nil if not parsed
|
|
39
|
+
def bounding_box
|
|
40
|
+
return nil unless parsed
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
x_min: parsed.x_min,
|
|
44
|
+
y_min: parsed.y_min,
|
|
45
|
+
x_max: parsed.x_max,
|
|
46
|
+
y_max: parsed.y_max,
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get index to loc format
|
|
51
|
+
#
|
|
52
|
+
# @return [Integer, nil] IndexToLocFormat value, or nil if not parsed
|
|
53
|
+
def index_to_loc_format
|
|
54
|
+
parsed&.index_to_loc_format
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if using long loca format
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] true if using 32-bit loca offsets
|
|
60
|
+
def long_loca_format?
|
|
61
|
+
index_to_loc_format == 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get font version
|
|
65
|
+
#
|
|
66
|
+
# @return [Float, nil] Font version as Fixed-point number
|
|
67
|
+
def version
|
|
68
|
+
parsed&.version
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get font revision
|
|
72
|
+
#
|
|
73
|
+
# @return [Float, nil] Font revision as Fixed-point number
|
|
74
|
+
def font_revision
|
|
75
|
+
parsed&.font_revision
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
protected
|
|
79
|
+
|
|
80
|
+
# Validate the parsed head table
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean] true if valid
|
|
83
|
+
# @raise [InvalidFontError] if magic number is invalid
|
|
84
|
+
def validate_parsed_table?
|
|
85
|
+
return true unless parsed
|
|
86
|
+
|
|
87
|
+
unless magic_number_valid?
|
|
88
|
+
raise InvalidFontError,
|
|
89
|
+
"Invalid head table magic number: expected 0x#{Tables::Head::MAGIC_NUMBER.to_s(16).upcase}, " \
|
|
90
|
+
"got 0x#{parsed.magic_number.to_s(16).upcase}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Validate units_per_em is power of 2
|
|
94
|
+
upem = parsed.units_per_em
|
|
95
|
+
unless upem&.positive? && (upem & (upem - 1)).zero?
|
|
96
|
+
raise InvalidFontError,
|
|
97
|
+
"Invalid head table units_per_em: #{upem} (must be power of 2)"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Validate index_to_loc_format
|
|
101
|
+
ilf = parsed.index_to_loc_format
|
|
102
|
+
unless [0, 1].include?(ilf)
|
|
103
|
+
raise InvalidFontError,
|
|
104
|
+
"Invalid head table indexToLocFormat: #{ilf} (must be 0 or 1)"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "hhea"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'hhea' (Horizontal Header) table
|
|
9
|
+
#
|
|
10
|
+
# The hhea table contains horizontal layout metrics for the entire font,
|
|
11
|
+
# defining font-wide horizontal metrics such as ascent, descent, line gap,
|
|
12
|
+
# and the number of horizontal metrics in the hmtx table.
|
|
13
|
+
#
|
|
14
|
+
# This class extends SfntTable to provide hhea-specific validation and
|
|
15
|
+
# convenience methods for accessing common hhea table fields.
|
|
16
|
+
#
|
|
17
|
+
# @example Accessing hhea table data
|
|
18
|
+
# hhea = font.sfnt_table("hhea")
|
|
19
|
+
# hhea.ascent # => 2048
|
|
20
|
+
# hhea.descent # => -512
|
|
21
|
+
# hhea.line_gap # => 0
|
|
22
|
+
# hhea.line_height # => 2560
|
|
23
|
+
class HheaTable < SfntTable
|
|
24
|
+
# Fixed value 0x00010000 for version 1.0
|
|
25
|
+
VERSION_1_0 = 0x00010000
|
|
26
|
+
|
|
27
|
+
# Get hhea table version
|
|
28
|
+
#
|
|
29
|
+
# @return [Float, nil] Version number (typically 1.0), or nil if not parsed
|
|
30
|
+
def version
|
|
31
|
+
return nil unless parsed
|
|
32
|
+
|
|
33
|
+
parsed.version
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get typographic ascent
|
|
37
|
+
#
|
|
38
|
+
# Distance from baseline to highest ascender (positive value)
|
|
39
|
+
#
|
|
40
|
+
# @return [Integer, nil] Ascent in FUnits, or nil if not parsed
|
|
41
|
+
def ascent
|
|
42
|
+
parsed&.ascent
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get typographic descent
|
|
46
|
+
#
|
|
47
|
+
# Distance from baseline to lowest descender (negative value)
|
|
48
|
+
#
|
|
49
|
+
# @return [Integer, nil] Descent in FUnits, or nil if not parsed
|
|
50
|
+
def descent
|
|
51
|
+
parsed&.descent
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get typographic line gap
|
|
55
|
+
#
|
|
56
|
+
# Additional space between lines (non-negative value)
|
|
57
|
+
#
|
|
58
|
+
# @return [Integer, nil] Line gap in FUnits, or nil if not parsed
|
|
59
|
+
def line_gap
|
|
60
|
+
parsed&.line_gap
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get total line height
|
|
64
|
+
#
|
|
65
|
+
# Calculated as ascent - descent + line_gap
|
|
66
|
+
#
|
|
67
|
+
# @return [Integer, nil] Line height in FUnits, or nil if not parsed
|
|
68
|
+
def line_height
|
|
69
|
+
return nil unless parsed
|
|
70
|
+
|
|
71
|
+
ascent - descent + line_gap
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get maximum advance width
|
|
75
|
+
#
|
|
76
|
+
# Maximum advance width value in hmtx table
|
|
77
|
+
#
|
|
78
|
+
# @return [Integer, nil] Maximum advance width, or nil if not parsed
|
|
79
|
+
def advance_width_max
|
|
80
|
+
parsed&.advance_width_max
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get minimum left sidebearing
|
|
84
|
+
#
|
|
85
|
+
# @return [Integer, nil] Minimum lsb value, or nil if not parsed
|
|
86
|
+
def min_left_side_bearing
|
|
87
|
+
parsed&.min_left_side_bearing
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get minimum right sidebearing
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer, nil] Minimum rsb value, or nil if not parsed
|
|
93
|
+
def min_right_side_bearing
|
|
94
|
+
parsed&.min_right_side_bearing
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get maximum x extent
|
|
98
|
+
#
|
|
99
|
+
# Maximum of lsb + (xMax - xMin) for all glyphs
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer, nil] Maximum extent, or nil if not parsed
|
|
102
|
+
def x_max_extent
|
|
103
|
+
parsed&.x_max_extent
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get caret slope rise
|
|
107
|
+
#
|
|
108
|
+
# Used to calculate slope of cursor (rise/run)
|
|
109
|
+
# For vertical text: rise = 1, run = 0
|
|
110
|
+
#
|
|
111
|
+
# @return [Integer, nil] Caret slope rise, or nil if not parsed
|
|
112
|
+
def caret_slope_rise
|
|
113
|
+
parsed&.caret_slope_rise
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get caret slope run
|
|
117
|
+
#
|
|
118
|
+
# Used to calculate slope of cursor (rise/run)
|
|
119
|
+
# For vertical text: run = 0
|
|
120
|
+
#
|
|
121
|
+
# @return [Integer, nil] Caret slope run, or nil if not parsed
|
|
122
|
+
def caret_slope_run
|
|
123
|
+
parsed&.caret_slope_run
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get caret offset
|
|
127
|
+
#
|
|
128
|
+
# Amount by which slanted highlight should be shifted
|
|
129
|
+
#
|
|
130
|
+
# @return [Integer, nil] Caret offset, or nil if not parsed
|
|
131
|
+
def caret_offset
|
|
132
|
+
parsed&.caret_offset
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get metric data format
|
|
136
|
+
#
|
|
137
|
+
# Format of metric data (0 for current format)
|
|
138
|
+
#
|
|
139
|
+
# @return [Integer, nil] Metric data format, or nil if not parsed
|
|
140
|
+
def metric_data_format
|
|
141
|
+
parsed&.metric_data_format
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get number of hmetrics
|
|
145
|
+
#
|
|
146
|
+
# Number of hMetric entries in hmtx table
|
|
147
|
+
#
|
|
148
|
+
# @return [Integer, nil] Number of metrics (must be >= 1), or nil if not parsed
|
|
149
|
+
def number_of_h_metrics
|
|
150
|
+
parsed&.number_of_h_metrics
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check if caret is vertical
|
|
154
|
+
#
|
|
155
|
+
# @return [Boolean] true if caret is vertical (rise != 0, run == 0)
|
|
156
|
+
def vertical_caret?
|
|
157
|
+
return false unless parsed
|
|
158
|
+
|
|
159
|
+
caret_slope_rise != 0 && caret_slope_run.zero?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Check if caret is italic
|
|
163
|
+
#
|
|
164
|
+
# @return [Boolean] true if caret is slanted (both rise and run non-zero)
|
|
165
|
+
def italic_caret?
|
|
166
|
+
return false unless parsed
|
|
167
|
+
|
|
168
|
+
caret_slope_rise != 0 && caret_slope_run != 0
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if caret is horizontal
|
|
172
|
+
#
|
|
173
|
+
# @return [Boolean] true if caret is horizontal (rise == 0, run != 0)
|
|
174
|
+
def horizontal_caret?
|
|
175
|
+
return false unless parsed
|
|
176
|
+
|
|
177
|
+
caret_slope_rise.zero? && caret_slope_run != 0
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Get caret angle in degrees
|
|
181
|
+
#
|
|
182
|
+
# @return [Float, nil] Caret angle in degrees, or nil if not parsed
|
|
183
|
+
def caret_angle
|
|
184
|
+
return nil unless parsed
|
|
185
|
+
|
|
186
|
+
return 0.0 if caret_slope_run.zero?
|
|
187
|
+
|
|
188
|
+
Math.atan2(caret_slope_rise, caret_slope_run) * (180.0 / Math::PI)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
protected
|
|
192
|
+
|
|
193
|
+
# Validate the parsed hhea table
|
|
194
|
+
#
|
|
195
|
+
# @return [Boolean] true if valid
|
|
196
|
+
# @raise [InvalidFontError] if hhea table is invalid
|
|
197
|
+
def validate_parsed_table?
|
|
198
|
+
return true unless parsed
|
|
199
|
+
|
|
200
|
+
# Validate version
|
|
201
|
+
unless parsed.valid_version?
|
|
202
|
+
raise InvalidFontError,
|
|
203
|
+
"Invalid hhea table version: expected 0x00010000 (1.0), " \
|
|
204
|
+
"got 0x#{parsed.version_raw.to_s(16).upcase}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Validate metric data format
|
|
208
|
+
unless parsed.valid_metric_data_format?
|
|
209
|
+
raise InvalidFontError,
|
|
210
|
+
"Invalid hhea metric data format: #{parsed.metric_data_format} (must be 0)"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Validate number of h metrics
|
|
214
|
+
unless parsed.valid_number_of_h_metrics?
|
|
215
|
+
raise InvalidFontError,
|
|
216
|
+
"Invalid hhea number_of_h_metrics: #{parsed.number_of_h_metrics} (must be >= 1)"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Validate ascent/descent
|
|
220
|
+
unless parsed.valid_ascent_descent?
|
|
221
|
+
raise InvalidFontError,
|
|
222
|
+
"Invalid hhea ascent/descent: ascent=#{parsed.ascent}, " \
|
|
223
|
+
"descent=#{parsed.descent} (ascent must be positive, descent must be negative)"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Validate line gap
|
|
227
|
+
unless parsed.valid_line_gap?
|
|
228
|
+
raise InvalidFontError,
|
|
229
|
+
"Invalid hhea line_gap: #{parsed.line_gap} (must be >= 0)"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Validate advance width max
|
|
233
|
+
unless parsed.valid_advance_width_max?
|
|
234
|
+
raise InvalidFontError,
|
|
235
|
+
"Invalid hhea advance_width_max: #{parsed.advance_width_max} (must be > 0)"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Validate caret slope
|
|
239
|
+
unless parsed.valid_caret_slope?
|
|
240
|
+
raise InvalidFontError,
|
|
241
|
+
"Invalid hhea caret slope: rise=#{parsed.caret_slope_rise}, " \
|
|
242
|
+
"run=#{parsed.caret_slope_run} (at least one must be non-zero)"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Validate x max extent
|
|
246
|
+
unless parsed.valid_x_max_extent?
|
|
247
|
+
raise InvalidFontError,
|
|
248
|
+
"Invalid hhea x_max_extent: #{parsed.x_max_extent} (must be > 0)"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
true
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|