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,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "hmtx"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'hmtx' (Horizontal Metrics) table
|
|
9
|
+
#
|
|
10
|
+
# The hmtx table contains horizontal metrics for each glyph in the font,
|
|
11
|
+
# providing advance width and left sidebearing values needed for proper
|
|
12
|
+
# glyph positioning and text layout.
|
|
13
|
+
#
|
|
14
|
+
# This class extends SfntTable to provide hmtx-specific convenience
|
|
15
|
+
# methods for accessing glyph metrics. The hmtx table requires context
|
|
16
|
+
# from the hhea and maxp tables to function properly.
|
|
17
|
+
#
|
|
18
|
+
# @example Accessing horizontal metrics
|
|
19
|
+
# hmtx = font.sfnt_table("hmtx")
|
|
20
|
+
# hhea = font.table("hhea")
|
|
21
|
+
# maxp = font.table("maxp")
|
|
22
|
+
#
|
|
23
|
+
# hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
24
|
+
# metric = hmtx.metric_for(42) # Get glyph metrics
|
|
25
|
+
# metric[:advance_width] # => 1000
|
|
26
|
+
# metric[:lsb] # => 50
|
|
27
|
+
class HmtxTable < SfntTable
|
|
28
|
+
# Cache for context tables
|
|
29
|
+
attr_reader :hhea_table, :maxp_table
|
|
30
|
+
|
|
31
|
+
# Parse the hmtx table with required context
|
|
32
|
+
#
|
|
33
|
+
# The hmtx table cannot be used without context from hhea and maxp tables.
|
|
34
|
+
#
|
|
35
|
+
# @param number_of_h_metrics [Integer] Number of LongHorMetric records (from hhea)
|
|
36
|
+
# @param num_glyphs [Integer] Total number of glyphs (from maxp)
|
|
37
|
+
# @return [self] Returns self for chaining
|
|
38
|
+
# @raise [ArgumentError] if context is invalid
|
|
39
|
+
def parse_with_context(number_of_h_metrics, num_glyphs)
|
|
40
|
+
unless number_of_h_metrics && num_glyphs
|
|
41
|
+
raise ArgumentError,
|
|
42
|
+
"hmtx table requires number_of_h_metrics and num_glyphs"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@number_of_h_metrics = number_of_h_metrics
|
|
46
|
+
@num_glyphs = num_glyphs
|
|
47
|
+
|
|
48
|
+
# Ensure parsed data is loaded
|
|
49
|
+
parse
|
|
50
|
+
|
|
51
|
+
# Parse with context
|
|
52
|
+
parsed.parse_with_context(number_of_h_metrics, num_glyphs)
|
|
53
|
+
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Check if table has been parsed with context
|
|
58
|
+
#
|
|
59
|
+
# @return [Boolean] true if context is available
|
|
60
|
+
def has_context?
|
|
61
|
+
!@number_of_h_metrics.nil? && !@num_glyphs.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get horizontal metrics for a glyph
|
|
65
|
+
#
|
|
66
|
+
# @param glyph_id [Integer] Glyph ID (0-based)
|
|
67
|
+
# @return [Hash, nil] Hash with :advance_width and :lsb keys, or nil
|
|
68
|
+
# @raise [ArgumentError] if context not set
|
|
69
|
+
def metric_for(glyph_id)
|
|
70
|
+
ensure_context!
|
|
71
|
+
return nil unless parsed
|
|
72
|
+
|
|
73
|
+
parsed.metric_for(glyph_id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get advance width for a glyph
|
|
77
|
+
#
|
|
78
|
+
# @param glyph_id [Integer] Glyph ID
|
|
79
|
+
# @return [Integer, nil] Advance width in FUnits, or nil
|
|
80
|
+
def advance_width_for(glyph_id)
|
|
81
|
+
metric = metric_for(glyph_id)
|
|
82
|
+
metric&.dig(:advance_width)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get left sidebearing for a glyph
|
|
86
|
+
#
|
|
87
|
+
# @param glyph_id [integer] Glyph ID
|
|
88
|
+
# @return [Integer, nil] Left sidebearing in FUnits, or nil
|
|
89
|
+
def lsb_for(glyph_id)
|
|
90
|
+
metric = metric_for(glyph_id)
|
|
91
|
+
metric&.dig(:lsb)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get number of horizontal metrics
|
|
95
|
+
#
|
|
96
|
+
# @return [Integer, nil] Number of hMetrics, or nil if not parsed
|
|
97
|
+
def number_of_h_metrics
|
|
98
|
+
return @number_of_h_metrics if @number_of_h_metrics
|
|
99
|
+
return nil unless parsed
|
|
100
|
+
|
|
101
|
+
parsed.number_of_h_metrics
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get total number of glyphs
|
|
105
|
+
#
|
|
106
|
+
# @return [Integer, nil] Total glyph count, or nil if not parsed
|
|
107
|
+
def num_glyphs
|
|
108
|
+
return @num_glyphs if @num_glyphs
|
|
109
|
+
return nil unless parsed
|
|
110
|
+
|
|
111
|
+
parsed.num_glyphs
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if a glyph has its own metrics
|
|
115
|
+
#
|
|
116
|
+
# Glyphs with ID < numberOfHMetrics have their own advance width
|
|
117
|
+
#
|
|
118
|
+
# @param glyph_id [Integer] Glyph ID
|
|
119
|
+
# @return [Boolean] true if glyph has unique metrics
|
|
120
|
+
def has_unique_metrics?(glyph_id)
|
|
121
|
+
num = number_of_h_metrics
|
|
122
|
+
return false if num.nil?
|
|
123
|
+
|
|
124
|
+
glyph_id < num
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get all advance widths
|
|
128
|
+
#
|
|
129
|
+
# @return [Array<Integer>] Array of advance widths
|
|
130
|
+
def all_advance_widths
|
|
131
|
+
return [] unless has_context?
|
|
132
|
+
|
|
133
|
+
(0...num_glyphs).map { |gid| advance_width_for(gid) || 0 }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get all left sidebearings
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<Integer>] Array of LSB values
|
|
139
|
+
def all_lsbs
|
|
140
|
+
return [] unless has_context?
|
|
141
|
+
|
|
142
|
+
(0...num_glyphs).map { |gid| lsb_for(gid) || 0 }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Get metrics statistics
|
|
146
|
+
#
|
|
147
|
+
# @return [Hash] Statistics about horizontal metrics
|
|
148
|
+
def statistics
|
|
149
|
+
return {} unless has_context?
|
|
150
|
+
|
|
151
|
+
widths = all_advance_widths
|
|
152
|
+
lsbs = all_lsbs
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
num_glyphs: num_glyphs,
|
|
156
|
+
number_of_h_metrics: number_of_h_metrics,
|
|
157
|
+
min_advance_width: widths.min,
|
|
158
|
+
max_advance_width: widths.max,
|
|
159
|
+
avg_advance_width: widths.sum.fdiv(widths.size).round(2),
|
|
160
|
+
min_lsb: lsbs.min,
|
|
161
|
+
max_lsb: lsbs.max,
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# Ensure context tables are set
|
|
168
|
+
#
|
|
169
|
+
# @raise [ArgumentError] if context not set
|
|
170
|
+
def ensure_context!
|
|
171
|
+
unless has_context?
|
|
172
|
+
raise ArgumentError,
|
|
173
|
+
"hmtx table requires context. " \
|
|
174
|
+
"Call parse_with_context(number_of_h_metrics, num_glyphs) first"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
protected
|
|
179
|
+
|
|
180
|
+
# Validate the parsed hmtx table
|
|
181
|
+
#
|
|
182
|
+
# @return [Boolean] true if valid
|
|
183
|
+
# @raise [InvalidFontError] if hmtx table is invalid
|
|
184
|
+
def validate_parsed_table?
|
|
185
|
+
# Hmtx table validation requires context, so we can't validate here
|
|
186
|
+
# Validation is deferred until context is provided
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "loca"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'loca' (Index to Location) table
|
|
9
|
+
#
|
|
10
|
+
# The loca table provides offsets to glyph data in the glyf table.
|
|
11
|
+
# Each glyph has an entry indicating where its data begins in the glyf table.
|
|
12
|
+
#
|
|
13
|
+
# This class extends SfntTable to provide loca-specific convenience
|
|
14
|
+
# methods for accessing glyph locations. The loca table requires context
|
|
15
|
+
# from the head and maxp tables to function properly.
|
|
16
|
+
#
|
|
17
|
+
# @example Accessing glyph locations
|
|
18
|
+
# loca = font.sfnt_table("loca")
|
|
19
|
+
# head = font.table("head")
|
|
20
|
+
# maxp = font.table("maxp")
|
|
21
|
+
#
|
|
22
|
+
# loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
23
|
+
# offset = loca.offset_for(42) # Get offset for glyph 42
|
|
24
|
+
# size = loca.size_of(42) # Get size of glyph 42
|
|
25
|
+
# loca.empty?(32) # Check if glyph 32 is empty
|
|
26
|
+
class LocaTable < SfntTable
|
|
27
|
+
# Short format constant
|
|
28
|
+
FORMAT_SHORT = 0
|
|
29
|
+
|
|
30
|
+
# Long format constant
|
|
31
|
+
FORMAT_LONG = 1
|
|
32
|
+
|
|
33
|
+
# Cache for context
|
|
34
|
+
attr_reader :index_to_loc_format, :num_glyphs
|
|
35
|
+
|
|
36
|
+
# Parse the loca table with required context
|
|
37
|
+
#
|
|
38
|
+
# The loca table cannot be used without context from head and maxp tables.
|
|
39
|
+
#
|
|
40
|
+
# @param index_to_loc_format [Integer] Format (0 = short, 1 = long) from head
|
|
41
|
+
# @param num_glyphs [Integer] Total number of glyphs from maxp
|
|
42
|
+
# @return [self] Returns self for chaining
|
|
43
|
+
# @raise [ArgumentError] if context is invalid
|
|
44
|
+
def parse_with_context(index_to_loc_format, num_glyphs)
|
|
45
|
+
unless index_to_loc_format && num_glyphs
|
|
46
|
+
raise ArgumentError,
|
|
47
|
+
"loca table requires index_to_loc_format and num_glyphs"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@index_to_loc_format = index_to_loc_format
|
|
51
|
+
@num_glyphs = num_glyphs
|
|
52
|
+
|
|
53
|
+
# Ensure parsed data is loaded
|
|
54
|
+
parse
|
|
55
|
+
|
|
56
|
+
# Parse with context
|
|
57
|
+
parsed.parse_with_context(index_to_loc_format, num_glyphs)
|
|
58
|
+
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if table has been parsed with context
|
|
63
|
+
#
|
|
64
|
+
# @return [Boolean] true if context is available
|
|
65
|
+
def has_context?
|
|
66
|
+
!@index_to_loc_format.nil? && !@num_glyphs.nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get the offset for a glyph ID in the glyf table
|
|
70
|
+
#
|
|
71
|
+
# @param glyph_id [Integer] Glyph ID (0-based)
|
|
72
|
+
# @return [Integer, nil] Byte offset in glyf table, or nil if invalid
|
|
73
|
+
# @raise [ArgumentError] if context not set
|
|
74
|
+
def offset_for(glyph_id)
|
|
75
|
+
ensure_context!
|
|
76
|
+
return nil unless parsed
|
|
77
|
+
|
|
78
|
+
parsed.offset_for(glyph_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Calculate the size of glyph data for a glyph ID
|
|
82
|
+
#
|
|
83
|
+
# @param glyph_id [Integer] Glyph ID (0-based)
|
|
84
|
+
# @return [Integer, nil] Size in bytes, or nil if invalid
|
|
85
|
+
# @raise [ArgumentError] if context not set
|
|
86
|
+
def size_of(glyph_id)
|
|
87
|
+
ensure_context!
|
|
88
|
+
return nil unless parsed
|
|
89
|
+
|
|
90
|
+
parsed.size_of(glyph_id)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if a glyph has no outline data
|
|
94
|
+
#
|
|
95
|
+
# @param glyph_id [Integer] Glyph ID (0-based)
|
|
96
|
+
# @return [Boolean, nil] True if empty, false if has data, nil if invalid
|
|
97
|
+
# @raise [ArgumentError] if context not set
|
|
98
|
+
def empty?(glyph_id)
|
|
99
|
+
ensure_context!
|
|
100
|
+
return nil unless parsed
|
|
101
|
+
|
|
102
|
+
parsed.empty?(glyph_id)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if using short format (format 0)
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean] true if short format
|
|
108
|
+
def short_format?
|
|
109
|
+
return false unless parsed?
|
|
110
|
+
|
|
111
|
+
@index_to_loc_format == FORMAT_SHORT
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if using long format (format 1)
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] true if long format
|
|
117
|
+
def long_format?
|
|
118
|
+
return false unless parsed?
|
|
119
|
+
|
|
120
|
+
@index_to_loc_format == FORMAT_LONG
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get format name
|
|
124
|
+
#
|
|
125
|
+
# @return [String] "short" or "long"
|
|
126
|
+
def format_name
|
|
127
|
+
short_format? ? "short" : "long"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get all offsets
|
|
131
|
+
#
|
|
132
|
+
# @return [Array<Integer>] Array of glyph offsets
|
|
133
|
+
def all_offsets
|
|
134
|
+
return [] unless parsed?
|
|
135
|
+
|
|
136
|
+
parsed.offsets || []
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get all glyph sizes
|
|
140
|
+
#
|
|
141
|
+
# @return [Array<Integer>] Array of glyph sizes
|
|
142
|
+
def all_sizes
|
|
143
|
+
return [] unless has_context?
|
|
144
|
+
|
|
145
|
+
(0...num_glyphs).map { |gid| size_of(gid) || 0 }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Get empty glyph IDs
|
|
149
|
+
#
|
|
150
|
+
# @return [Array<Integer>] Array of empty glyph IDs
|
|
151
|
+
def empty_glyph_ids
|
|
152
|
+
return [] unless has_context?
|
|
153
|
+
|
|
154
|
+
(0...num_glyphs).select { |gid| empty?(gid) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get non-empty glyph IDs
|
|
158
|
+
#
|
|
159
|
+
# @return [Array<Integer>] Array of glyph IDs with data
|
|
160
|
+
def non_empty_glyph_ids
|
|
161
|
+
return [] unless has_context?
|
|
162
|
+
|
|
163
|
+
(0...num_glyphs).reject { |gid| empty?(gid) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Get statistics about glyph locations
|
|
167
|
+
#
|
|
168
|
+
# @return [Hash] Statistics about loca table
|
|
169
|
+
def statistics
|
|
170
|
+
return {} unless has_context?
|
|
171
|
+
|
|
172
|
+
sizes = all_sizes
|
|
173
|
+
offsets = all_offsets
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
num_glyphs: num_glyphs,
|
|
177
|
+
format: format_name,
|
|
178
|
+
empty_glyph_count: empty_glyph_ids.length,
|
|
179
|
+
non_empty_glyph_count: non_empty_glyph_ids.length,
|
|
180
|
+
min_size: sizes.compact.min,
|
|
181
|
+
max_size: sizes.compact.max,
|
|
182
|
+
total_data_size: sizes.sum,
|
|
183
|
+
glyf_table_size: offsets.last || 0,
|
|
184
|
+
}
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# Ensure context tables are set
|
|
190
|
+
#
|
|
191
|
+
# @raise [ArgumentError] if context not set
|
|
192
|
+
def ensure_context!
|
|
193
|
+
unless has_context?
|
|
194
|
+
raise ArgumentError,
|
|
195
|
+
"loca table requires context. " \
|
|
196
|
+
"Call parse_with_context(index_to_loc_format, num_glyphs) first"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
protected
|
|
201
|
+
|
|
202
|
+
# Validate the parsed loca table
|
|
203
|
+
#
|
|
204
|
+
# @return [Boolean] true if valid
|
|
205
|
+
# @raise [InvalidFontError] if loca table is invalid
|
|
206
|
+
def validate_parsed_table?
|
|
207
|
+
# Loca table validation requires context, so we can't validate here
|
|
208
|
+
true
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
data/lib/fontisan/tables/maxp.rb
CHANGED
|
@@ -179,9 +179,9 @@ module Fontisan
|
|
|
179
179
|
#
|
|
180
180
|
# @return [Boolean] True if maxZones is valid or not applicable
|
|
181
181
|
def valid_max_zones?
|
|
182
|
-
return true if version_0_5?
|
|
182
|
+
return true if version_0_5? # Not applicable for CFF
|
|
183
183
|
|
|
184
|
-
max_zones
|
|
184
|
+
max_zones&.between?(1, 2)
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
# Validation helper: Check if all TrueType metrics are present
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../sfnt_table"
|
|
4
|
+
require_relative "maxp"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# OOP representation of the 'maxp' (Maximum Profile) table
|
|
9
|
+
#
|
|
10
|
+
# The maxp table contains memory and complexity limits for the font,
|
|
11
|
+
# providing the number of glyphs and various maximum values needed
|
|
12
|
+
# for font rendering and processing.
|
|
13
|
+
#
|
|
14
|
+
# This class extends SfntTable to provide maxp-specific validation and
|
|
15
|
+
# convenience methods for accessing font complexity metrics.
|
|
16
|
+
#
|
|
17
|
+
# @example Accessing maxp table data
|
|
18
|
+
# maxp = font.sfnt_table("maxp")
|
|
19
|
+
# maxp.num_glyphs # => 512
|
|
20
|
+
# maxp.version # => 1.0 or 0.5
|
|
21
|
+
# maxp.truetype? # => true or false
|
|
22
|
+
class MaxpTable < SfntTable
|
|
23
|
+
# Version 0.5 constant (CFF fonts)
|
|
24
|
+
VERSION_0_5 = 0x00005000
|
|
25
|
+
|
|
26
|
+
# Version 1.0 constant (TrueType fonts)
|
|
27
|
+
VERSION_1_0 = 0x00010000
|
|
28
|
+
|
|
29
|
+
# Get maxp table version
|
|
30
|
+
#
|
|
31
|
+
# @return [Float, nil] Version number (0.5 or 1.0), or nil if not parsed
|
|
32
|
+
def version
|
|
33
|
+
return nil unless parsed
|
|
34
|
+
|
|
35
|
+
parsed.version
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get number of glyphs
|
|
39
|
+
#
|
|
40
|
+
# @return [Integer, nil] Total number of glyphs, or nil if not parsed
|
|
41
|
+
def num_glyphs
|
|
42
|
+
parsed&.num_glyphs
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if this is a TrueType font (version 1.0)
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean] true if version 1.0
|
|
48
|
+
def truetype?
|
|
49
|
+
return false unless parsed
|
|
50
|
+
|
|
51
|
+
parsed.truetype?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check if this is a CFF font (version 0.5)
|
|
55
|
+
#
|
|
56
|
+
# @return [Boolean] true if version 0.5
|
|
57
|
+
def cff?
|
|
58
|
+
return false unless parsed
|
|
59
|
+
|
|
60
|
+
parsed.cff?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get maximum points in a non-composite glyph (version 1.0)
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer, nil] Maximum points, or nil if not available
|
|
66
|
+
def max_points
|
|
67
|
+
return nil unless parsed&.version_1_0?
|
|
68
|
+
|
|
69
|
+
parsed.max_points
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get maximum contours in a non-composite glyph (version 1.0)
|
|
73
|
+
#
|
|
74
|
+
# @return [Integer, nil] Maximum contours, or nil if not available
|
|
75
|
+
def max_contours
|
|
76
|
+
return nil unless parsed&.version_1_0?
|
|
77
|
+
|
|
78
|
+
parsed.max_contours
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get maximum points in a composite glyph (version 1.0)
|
|
82
|
+
#
|
|
83
|
+
# @return [Integer, nil] Maximum composite points, or nil if not available
|
|
84
|
+
def max_composite_points
|
|
85
|
+
return nil unless parsed&.version_1_0?
|
|
86
|
+
|
|
87
|
+
parsed.max_composite_points
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get maximum contours in a composite glyph (version 1.0)
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer, nil] Maximum composite contours, or nil if not available
|
|
93
|
+
def max_composite_contours
|
|
94
|
+
return nil unless parsed&.version_1_0?
|
|
95
|
+
|
|
96
|
+
parsed.max_composite_contours
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get maximum zones (version 1.0)
|
|
100
|
+
#
|
|
101
|
+
# @return [Integer, nil] Maximum zones (1 or 2), or nil if not available
|
|
102
|
+
def max_zones
|
|
103
|
+
return nil unless parsed&.version_1_0?
|
|
104
|
+
|
|
105
|
+
parsed.max_zones
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get maximum twilight zone points (version 1.0)
|
|
109
|
+
#
|
|
110
|
+
# @return [Integer, nil] Maximum twilight points, or nil if not available
|
|
111
|
+
def max_twilight_points
|
|
112
|
+
return nil unless parsed&.version_1_0?
|
|
113
|
+
|
|
114
|
+
parsed.max_twilight_points
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get maximum storage area locations (version 1.0)
|
|
118
|
+
#
|
|
119
|
+
# @return [Integer, nil] Maximum storage, or nil if not available
|
|
120
|
+
def max_storage
|
|
121
|
+
return nil unless parsed&.version_1_0?
|
|
122
|
+
|
|
123
|
+
parsed.max_storage
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get maximum function definitions (version 1.0)
|
|
127
|
+
#
|
|
128
|
+
# @return [Integer, nil] Maximum function defs, or nil if not available
|
|
129
|
+
def max_function_defs
|
|
130
|
+
return nil unless parsed&.version_1_0?
|
|
131
|
+
|
|
132
|
+
parsed.max_function_defs
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get maximum instruction definitions (version 1.0)
|
|
136
|
+
#
|
|
137
|
+
# @return [Integer, nil] Maximum instruction defs, or nil if not available
|
|
138
|
+
def max_instruction_defs
|
|
139
|
+
return nil unless parsed&.version_1_0?
|
|
140
|
+
|
|
141
|
+
parsed.max_instruction_defs
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get maximum stack depth (version 1.0)
|
|
145
|
+
#
|
|
146
|
+
# @return [Integer, nil] Maximum stack elements, or nil if not available
|
|
147
|
+
def max_stack_elements
|
|
148
|
+
return nil unless parsed&.version_1_0?
|
|
149
|
+
|
|
150
|
+
parsed.max_stack_elements
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get maximum byte count for glyph instructions (version 1.0)
|
|
154
|
+
#
|
|
155
|
+
# @return [Integer, nil] Maximum instruction size, or nil if not available
|
|
156
|
+
def max_size_of_instructions
|
|
157
|
+
return nil unless parsed&.version_1_0?
|
|
158
|
+
|
|
159
|
+
parsed.max_size_of_instructions
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Get maximum component elements in composite glyph (version 1.0)
|
|
163
|
+
#
|
|
164
|
+
# @return [Integer, nil] Maximum component elements, or nil if not available
|
|
165
|
+
def max_component_elements
|
|
166
|
+
return nil unless parsed&.version_1_0?
|
|
167
|
+
|
|
168
|
+
parsed.max_component_elements
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get maximum levels of recursion in composite glyphs (version 1.0)
|
|
172
|
+
#
|
|
173
|
+
# @return [Integer, nil] Maximum component depth, or nil if not available
|
|
174
|
+
def max_component_depth
|
|
175
|
+
return nil unless parsed&.version_1_0?
|
|
176
|
+
|
|
177
|
+
parsed.max_component_depth
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Check if font uses composite glyphs
|
|
181
|
+
#
|
|
182
|
+
# @return [Boolean] true if max_component_elements > 0
|
|
183
|
+
def has_composite_glyphs?
|
|
184
|
+
max_comp = max_component_elements
|
|
185
|
+
!max_comp.nil? && max_comp.positive?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Check if font uses twilight zone
|
|
189
|
+
#
|
|
190
|
+
# @return [Boolean] true if max_zones == 2
|
|
191
|
+
def has_twilight_zone?
|
|
192
|
+
max_zones == 2
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Get complexity statistics
|
|
196
|
+
#
|
|
197
|
+
# @return [Hash] Statistics about font complexity
|
|
198
|
+
def statistics
|
|
199
|
+
stats = {
|
|
200
|
+
num_glyphs: num_glyphs,
|
|
201
|
+
version: version,
|
|
202
|
+
truetype: truetype?,
|
|
203
|
+
cff: cff?,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if truetype?
|
|
207
|
+
stats[:max_points] = max_points
|
|
208
|
+
stats[:max_contours] = max_contours
|
|
209
|
+
stats[:max_composite_points] = max_composite_points
|
|
210
|
+
stats[:max_composite_contours] = max_composite_contours
|
|
211
|
+
stats[:max_component_elements] = max_component_elements
|
|
212
|
+
stats[:max_component_depth] = max_component_depth
|
|
213
|
+
stats[:has_composite_glyphs] = has_composite_glyphs?
|
|
214
|
+
stats[:has_twilight_zone] = has_twilight_zone?
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
stats
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
protected
|
|
221
|
+
|
|
222
|
+
# Validate the parsed maxp table
|
|
223
|
+
#
|
|
224
|
+
# @return [Boolean] true if valid
|
|
225
|
+
# @raise [InvalidFontError] if maxp table is invalid
|
|
226
|
+
def validate_parsed_table?
|
|
227
|
+
return true unless parsed
|
|
228
|
+
|
|
229
|
+
# Validate version
|
|
230
|
+
unless parsed.valid_version?
|
|
231
|
+
raise InvalidFontError,
|
|
232
|
+
"Invalid maxp table version: #{parsed.version} " \
|
|
233
|
+
"(must be 0.5 or 1.0)"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Validate number of glyphs
|
|
237
|
+
unless parsed.valid_num_glyphs?
|
|
238
|
+
raise InvalidFontError,
|
|
239
|
+
"Invalid maxp num_glyphs: #{parsed.num_glyphs} (must be >= 1)"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Validate max zones
|
|
243
|
+
unless parsed.valid_max_zones?
|
|
244
|
+
raise InvalidFontError,
|
|
245
|
+
"Invalid maxp max_zones: #{parsed.max_zones} (must be 1 or 2)"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Validate metrics are reasonable
|
|
249
|
+
unless parsed.reasonable_metrics?
|
|
250
|
+
raise InvalidFontError,
|
|
251
|
+
"Invalid maxp metrics: values exceed reasonable limits"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
true
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
data/lib/fontisan/tables/name.rb
CHANGED