fontisan 0.2.7 → 0.2.9
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.yml +103 -0
- data/.rubocop_todo.yml +65 -361
- data/CHANGELOG.md +116 -0
- data/Gemfile +1 -1
- data/README.adoc +106 -27
- data/Rakefile +12 -7
- data/benchmark/variation_quick_bench.rb +1 -1
- 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/cli.rb +45 -13
- data/lib/fontisan/collection/dfont_builder.rb +2 -1
- data/lib/fontisan/commands/convert_command.rb +2 -4
- data/lib/fontisan/commands/info_command.rb +3 -3
- data/lib/fontisan/commands/pack_command.rb +2 -1
- data/lib/fontisan/commands/validate_command.rb +157 -6
- data/lib/fontisan/converters/collection_converter.rb +22 -13
- data/lib/fontisan/converters/svg_generator.rb +2 -1
- data/lib/fontisan/converters/woff2_encoder.rb +6 -6
- data/lib/fontisan/converters/woff_writer.rb +3 -1
- data/lib/fontisan/font_loader.rb +7 -6
- data/lib/fontisan/formatters/text_formatter.rb +18 -14
- data/lib/fontisan/hints/hint_converter.rb +1 -1
- data/lib/fontisan/hints/hint_validator.rb +13 -10
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +15 -8
- data/lib/fontisan/hints/truetype_instruction_generator.rb +1 -1
- data/lib/fontisan/models/collection_validation_report.rb +104 -0
- data/lib/fontisan/models/font_report.rb +24 -0
- data/lib/fontisan/models/validation_report.rb +7 -2
- data/lib/fontisan/open_type_font.rb +18 -425
- data/lib/fontisan/optimizers/charstring_rewriter.rb +1 -1
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +6 -2
- data/lib/fontisan/sfnt_font.rb +699 -0
- data/lib/fontisan/sfnt_table.rb +264 -0
- data/lib/fontisan/subset/glyph_mapping.rb +2 -0
- data/lib/fontisan/subset/table_subsetter.rb +2 -2
- data/lib/fontisan/tables/cblc.rb +8 -4
- data/lib/fontisan/tables/cff/index.rb +2 -0
- data/lib/fontisan/tables/cff.rb +6 -3
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +1 -1
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/cmap.rb +5 -5
- data/lib/fontisan/tables/cmap_table.rb +231 -0
- data/lib/fontisan/tables/glyf.rb +8 -10
- data/lib/fontisan/tables/glyf_table.rb +255 -0
- data/lib/fontisan/tables/head.rb +3 -3
- data/lib/fontisan/tables/head_table.rb +111 -0
- data/lib/fontisan/tables/hhea.rb +4 -4
- 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.rb +2 -2
- data/lib/fontisan/tables/maxp_table.rb +258 -0
- data/lib/fontisan/tables/name.rb +1 -1
- data/lib/fontisan/tables/name_table.rb +176 -0
- data/lib/fontisan/tables/os2.rb +8 -8
- data/lib/fontisan/tables/os2_table.rb +329 -0
- data/lib/fontisan/tables/post.rb +2 -2
- data/lib/fontisan/tables/post_table.rb +183 -0
- data/lib/fontisan/tables/sbix.rb +5 -4
- data/lib/fontisan/true_type_font.rb +12 -464
- data/lib/fontisan/utilities/checksum_calculator.rb +0 -44
- data/lib/fontisan/validation/collection_validator.rb +4 -2
- data/lib/fontisan/validators/basic_validator.rb +11 -21
- data/lib/fontisan/validators/font_book_validator.rb +29 -50
- data/lib/fontisan/validators/opentype_validator.rb +24 -28
- data/lib/fontisan/validators/validator.rb +87 -66
- data/lib/fontisan/validators/web_font_validator.rb +16 -21
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +31 -8
- data/lib/fontisan/woff2/hmtx_transformer.rb +2 -1
- data/lib/fontisan/woff2/table_transformer.rb +4 -2
- data/lib/fontisan/woff2_font.rb +4 -2
- data/lib/fontisan/woff_font.rb +46 -30
- data/lib/fontisan.rb +2 -2
- data/scripts/compare_stack_aware.rb +1 -1
- data/scripts/measure_optimization.rb +1 -2
- metadata +23 -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
|
data/lib/fontisan/tables/os2.rb
CHANGED
|
@@ -177,7 +177,7 @@ module Fontisan
|
|
|
177
177
|
#
|
|
178
178
|
# @return [Boolean] True if version is 0-5
|
|
179
179
|
def valid_version?
|
|
180
|
-
version
|
|
180
|
+
version&.between?(0, 5)
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
# Validation helper: Check if weight class is valid
|
|
@@ -186,7 +186,7 @@ module Fontisan
|
|
|
186
186
|
#
|
|
187
187
|
# @return [Boolean] True if weight class is valid
|
|
188
188
|
def valid_weight_class?
|
|
189
|
-
us_weight_class
|
|
189
|
+
us_weight_class&.between?(1, 1000)
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
# Validation helper: Check if width class is valid
|
|
@@ -195,7 +195,7 @@ module Fontisan
|
|
|
195
195
|
#
|
|
196
196
|
# @return [Boolean] True if width class is 1-9
|
|
197
197
|
def valid_width_class?
|
|
198
|
-
us_width_class
|
|
198
|
+
us_width_class&.between?(1, 9)
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
# Validation helper: Check if vendor ID is present
|
|
@@ -213,7 +213,7 @@ module Fontisan
|
|
|
213
213
|
#
|
|
214
214
|
# @return [Boolean] True if typo metrics have correct signs
|
|
215
215
|
def valid_typo_metrics?
|
|
216
|
-
s_typo_ascender
|
|
216
|
+
s_typo_ascender.positive? && s_typo_descender.negative? && s_typo_line_gap >= 0
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
# Validation helper: Check if Win metrics are valid
|
|
@@ -222,7 +222,7 @@ module Fontisan
|
|
|
222
222
|
#
|
|
223
223
|
# @return [Boolean] True if Win ascent and descent are positive
|
|
224
224
|
def valid_win_metrics?
|
|
225
|
-
us_win_ascent
|
|
225
|
+
us_win_ascent.positive? && us_win_descent.positive?
|
|
226
226
|
end
|
|
227
227
|
|
|
228
228
|
# Validation helper: Check if Unicode ranges are set
|
|
@@ -240,7 +240,7 @@ module Fontisan
|
|
|
240
240
|
#
|
|
241
241
|
# @return [Boolean] True if PANOSE seems to be set
|
|
242
242
|
def has_panose?
|
|
243
|
-
panose
|
|
243
|
+
panose&.any? { |val| val != 0 }
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
# Validation helper: Check if embedding permissions are set
|
|
@@ -270,9 +270,9 @@ module Fontisan
|
|
|
270
270
|
#
|
|
271
271
|
# @return [Boolean] True if metrics are present (or not required)
|
|
272
272
|
def has_x_height_cap_height?
|
|
273
|
-
return true if version < 2
|
|
273
|
+
return true if version < 2 # Not required for v0-1
|
|
274
274
|
|
|
275
|
-
!sx_height.nil? && !s_cap_height.nil? && sx_height
|
|
275
|
+
!sx_height.nil? && !s_cap_height.nil? && sx_height.positive? && s_cap_height.positive?
|
|
276
276
|
end
|
|
277
277
|
|
|
278
278
|
# Validation helper: Check if first/last char indices are reasonable
|
|
@@ -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
|
data/lib/fontisan/tables/post.rb
CHANGED
|
@@ -177,7 +177,7 @@ module Fontisan
|
|
|
177
177
|
#
|
|
178
178
|
# @return [Boolean] True if is_fixed_pitch is 0 or 1
|
|
179
179
|
def valid_fixed_pitch_flag?
|
|
180
|
-
|
|
180
|
+
[0, 1].include?(is_fixed_pitch)
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
# Validation helper: Check if glyph names are available
|
|
@@ -198,7 +198,7 @@ module Fontisan
|
|
|
198
198
|
def complete_version_2_data?
|
|
199
199
|
return true unless version == 2.0
|
|
200
200
|
|
|
201
|
-
!num_glyphs_v2.nil? && num_glyphs_v2
|
|
201
|
+
!num_glyphs_v2.nil? && num_glyphs_v2.positive? && !remaining_data.empty?
|
|
202
202
|
end
|
|
203
203
|
end
|
|
204
204
|
end
|