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,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "name"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'name' (Naming) table
|
|
9
|
+
#
|
|
10
|
+
# The name table contains all naming strings for the font, including
|
|
11
|
+
# font family name, style name, designer, license, etc.
|
|
12
|
+
#
|
|
13
|
+
# This class extends SfntTable to provide name-specific convenience
|
|
14
|
+
# methods for accessing common name records.
|
|
15
|
+
#
|
|
16
|
+
# @example Accessing name table data
|
|
17
|
+
# name = font.table("name") # Returns SfntTable instance
|
|
18
|
+
# name.family_name # => "Noto Sans"
|
|
19
|
+
# name.subfamily_name # => "Regular"
|
|
20
|
+
# name.full_name # => "Noto Sans Regular"
|
|
21
|
+
# name.postscript_name # => "NotoSans-Regular"
|
|
22
|
+
class NameTable < SfntTable
|
|
23
|
+
# Name record identifiers
|
|
24
|
+
#
|
|
25
|
+
# These are the name IDs defined in the OpenType spec
|
|
26
|
+
FAMILY = 1
|
|
27
|
+
SUBFAMILY = 2
|
|
28
|
+
FULL_NAME = 4
|
|
29
|
+
POSTSCRIPT_NAME = 6
|
|
30
|
+
PREFERRED_FAMILY = 16
|
|
31
|
+
PREFERRED_SUBFAMILY = 17
|
|
32
|
+
WWS_FAMILY = 21
|
|
33
|
+
WWS_SUBFAMILY = 22
|
|
34
|
+
|
|
35
|
+
# Platform IDs
|
|
36
|
+
PLATFORM_UNICODE = 0
|
|
37
|
+
PLATFORM_MACINTOSH = 1
|
|
38
|
+
PLATFORM_WINDOWS = 3
|
|
39
|
+
|
|
40
|
+
# Get font family name
|
|
41
|
+
#
|
|
42
|
+
# Attempts to get the preferred family name, falling back to the
|
|
43
|
+
# standard family name if preferred is not available.
|
|
44
|
+
#
|
|
45
|
+
# @return [String, nil] Family name or nil if not found
|
|
46
|
+
def family_name
|
|
47
|
+
english_name(PREFERRED_FAMILY) || english_name(FAMILY)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get font subfamily name
|
|
51
|
+
#
|
|
52
|
+
# Attempts to get the preferred subfamily name, falling back to the
|
|
53
|
+
# standard subfamily name if preferred is not available.
|
|
54
|
+
#
|
|
55
|
+
# @return [String, nil] Subfamily name or nil if not found
|
|
56
|
+
def subfamily_name
|
|
57
|
+
english_name(PREFERRED_SUBFAMILY) || english_name(SUBFAMILY)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get full font name
|
|
61
|
+
#
|
|
62
|
+
# @return [String, nil] Full name or nil if not found
|
|
63
|
+
def full_name
|
|
64
|
+
english_name(FULL_NAME)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get PostScript name
|
|
68
|
+
#
|
|
69
|
+
# @return [String, nil] PostScript name or nil if not found
|
|
70
|
+
def postscript_name
|
|
71
|
+
english_name(POSTSCRIPT_NAME)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get preferred family name
|
|
75
|
+
#
|
|
76
|
+
# @return [String, nil] Preferred family name or nil if not found
|
|
77
|
+
def preferred_family_name
|
|
78
|
+
english_name(PREFERRED_FAMILY)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get preferred subfamily name
|
|
82
|
+
#
|
|
83
|
+
# @return [String, nil] Preferred subfamily name or nil if not found
|
|
84
|
+
def preferred_subfamily_name
|
|
85
|
+
english_name(PREFERRED_SUBFAMILY)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get English name for a specific name ID
|
|
89
|
+
#
|
|
90
|
+
# Searches for an English name record with the given name ID.
|
|
91
|
+
# Prefers Windows (platform 3) over Mac (platform 1) over Unicode (platform 0).
|
|
92
|
+
#
|
|
93
|
+
# @param name_id [Integer] The name record ID
|
|
94
|
+
# @return [String, nil] The name string, or nil if not found
|
|
95
|
+
def english_name(name_id)
|
|
96
|
+
return nil unless parsed
|
|
97
|
+
|
|
98
|
+
# Find all name records with this name_id
|
|
99
|
+
records = parsed.name_records.select { |nr| nr.name_id == name_id }
|
|
100
|
+
return nil if records.empty?
|
|
101
|
+
|
|
102
|
+
# Try to find English Windows name first (platform 3, language 0x409)
|
|
103
|
+
windows = records.find do |nr|
|
|
104
|
+
nr.platform_id == PLATFORM_WINDOWS && nr.language_id == 0x409
|
|
105
|
+
end
|
|
106
|
+
return windows.string if windows&.string
|
|
107
|
+
|
|
108
|
+
# Try Mac English (platform 1, language 0)
|
|
109
|
+
mac = records.find do |nr|
|
|
110
|
+
nr.platform_id == PLATFORM_MACINTOSH && nr.language_id.zero?
|
|
111
|
+
end
|
|
112
|
+
return mac.string if mac&.string
|
|
113
|
+
|
|
114
|
+
# Try any English Unicode name (platform 0, language 0)
|
|
115
|
+
unicode = records.find do |nr|
|
|
116
|
+
nr.platform_id == PLATFORM_UNICODE && nr.language_id.zero?
|
|
117
|
+
end
|
|
118
|
+
return unicode.string if unicode&.string
|
|
119
|
+
|
|
120
|
+
# Fallback to first record with this name_id
|
|
121
|
+
first = records.first
|
|
122
|
+
first&.string
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get all name records
|
|
126
|
+
#
|
|
127
|
+
# @return [Array<NameRecord>, nil] Array of name records, or nil if not parsed
|
|
128
|
+
def name_records
|
|
129
|
+
parsed&.name_records
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get all names for a specific name ID
|
|
133
|
+
#
|
|
134
|
+
# @param name_id [Integer] The name record ID
|
|
135
|
+
# @return [Array<Hash>] Array of hashes with platform, encoding, language, and string
|
|
136
|
+
def all_names_for(name_id)
|
|
137
|
+
return [] unless parsed
|
|
138
|
+
|
|
139
|
+
parsed.name_records
|
|
140
|
+
.select { |nr| nr.name_id == name_id }
|
|
141
|
+
.map do |nr|
|
|
142
|
+
{
|
|
143
|
+
platform_id: nr.platform_id,
|
|
144
|
+
encoding_id: nr.encoding_id,
|
|
145
|
+
language_id: nr.language_id,
|
|
146
|
+
string: nr.string,
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
protected
|
|
152
|
+
|
|
153
|
+
# Validate the parsed name table
|
|
154
|
+
#
|
|
155
|
+
# @return [Boolean] true if valid
|
|
156
|
+
# @raise [InvalidFontError] if format identifier is invalid
|
|
157
|
+
def validate_parsed_table?
|
|
158
|
+
return true unless parsed
|
|
159
|
+
|
|
160
|
+
# Validate format selector
|
|
161
|
+
unless [0, 1].include?(parsed.format)
|
|
162
|
+
raise InvalidFontError,
|
|
163
|
+
"Invalid name table format: #{parsed.format} (must be 0 or 1)"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Validate that we have at least some name records
|
|
167
|
+
if parsed.name_records.empty?
|
|
168
|
+
raise InvalidFontError,
|
|
169
|
+
"Name table has no name records"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
true
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "os2"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'OS/2' (OS/2 and Windows Metrics) table
|
|
9
|
+
#
|
|
10
|
+
# The OS/2 table contains OS/2 and Windows-specific metrics required by
|
|
11
|
+
# Windows and OS/2, including font metrics, character ranges, vendor
|
|
12
|
+
# information, and embedding permissions.
|
|
13
|
+
#
|
|
14
|
+
# This class extends SfntTable to provide OS/2-specific validation and
|
|
15
|
+
# convenience methods for accessing common OS/2 table fields.
|
|
16
|
+
#
|
|
17
|
+
# @example Accessing OS/2 table data
|
|
18
|
+
# os2 = font.sfnt_table("OS/2")
|
|
19
|
+
# os2.weight_class # => 400 (Normal)
|
|
20
|
+
# os2.width_class # => 5 (Medium)
|
|
21
|
+
# os2.vendor_id # => "APPL"
|
|
22
|
+
# os2.embedding_allowed? # => true
|
|
23
|
+
class Os2Table < SfntTable
|
|
24
|
+
# Weight class names (from OpenType spec)
|
|
25
|
+
WEIGHT_NAMES = {
|
|
26
|
+
100 => "Thin",
|
|
27
|
+
200 => "Extra-light (Ultra-light)",
|
|
28
|
+
300 => "Light",
|
|
29
|
+
400 => "Normal (Regular)",
|
|
30
|
+
500 => "Medium",
|
|
31
|
+
600 => "Semi-bold (Demi-bold)",
|
|
32
|
+
700 => "Bold",
|
|
33
|
+
800 => "Extra-bold (Ultra-bold)",
|
|
34
|
+
900 => "Black (Heavy)",
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Width class names (from OpenType spec)
|
|
38
|
+
WIDTH_NAMES = {
|
|
39
|
+
1 => "Ultra-condensed",
|
|
40
|
+
2 => "Extra-condensed",
|
|
41
|
+
3 => "Condensed",
|
|
42
|
+
4 => "Semi-condensed",
|
|
43
|
+
5 => "Medium (Normal)",
|
|
44
|
+
6 => "Semi-expanded",
|
|
45
|
+
7 => "Expanded",
|
|
46
|
+
8 => "Extra-expanded",
|
|
47
|
+
9 => "Ultra-expanded",
|
|
48
|
+
}.freeze
|
|
49
|
+
|
|
50
|
+
# Selection flags (bit field)
|
|
51
|
+
FS_ITALIC = 1 << 0
|
|
52
|
+
FS_UNDERSCORE = 1 << 1
|
|
53
|
+
FS_NEGATIVE = 1 << 2
|
|
54
|
+
FS_OUTLINED = 1 << 3
|
|
55
|
+
FS_STRIKEOUT = 1 << 4
|
|
56
|
+
FS_BOLD = 1 << 5
|
|
57
|
+
FS_REGULAR = 1 << 6
|
|
58
|
+
FS_USE_TYPO_METRICS = 1 << 7
|
|
59
|
+
FS_WWS = 1 << 8
|
|
60
|
+
FS_OBLIQUE = 1 << 9
|
|
61
|
+
|
|
62
|
+
# Get OS/2 table version
|
|
63
|
+
#
|
|
64
|
+
# @return [Integer, nil] Version number (0-5), or nil if not parsed
|
|
65
|
+
def version
|
|
66
|
+
parsed&.version
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get weight class
|
|
70
|
+
#
|
|
71
|
+
# @return [Integer, nil] Weight class (100-900), or nil if not parsed
|
|
72
|
+
def weight_class
|
|
73
|
+
parsed&.us_weight_class
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get weight class name
|
|
77
|
+
#
|
|
78
|
+
# @return [String, nil] Human-readable weight name, or nil if not parsed
|
|
79
|
+
def weight_class_name
|
|
80
|
+
return nil unless parsed
|
|
81
|
+
|
|
82
|
+
WEIGHT_NAMES[parsed.us_weight_class] || "Unknown"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get width class
|
|
86
|
+
#
|
|
87
|
+
# @return [Integer, nil] Width class (1-9), or nil if not parsed
|
|
88
|
+
def width_class
|
|
89
|
+
parsed&.us_width_class
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get width class name
|
|
93
|
+
#
|
|
94
|
+
# @return [String, nil] Human-readable width name, or nil if not parsed
|
|
95
|
+
def width_class_name
|
|
96
|
+
return nil unless parsed
|
|
97
|
+
|
|
98
|
+
WIDTH_NAMES[parsed.us_width_class] || "Unknown"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get vendor ID
|
|
102
|
+
#
|
|
103
|
+
# @return [String, nil] 4-character vendor identifier, or nil if not parsed
|
|
104
|
+
def vendor_id
|
|
105
|
+
parsed&.vendor_id
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if font is italic
|
|
109
|
+
#
|
|
110
|
+
# @return [Boolean] true if italic flag is set
|
|
111
|
+
def italic?
|
|
112
|
+
parsed && (parsed.fs_selection & FS_ITALIC) != 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if font is bold
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean] true if bold flag is set
|
|
118
|
+
def bold?
|
|
119
|
+
parsed && (parsed.fs_selection & FS_BOLD) != 0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if font uses regular style
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean] true if regular flag is set
|
|
125
|
+
def regular?
|
|
126
|
+
parsed && (parsed.fs_selection & FS_REGULAR) != 0
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if font uses typographic metrics
|
|
130
|
+
#
|
|
131
|
+
# @return [Boolean] true if use typo metrics flag is set
|
|
132
|
+
def use_typo_metrics?
|
|
133
|
+
parsed && (parsed.fs_selection & FS_USE_TYPO_METRICS) != 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Check if font is oblique
|
|
137
|
+
#
|
|
138
|
+
# @return [Boolean] true if oblique flag is set
|
|
139
|
+
def oblique?
|
|
140
|
+
parsed && (parsed.fs_selection & FS_OBLIQUE) != 0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Get typographic ascent
|
|
144
|
+
#
|
|
145
|
+
# @return [Integer, nil] Typographic ascender, or nil if not parsed
|
|
146
|
+
def typo_ascender
|
|
147
|
+
parsed&.s_typo_ascender
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get typographic descent
|
|
151
|
+
#
|
|
152
|
+
# @return [Integer, nil] Typographic descender (negative value), or nil if not parsed
|
|
153
|
+
def typo_descender
|
|
154
|
+
parsed&.s_typo_descender
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get typographic line gap
|
|
158
|
+
#
|
|
159
|
+
# @return [Integer, nil] Line gap, or nil if not parsed
|
|
160
|
+
def typo_line_gap
|
|
161
|
+
parsed&.s_typo_line_gap
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get Windows ascent
|
|
165
|
+
#
|
|
166
|
+
# @return [Integer, nil] Windows ascender, or nil if not parsed
|
|
167
|
+
def win_ascent
|
|
168
|
+
parsed&.us_win_ascent
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get Windows descent
|
|
172
|
+
#
|
|
173
|
+
# @return [Integer, nil] Windows descender, or nil if not parsed
|
|
174
|
+
def win_descent
|
|
175
|
+
parsed&.us_win_descent
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get x-height (version 2+)
|
|
179
|
+
#
|
|
180
|
+
# @return [Integer, nil] x-height value, or nil if not available
|
|
181
|
+
def x_height
|
|
182
|
+
parsed&.sx_height
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Get cap height (version 2+)
|
|
186
|
+
#
|
|
187
|
+
# @return [Integer, nil] Cap height value, or nil if not available
|
|
188
|
+
def cap_height
|
|
189
|
+
parsed&.s_cap_height
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Check if embedding is allowed
|
|
193
|
+
#
|
|
194
|
+
# @return [Boolean] true if embedding is permitted (fs_type & 0x8 == 0)
|
|
195
|
+
def embedding_allowed?
|
|
196
|
+
return false unless parsed
|
|
197
|
+
|
|
198
|
+
# fs_type bit 3 (0x8) = Embedding must not be allowed
|
|
199
|
+
# If bit 3 is NOT set, embedding is allowed
|
|
200
|
+
(parsed.fs_type & 0x8).zero?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Check if embedding is restricted
|
|
204
|
+
#
|
|
205
|
+
# @return [Boolean] true if embedding is restricted
|
|
206
|
+
def embedding_restricted?
|
|
207
|
+
!embedding_allowed?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Check if preview/print embedding is allowed
|
|
211
|
+
#
|
|
212
|
+
# @return [Boolean] true if preview and print embedding is permitted
|
|
213
|
+
def preview_print_allowed?
|
|
214
|
+
return false unless parsed
|
|
215
|
+
|
|
216
|
+
# fs_type bit 1 (0x2) = Preview & Print embedding allowed
|
|
217
|
+
(parsed.fs_type & 0x2) != 0
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Check if editable embedding is allowed
|
|
221
|
+
#
|
|
222
|
+
# @return [Boolean] true if editable embedding is permitted
|
|
223
|
+
def editable_allowed?
|
|
224
|
+
return false unless parsed
|
|
225
|
+
|
|
226
|
+
# fs_type bit 2 (0x4) = Editable embedding allowed
|
|
227
|
+
(parsed.fs_type & 0x4) != 0
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Check if subsetting is allowed
|
|
231
|
+
#
|
|
232
|
+
# @return [Boolean] true if subsetting is permitted (fs_type bit 8 is NOT set)
|
|
233
|
+
def subsetting_allowed?
|
|
234
|
+
return false unless parsed
|
|
235
|
+
|
|
236
|
+
# fs_type bit 8 (0x100) = No subsetting
|
|
237
|
+
(parsed.fs_type & 0x100).zero?
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Check if bitmap embedding only is allowed
|
|
241
|
+
#
|
|
242
|
+
# @return [Boolean] true if only bitmaps can be embedded
|
|
243
|
+
def bitmap_embedding_only?
|
|
244
|
+
return false unless parsed
|
|
245
|
+
|
|
246
|
+
# fs_type bit 9 (0x200) = Bitmap embedding only
|
|
247
|
+
(parsed.fs_type & 0x200) != 0
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Get PANOSE classification
|
|
251
|
+
#
|
|
252
|
+
# @return [Array<Integer>, nil] Array of 10 PANOSE bytes, or nil if not parsed
|
|
253
|
+
def panose
|
|
254
|
+
parsed&.panose&.to_a
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Get first character index
|
|
258
|
+
#
|
|
259
|
+
# @return [Integer, nil] First character Unicode value, or nil if not parsed
|
|
260
|
+
def first_char_index
|
|
261
|
+
parsed&.us_first_char_index
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Get last character index
|
|
265
|
+
#
|
|
266
|
+
# @return [Integer, nil] Last character Unicode value, or nil if not parsed
|
|
267
|
+
def last_char_index
|
|
268
|
+
parsed&.us_last_char_index
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
protected
|
|
272
|
+
|
|
273
|
+
# Validate the parsed OS/2 table
|
|
274
|
+
#
|
|
275
|
+
# @return [Boolean] true if valid
|
|
276
|
+
# @raise [InvalidFontError] if OS/2 table is invalid
|
|
277
|
+
def validate_parsed_table?
|
|
278
|
+
return true unless parsed
|
|
279
|
+
|
|
280
|
+
# Validate version
|
|
281
|
+
unless parsed.valid_version?
|
|
282
|
+
raise InvalidFontError,
|
|
283
|
+
"Invalid OS/2 table version: #{parsed.version} (must be 0-5)"
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Validate weight class
|
|
287
|
+
unless parsed.valid_weight_class?
|
|
288
|
+
raise InvalidFontError,
|
|
289
|
+
"Invalid OS/2 weight class: #{parsed.us_weight_class} (must be 1-1000)"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Validate width class
|
|
293
|
+
unless parsed.valid_width_class?
|
|
294
|
+
raise InvalidFontError,
|
|
295
|
+
"Invalid OS/2 width class: #{parsed.us_width_class} (must be 1-9)"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Validate vendor ID
|
|
299
|
+
unless parsed.has_vendor_id?
|
|
300
|
+
raise InvalidFontError,
|
|
301
|
+
"Invalid OS/2 vendor ID: empty or missing"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Validate typo metrics
|
|
305
|
+
unless parsed.valid_typo_metrics?
|
|
306
|
+
raise InvalidFontError,
|
|
307
|
+
"Invalid OS/2 typo metrics: ascent=#{parsed.s_typo_ascender}, " \
|
|
308
|
+
"descent=#{parsed.s_typo_descender}, line_gap=#{parsed.s_typo_line_gap}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Validate Win metrics
|
|
312
|
+
unless parsed.valid_win_metrics?
|
|
313
|
+
raise InvalidFontError,
|
|
314
|
+
"Invalid OS/2 Win metrics: win_ascent=#{parsed.us_win_ascent}, " \
|
|
315
|
+
"win_descent=#{parsed.us_win_descent} (both must be positive)"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Validate character range
|
|
319
|
+
unless parsed.valid_char_range?
|
|
320
|
+
raise InvalidFontError,
|
|
321
|
+
"Invalid OS/2 character range: first=#{parsed.us_first_char_index}, " \
|
|
322
|
+
"last=#{parsed.us_last_char_index} (first must be <= last)"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
true
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "post"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'post' (PostScript) table
|
|
9
|
+
#
|
|
10
|
+
# The post table contains PostScript information, primarily glyph names.
|
|
11
|
+
# Different versions exist (1.0, 2.0, 2.5, 3.0, 4.0) with varying
|
|
12
|
+
# glyph name storage strategies.
|
|
13
|
+
#
|
|
14
|
+
# This class extends SfntTable to provide post-specific validation and
|
|
15
|
+
# convenience methods for accessing PostScript metrics and glyph names.
|
|
16
|
+
#
|
|
17
|
+
# @example Accessing post table data
|
|
18
|
+
# post = font.sfnt_table("post")
|
|
19
|
+
# post.italic_angle # => 0.0
|
|
20
|
+
# post.underline_position # => -100
|
|
21
|
+
# post.underline_thickness # => 50
|
|
22
|
+
# post.glyph_name_for(42) # => "A"
|
|
23
|
+
class PostTable < SfntTable
|
|
24
|
+
# Get post table version
|
|
25
|
+
#
|
|
26
|
+
# @return [Float, nil] Version number (1.0, 2.0, 2.5, 3.0, or 4.0)
|
|
27
|
+
def version
|
|
28
|
+
return nil unless parsed
|
|
29
|
+
|
|
30
|
+
parsed.version
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get italic angle in degrees
|
|
34
|
+
#
|
|
35
|
+
# Positive value means counter-clockwise tilt
|
|
36
|
+
#
|
|
37
|
+
# @return [Float, nil] Italic angle in degrees, or nil if not parsed
|
|
38
|
+
def italic_angle
|
|
39
|
+
return nil unless parsed
|
|
40
|
+
|
|
41
|
+
parsed.italic_angle
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if font is italic
|
|
45
|
+
#
|
|
46
|
+
# @return [Boolean] true if italic_angle != 0
|
|
47
|
+
def italic?
|
|
48
|
+
angle = italic_angle
|
|
49
|
+
!angle.nil? && angle != 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get underline position
|
|
53
|
+
#
|
|
54
|
+
# Distance from baseline to top of underline (negative for under baseline)
|
|
55
|
+
#
|
|
56
|
+
# @return [Integer, nil] Underline position in FUnits, or nil if not parsed
|
|
57
|
+
def underline_position
|
|
58
|
+
parsed&.underline_position
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get underline thickness
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer, nil] Underline thickness in FUnits, or nil if not parsed
|
|
64
|
+
def underline_thickness
|
|
65
|
+
parsed&.underline_thickness
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if font is fixed pitch (monospaced)
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean] true if font is monospaced
|
|
71
|
+
def fixed_pitch?
|
|
72
|
+
return false unless parsed
|
|
73
|
+
|
|
74
|
+
parsed.is_fixed_pitch == 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get minimum memory for Type 42 fonts
|
|
78
|
+
#
|
|
79
|
+
# @return [Integer, nil] Minimum memory in bytes, or nil if not parsed
|
|
80
|
+
def min_mem_type42
|
|
81
|
+
parsed&.min_mem_type42
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get maximum memory for Type 42 fonts
|
|
85
|
+
#
|
|
86
|
+
# @return [Integer, nil] Maximum memory in bytes, or nil if not parsed
|
|
87
|
+
def max_mem_type42
|
|
88
|
+
parsed&.max_mem_type42
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get minimum memory for Type 1 fonts
|
|
92
|
+
#
|
|
93
|
+
# @return [Integer, nil] Minimum memory in bytes, or nil if not parsed
|
|
94
|
+
def min_mem_type1
|
|
95
|
+
parsed&.min_mem_type1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get maximum memory for Type 1 fonts
|
|
99
|
+
#
|
|
100
|
+
# @return [Integer, nil] Maximum memory in bytes, or nil if not parsed
|
|
101
|
+
def max_mem_type1
|
|
102
|
+
parsed&.max_mem_type1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get all glyph names
|
|
106
|
+
#
|
|
107
|
+
# Only available for version 1.0 and 2.0
|
|
108
|
+
#
|
|
109
|
+
# @return [Array<String>] Array of glyph names
|
|
110
|
+
def glyph_names
|
|
111
|
+
return [] unless parsed
|
|
112
|
+
|
|
113
|
+
parsed.glyph_names || []
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get glyph name by ID
|
|
117
|
+
#
|
|
118
|
+
# @param glyph_id [Integer] Glyph ID
|
|
119
|
+
# @return [String, nil] Glyph name, or nil if not found
|
|
120
|
+
def glyph_name_for(glyph_id)
|
|
121
|
+
names = glyph_names
|
|
122
|
+
return nil if glyph_id.negative? || glyph_id >= names.length
|
|
123
|
+
|
|
124
|
+
names[glyph_id]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check if glyph names are available
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] true if glyph names can be retrieved
|
|
130
|
+
def has_glyph_names?
|
|
131
|
+
return false unless parsed
|
|
132
|
+
|
|
133
|
+
parsed.has_glyph_names?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get the number of glyphs with names
|
|
137
|
+
#
|
|
138
|
+
# @return [Integer] Number of named glyphs
|
|
139
|
+
def named_glyph_count
|
|
140
|
+
glyph_names.length
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
protected
|
|
144
|
+
|
|
145
|
+
# Validate the parsed post table
|
|
146
|
+
#
|
|
147
|
+
# @return [Boolean] true if valid
|
|
148
|
+
# @raise [InvalidFontError] if post table is invalid
|
|
149
|
+
def validate_parsed_table?
|
|
150
|
+
return true unless parsed
|
|
151
|
+
|
|
152
|
+
# Validate version
|
|
153
|
+
unless parsed.valid_version?
|
|
154
|
+
raise InvalidFontError,
|
|
155
|
+
"Invalid post table version: #{parsed.version} " \
|
|
156
|
+
"(must be 1.0, 2.0, 2.5, 3.0, or 4.0)"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Validate italic angle
|
|
160
|
+
unless parsed.valid_italic_angle?
|
|
161
|
+
raise InvalidFontError,
|
|
162
|
+
"Invalid post italic angle: #{parsed.italic_angle} " \
|
|
163
|
+
"(must be between -60 and 60 degrees)"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Validate fixed pitch flag
|
|
167
|
+
unless parsed.valid_fixed_pitch_flag?
|
|
168
|
+
raise InvalidFontError,
|
|
169
|
+
"Invalid post is_fixed_pitch: #{parsed.is_fixed_pitch} " \
|
|
170
|
+
"(must be 0 or 1)"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Validate version 2.0 data completeness
|
|
174
|
+
unless parsed.complete_version_2_data?
|
|
175
|
+
raise InvalidFontError,
|
|
176
|
+
"Invalid post version 2.0 table: incomplete data"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
true
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|