fontisan 0.1.0 → 0.2.0
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 +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../binary/base_record"
|
|
4
|
+
require_relative "variation_common"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# Parser for the 'HVAR' (Horizontal Metrics Variations) table
|
|
9
|
+
#
|
|
10
|
+
# The HVAR table provides variation data for horizontal metrics including:
|
|
11
|
+
# - Advance width variations
|
|
12
|
+
# - Left side bearing (LSB) variations
|
|
13
|
+
#
|
|
14
|
+
# This table uses the ItemVariationStore structure to efficiently store
|
|
15
|
+
# delta values for different regions in the design space.
|
|
16
|
+
#
|
|
17
|
+
# Reference: OpenType specification, HVAR table
|
|
18
|
+
#
|
|
19
|
+
# @example Reading an HVAR table
|
|
20
|
+
# data = font.table_data("HVAR")
|
|
21
|
+
# hvar = Fontisan::Tables::Hvar.read(data)
|
|
22
|
+
# advance_deltas = hvar.advance_width_deltas(glyph_id, coordinates)
|
|
23
|
+
class Hvar < Binary::BaseRecord
|
|
24
|
+
uint16 :major_version
|
|
25
|
+
uint16 :minor_version
|
|
26
|
+
uint32 :item_variation_store_offset
|
|
27
|
+
uint32 :advance_width_mapping_offset
|
|
28
|
+
uint32 :lsb_mapping_offset
|
|
29
|
+
uint32 :rsb_mapping_offset
|
|
30
|
+
|
|
31
|
+
# Get version as a float
|
|
32
|
+
#
|
|
33
|
+
# @return [Float] Version number (e.g., 1.0)
|
|
34
|
+
def version
|
|
35
|
+
major_version + (minor_version / 10.0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Parse the item variation store
|
|
39
|
+
#
|
|
40
|
+
# @return [VariationCommon::ItemVariationStore, nil] Variation store
|
|
41
|
+
def item_variation_store
|
|
42
|
+
return @item_variation_store if defined?(@item_variation_store)
|
|
43
|
+
return @item_variation_store = nil if item_variation_store_offset.zero?
|
|
44
|
+
|
|
45
|
+
data = raw_data
|
|
46
|
+
offset = item_variation_store_offset
|
|
47
|
+
|
|
48
|
+
return @item_variation_store = nil if offset >= data.bytesize
|
|
49
|
+
|
|
50
|
+
store_data = data.byteslice(offset..-1)
|
|
51
|
+
@item_variation_store = VariationCommon::ItemVariationStore.read(store_data)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
warn "Failed to parse HVAR item variation store: #{e.message}"
|
|
54
|
+
@item_variation_store = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Parse advance width mapping
|
|
58
|
+
#
|
|
59
|
+
# @return [VariationCommon::DeltaSetIndexMap, nil] Advance width map
|
|
60
|
+
def advance_width_mapping
|
|
61
|
+
return @advance_width_mapping if defined?(@advance_width_mapping)
|
|
62
|
+
return @advance_width_mapping = nil if advance_width_mapping_offset.zero?
|
|
63
|
+
|
|
64
|
+
data = raw_data
|
|
65
|
+
offset = advance_width_mapping_offset
|
|
66
|
+
|
|
67
|
+
return @advance_width_mapping = nil if offset >= data.bytesize
|
|
68
|
+
|
|
69
|
+
map_data = data.byteslice(offset..-1)
|
|
70
|
+
@advance_width_mapping = VariationCommon::DeltaSetIndexMap.read(map_data)
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
warn "Failed to parse HVAR advance width mapping: #{e.message}"
|
|
73
|
+
@advance_width_mapping = nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Parse LSB (left side bearing) mapping
|
|
77
|
+
#
|
|
78
|
+
# @return [VariationCommon::DeltaSetIndexMap, nil] LSB map
|
|
79
|
+
def lsb_mapping
|
|
80
|
+
return @lsb_mapping if defined?(@lsb_mapping)
|
|
81
|
+
return @lsb_mapping = nil if lsb_mapping_offset.zero?
|
|
82
|
+
|
|
83
|
+
data = raw_data
|
|
84
|
+
offset = lsb_mapping_offset
|
|
85
|
+
|
|
86
|
+
return @lsb_mapping = nil if offset >= data.bytesize
|
|
87
|
+
|
|
88
|
+
map_data = data.byteslice(offset..-1)
|
|
89
|
+
@lsb_mapping = VariationCommon::DeltaSetIndexMap.read(map_data)
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
warn "Failed to parse HVAR LSB mapping: #{e.message}"
|
|
92
|
+
@lsb_mapping = nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Parse RSB (right side bearing) mapping
|
|
96
|
+
#
|
|
97
|
+
# @return [VariationCommon::DeltaSetIndexMap, nil] RSB map
|
|
98
|
+
def rsb_mapping
|
|
99
|
+
return @rsb_mapping if defined?(@rsb_mapping)
|
|
100
|
+
return @rsb_mapping = nil if rsb_mapping_offset.zero?
|
|
101
|
+
|
|
102
|
+
data = raw_data
|
|
103
|
+
offset = rsb_mapping_offset
|
|
104
|
+
|
|
105
|
+
return @rsb_mapping = nil if offset >= data.bytesize
|
|
106
|
+
|
|
107
|
+
map_data = data.byteslice(offset..-1)
|
|
108
|
+
@rsb_mapping = VariationCommon::DeltaSetIndexMap.read(map_data)
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
warn "Failed to parse HVAR RSB mapping: #{e.message}"
|
|
111
|
+
@rsb_mapping = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get advance width delta set for a glyph
|
|
115
|
+
#
|
|
116
|
+
# @param glyph_id [Integer] Glyph ID
|
|
117
|
+
# @return [Array<Integer>, nil] Delta values or nil
|
|
118
|
+
def advance_width_delta_set(glyph_id)
|
|
119
|
+
return nil unless item_variation_store
|
|
120
|
+
|
|
121
|
+
# If no mapping, use glyph_id directly
|
|
122
|
+
if advance_width_mapping.nil?
|
|
123
|
+
return item_variation_store.delta_set(0, glyph_id)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Use mapping to get delta set indices
|
|
127
|
+
map_data = advance_width_mapping.map_data
|
|
128
|
+
return nil if glyph_id >= map_data.length
|
|
129
|
+
|
|
130
|
+
delta_index = map_data[glyph_id]
|
|
131
|
+
outer_index = (delta_index >> 16) & 0xFFFF
|
|
132
|
+
inner_index = delta_index & 0xFFFF
|
|
133
|
+
|
|
134
|
+
item_variation_store.delta_set(outer_index, inner_index)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get LSB delta set for a glyph
|
|
138
|
+
#
|
|
139
|
+
# @param glyph_id [Integer] Glyph ID
|
|
140
|
+
# @return [Array<Integer>, nil] Delta values or nil
|
|
141
|
+
def lsb_delta_set(glyph_id)
|
|
142
|
+
return nil unless item_variation_store
|
|
143
|
+
|
|
144
|
+
# If no mapping, use glyph_id directly
|
|
145
|
+
if lsb_mapping.nil?
|
|
146
|
+
return item_variation_store.delta_set(0, glyph_id)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Use mapping to get delta set indices
|
|
150
|
+
map_data = lsb_mapping.map_data
|
|
151
|
+
return nil if glyph_id >= map_data.length
|
|
152
|
+
|
|
153
|
+
delta_index = map_data[glyph_id]
|
|
154
|
+
outer_index = (delta_index >> 16) & 0xFFFF
|
|
155
|
+
inner_index = delta_index & 0xFFFF
|
|
156
|
+
|
|
157
|
+
item_variation_store.delta_set(outer_index, inner_index)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get RSB delta set for a glyph
|
|
161
|
+
#
|
|
162
|
+
# @param glyph_id [Integer] Glyph ID
|
|
163
|
+
# @return [Array<Integer>, nil] Delta values or nil
|
|
164
|
+
def rsb_delta_set(glyph_id)
|
|
165
|
+
return nil unless item_variation_store
|
|
166
|
+
|
|
167
|
+
# If no mapping, use glyph_id directly
|
|
168
|
+
if rsb_mapping.nil?
|
|
169
|
+
return item_variation_store.delta_set(0, glyph_id)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Use mapping to get delta set indices
|
|
173
|
+
map_data = rsb_mapping.map_data
|
|
174
|
+
return nil if glyph_id >= map_data.length
|
|
175
|
+
|
|
176
|
+
delta_index = map_data[glyph_id]
|
|
177
|
+
outer_index = (delta_index >> 16) & 0xFFFF
|
|
178
|
+
inner_index = delta_index & 0xFFFF
|
|
179
|
+
|
|
180
|
+
item_variation_store.delta_set(outer_index, inner_index)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Check if table is valid
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] True if valid
|
|
186
|
+
def valid?
|
|
187
|
+
major_version == 1 && minor_version.zero?
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../binary/base_record"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
# Parser for the 'loca' (Index to Location) table
|
|
8
|
+
#
|
|
9
|
+
# The loca table provides offsets to glyph data in the glyf table.
|
|
10
|
+
# Each glyph has an entry in this table indicating where its data
|
|
11
|
+
# begins in the glyf table. An additional entry marks the end of the
|
|
12
|
+
# last glyph's data.
|
|
13
|
+
#
|
|
14
|
+
# The table has two formats:
|
|
15
|
+
# - Short format (0): uint16 offsets divided by 2 (actual offset = value × 2)
|
|
16
|
+
# - Long format (1): uint32 offsets used as-is
|
|
17
|
+
#
|
|
18
|
+
# The format is determined by head.indexToLocFormat:
|
|
19
|
+
# - 0 = short format (uint16, multiply by 2)
|
|
20
|
+
# - 1 = long format (uint32, use as-is)
|
|
21
|
+
#
|
|
22
|
+
# The table always contains (numGlyphs + 1) offsets, where the last
|
|
23
|
+
# offset marks the end of the last glyph's data in the glyf table.
|
|
24
|
+
#
|
|
25
|
+
# The table is context-dependent and requires:
|
|
26
|
+
# - indexToLocFormat from head table (format selection)
|
|
27
|
+
# - numGlyphs from maxp table (number of offsets to read)
|
|
28
|
+
#
|
|
29
|
+
# Reference: OpenType specification, loca table
|
|
30
|
+
# https://docs.microsoft.com/en-us/typography/opentype/spec/loca
|
|
31
|
+
#
|
|
32
|
+
# @example Parsing loca with context
|
|
33
|
+
# # Get required tables first
|
|
34
|
+
# head = font.table('head')
|
|
35
|
+
# maxp = font.table('maxp')
|
|
36
|
+
#
|
|
37
|
+
# # Parse loca with context
|
|
38
|
+
# data = font.read_table_data('loca')
|
|
39
|
+
# loca = Fontisan::Tables::Loca.read(data)
|
|
40
|
+
# loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
41
|
+
#
|
|
42
|
+
# # Get offset for a glyph
|
|
43
|
+
# offset = loca.offset_for(42)
|
|
44
|
+
# size = loca.size_of(42)
|
|
45
|
+
# is_empty = loca.empty?(42)
|
|
46
|
+
class Loca < Binary::BaseRecord
|
|
47
|
+
# Short format constant (from head.indexToLocFormat)
|
|
48
|
+
FORMAT_SHORT = 0
|
|
49
|
+
|
|
50
|
+
# Long format constant (from head.indexToLocFormat)
|
|
51
|
+
FORMAT_LONG = 1
|
|
52
|
+
|
|
53
|
+
# Store the raw data for deferred parsing
|
|
54
|
+
attr_accessor :raw_data
|
|
55
|
+
|
|
56
|
+
# Parsed offsets array
|
|
57
|
+
# @return [Array<Integer>] Array of glyph offsets in glyf table
|
|
58
|
+
attr_reader :offsets
|
|
59
|
+
|
|
60
|
+
# Format of the loca table (0 = short, 1 = long)
|
|
61
|
+
# @return [Integer] Format indicator
|
|
62
|
+
attr_reader :format
|
|
63
|
+
|
|
64
|
+
# Total number of glyphs from maxp table
|
|
65
|
+
# @return [Integer] Total glyph count
|
|
66
|
+
attr_reader :num_glyphs
|
|
67
|
+
|
|
68
|
+
# Override read to capture raw data
|
|
69
|
+
#
|
|
70
|
+
# @param io [IO, String] Input data
|
|
71
|
+
# @return [Loca] Parsed table instance
|
|
72
|
+
def self.read(io)
|
|
73
|
+
instance = new
|
|
74
|
+
|
|
75
|
+
# Handle nil or empty data gracefully
|
|
76
|
+
instance.raw_data = if io.nil?
|
|
77
|
+
"".b
|
|
78
|
+
elsif io.is_a?(String)
|
|
79
|
+
io
|
|
80
|
+
else
|
|
81
|
+
io.read || "".b
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
instance
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Parse the table with font context
|
|
88
|
+
#
|
|
89
|
+
# This method must be called after reading the table data, providing
|
|
90
|
+
# the indexToLocFormat from head and numGlyphs from maxp.
|
|
91
|
+
#
|
|
92
|
+
# @param index_to_loc_format [Integer] Format (0 = short, 1 = long) from head table
|
|
93
|
+
# @param num_glyphs [Integer] Total number of glyphs from maxp table
|
|
94
|
+
# @raise [ArgumentError] If context parameters are invalid
|
|
95
|
+
# @raise [Fontisan::CorruptedTableError] If table data is insufficient
|
|
96
|
+
def parse_with_context(index_to_loc_format, num_glyphs)
|
|
97
|
+
validate_context_params(index_to_loc_format, num_glyphs)
|
|
98
|
+
|
|
99
|
+
@format = index_to_loc_format
|
|
100
|
+
@num_glyphs = num_glyphs
|
|
101
|
+
|
|
102
|
+
io = StringIO.new(raw_data)
|
|
103
|
+
io.set_encoding(Encoding::BINARY)
|
|
104
|
+
|
|
105
|
+
# Number of offsets is numGlyphs + 1 (extra offset marks end of last glyph)
|
|
106
|
+
offset_count = num_glyphs + 1
|
|
107
|
+
|
|
108
|
+
@offsets = if short_format?
|
|
109
|
+
parse_short_offsets(io, offset_count)
|
|
110
|
+
else
|
|
111
|
+
parse_long_offsets(io, offset_count)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
validate_parsed_data!(io, offset_count)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get the offset for a specific glyph ID in the glyf table
|
|
118
|
+
#
|
|
119
|
+
# @param glyph_id [Integer] Glyph ID (0-based)
|
|
120
|
+
# @return [Integer, nil] Byte offset in glyf table, or nil if invalid ID
|
|
121
|
+
# @raise [RuntimeError] If table has not been parsed with context
|
|
122
|
+
#
|
|
123
|
+
# @example Getting glyph offset
|
|
124
|
+
# offset = loca.offset_for(0) # .notdef glyph offset
|
|
125
|
+
def offset_for(glyph_id)
|
|
126
|
+
raise "Table not parsed. Call parse_with_context first." unless @offsets
|
|
127
|
+
|
|
128
|
+
return nil if glyph_id >= num_glyphs || glyph_id.negative?
|
|
129
|
+
|
|
130
|
+
offsets[glyph_id]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Calculate the size of glyph data for a specific glyph ID
|
|
134
|
+
#
|
|
135
|
+
# The size is calculated as the difference between consecutive offsets:
|
|
136
|
+
# size = offsets[glyph_id + 1] - offsets[glyph_id]
|
|
137
|
+
#
|
|
138
|
+
# A size of 0 indicates an empty glyph (no outline data).
|
|
139
|
+
#
|
|
140
|
+
# @param glyph_id [Integer] Glyph ID (0-based)
|
|
141
|
+
# @return [Integer, nil] Size in bytes, or nil if invalid ID
|
|
142
|
+
# @raise [RuntimeError] If table has not been parsed with context
|
|
143
|
+
#
|
|
144
|
+
# @example Calculating glyph size
|
|
145
|
+
# size = loca.size_of(42) # Size of glyph 42 in bytes
|
|
146
|
+
def size_of(glyph_id)
|
|
147
|
+
raise "Table not parsed. Call parse_with_context first." unless @offsets
|
|
148
|
+
|
|
149
|
+
return nil if glyph_id >= num_glyphs || glyph_id.negative?
|
|
150
|
+
|
|
151
|
+
offsets[glyph_id + 1] - offsets[glyph_id]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check if a glyph has no outline data
|
|
155
|
+
#
|
|
156
|
+
# A glyph is empty when its size is 0, which occurs when consecutive
|
|
157
|
+
# offsets are equal. Empty glyphs are used for space characters and
|
|
158
|
+
# other non-visible glyphs.
|
|
159
|
+
#
|
|
160
|
+
# @param glyph_id [Integer] Glyph ID (0-based)
|
|
161
|
+
# @return [Boolean, nil] True if empty, false if has data, nil if invalid ID
|
|
162
|
+
# @raise [RuntimeError] If table has not been parsed with context
|
|
163
|
+
#
|
|
164
|
+
# @example Checking if glyph is empty
|
|
165
|
+
# is_empty = loca.empty?(32) # Check if space character is empty
|
|
166
|
+
def empty?(glyph_id)
|
|
167
|
+
size = size_of(glyph_id)
|
|
168
|
+
size&.zero?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Check if the table has been parsed with context
|
|
172
|
+
#
|
|
173
|
+
# @return [Boolean] True if parsed, false otherwise
|
|
174
|
+
def parsed?
|
|
175
|
+
!@offsets.nil?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Check if using short format (format 0)
|
|
179
|
+
#
|
|
180
|
+
# @return [Boolean] True if short format, false otherwise
|
|
181
|
+
def short_format?
|
|
182
|
+
format == FORMAT_SHORT
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Check if using long format (format 1)
|
|
186
|
+
#
|
|
187
|
+
# @return [Boolean] True if long format, false otherwise
|
|
188
|
+
def long_format?
|
|
189
|
+
format == FORMAT_LONG
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Get the expected size for this table
|
|
193
|
+
#
|
|
194
|
+
# @return [Integer, nil] Expected size in bytes, or nil if not parsed
|
|
195
|
+
def expected_size
|
|
196
|
+
return nil unless parsed?
|
|
197
|
+
|
|
198
|
+
offset_count = num_glyphs + 1
|
|
199
|
+
if short_format?
|
|
200
|
+
offset_count * 2 # uint16
|
|
201
|
+
else
|
|
202
|
+
offset_count * 4 # uint32
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
# Validate context parameters
|
|
209
|
+
#
|
|
210
|
+
# @param format [Integer] Format indicator
|
|
211
|
+
# @param num_glyphs [Integer] Total glyphs
|
|
212
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
213
|
+
def validate_context_params(format, num_glyphs)
|
|
214
|
+
if format.nil? || (format != FORMAT_SHORT && format != FORMAT_LONG)
|
|
215
|
+
raise ArgumentError,
|
|
216
|
+
"indexToLocFormat must be 0 (short) or 1 (long), " \
|
|
217
|
+
"got: #{format.inspect}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if num_glyphs.nil? || num_glyphs < 1
|
|
221
|
+
raise ArgumentError,
|
|
222
|
+
"numGlyphs must be >= 1, got: #{num_glyphs.inspect}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Parse short format offsets (uint16, multiply by 2)
|
|
227
|
+
#
|
|
228
|
+
# @param io [StringIO] Input stream
|
|
229
|
+
# @param count [Integer] Number of offsets to parse
|
|
230
|
+
# @return [Array<Integer>] Array of offsets
|
|
231
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
232
|
+
def parse_short_offsets(io, count)
|
|
233
|
+
offsets = []
|
|
234
|
+
count.times do |i|
|
|
235
|
+
value = read_uint16(io)
|
|
236
|
+
|
|
237
|
+
if value.nil?
|
|
238
|
+
raise Fontisan::CorruptedTableError,
|
|
239
|
+
"Insufficient data for short offset at index #{i}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Short offsets are divided by 2, so multiply to get actual offset
|
|
243
|
+
offsets << (value * 2)
|
|
244
|
+
end
|
|
245
|
+
offsets
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Parse long format offsets (uint32, use as-is)
|
|
249
|
+
#
|
|
250
|
+
# @param io [StringIO] Input stream
|
|
251
|
+
# @param count [Integer] Number of offsets to parse
|
|
252
|
+
# @return [Array<Integer>] Array of offsets
|
|
253
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
254
|
+
def parse_long_offsets(io, count)
|
|
255
|
+
offsets = []
|
|
256
|
+
count.times do |i|
|
|
257
|
+
value = read_uint32(io)
|
|
258
|
+
|
|
259
|
+
if value.nil?
|
|
260
|
+
raise Fontisan::CorruptedTableError,
|
|
261
|
+
"Insufficient data for long offset at index #{i}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
offsets << value
|
|
265
|
+
end
|
|
266
|
+
offsets
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Validate that all expected data was parsed
|
|
270
|
+
#
|
|
271
|
+
# @param io [StringIO] Input stream
|
|
272
|
+
# @param offset_count [Integer] Expected number of offsets
|
|
273
|
+
# @raise [Fontisan::CorruptedTableError] If data validation fails
|
|
274
|
+
def validate_parsed_data!(io, offset_count)
|
|
275
|
+
# Check that we parsed the expected number of offsets
|
|
276
|
+
if offsets.length != offset_count
|
|
277
|
+
raise Fontisan::CorruptedTableError,
|
|
278
|
+
"Expected #{offset_count} offsets, got #{offsets.length}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Check that offsets are monotonically increasing
|
|
282
|
+
offsets.each_cons(2).with_index do |(prev, curr), i|
|
|
283
|
+
if curr < prev
|
|
284
|
+
raise Fontisan::CorruptedTableError,
|
|
285
|
+
"Offsets are not monotonically increasing: " \
|
|
286
|
+
"offset[#{i}]=#{prev}, offset[#{i + 1}]=#{curr}"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Check for unexpected remaining data
|
|
291
|
+
remaining = io.read
|
|
292
|
+
if remaining && !remaining.empty? && remaining.length > 3
|
|
293
|
+
# Some fonts may have padding, only warn if significant
|
|
294
|
+
warn "Warning: loca table has #{remaining.length} unexpected " \
|
|
295
|
+
"bytes after parsing"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Read unsigned 16-bit integer
|
|
300
|
+
#
|
|
301
|
+
# @param io [StringIO] Input stream
|
|
302
|
+
# @return [Integer, nil] Value or nil if insufficient data
|
|
303
|
+
def read_uint16(io)
|
|
304
|
+
data = io.read(2)
|
|
305
|
+
return nil if data.nil? || data.length < 2
|
|
306
|
+
|
|
307
|
+
data.unpack1("n") # Big-endian unsigned 16-bit
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Read unsigned 32-bit integer
|
|
311
|
+
#
|
|
312
|
+
# @param io [StringIO] Input stream
|
|
313
|
+
# @return [Integer, nil] Value or nil if insufficient data
|
|
314
|
+
def read_uint32(io)
|
|
315
|
+
data = io.read(4)
|
|
316
|
+
return nil if data.nil? || data.length < 4
|
|
317
|
+
|
|
318
|
+
data.unpack1("N") # Big-endian unsigned 32-bit
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../binary/base_record"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
# BinData structure for the 'maxp' (Maximum Profile) table
|
|
8
|
+
#
|
|
9
|
+
# The maxp table contains memory and complexity limits for the font.
|
|
10
|
+
# It provides the number of glyphs and various maximum values needed
|
|
11
|
+
# for font rendering and processing.
|
|
12
|
+
#
|
|
13
|
+
# The table has two versions:
|
|
14
|
+
# - Version 0.5 (0x00005000): CFF fonts - only version and numGlyphs
|
|
15
|
+
# - Version 1.0 (0x00010000): TrueType fonts - includes additional fields
|
|
16
|
+
#
|
|
17
|
+
# Version 1.0 fields provide information about:
|
|
18
|
+
# - Glyph outline complexity (points, contours)
|
|
19
|
+
# - Composite glyph structure
|
|
20
|
+
# - TrueType instruction limitations
|
|
21
|
+
#
|
|
22
|
+
# Reference: OpenType specification, maxp table
|
|
23
|
+
# https://docs.microsoft.com/en-us/typography/opentype/spec/maxp
|
|
24
|
+
#
|
|
25
|
+
# @example Reading a maxp table
|
|
26
|
+
# data = File.binread("font.ttf", size, maxp_offset)
|
|
27
|
+
# maxp = Fontisan::Tables::Maxp.read(data)
|
|
28
|
+
# puts maxp.num_glyphs # => 512
|
|
29
|
+
# puts maxp.version # => 1.0 or 0.5
|
|
30
|
+
# puts maxp.truetype? # => true or false
|
|
31
|
+
class Maxp < Binary::BaseRecord
|
|
32
|
+
# Version 0.5 constant (CFF fonts)
|
|
33
|
+
VERSION_0_5 = 0x00005000
|
|
34
|
+
|
|
35
|
+
# Version 1.0 constant (TrueType fonts)
|
|
36
|
+
VERSION_1_0 = 0x00010000
|
|
37
|
+
|
|
38
|
+
# Minimum table size for version 0.5 (4 + 2 = 6 bytes)
|
|
39
|
+
TABLE_SIZE_V0_5 = 6
|
|
40
|
+
|
|
41
|
+
# Full table size for version 1.0 (4 + 2 + 13×2 = 32 bytes)
|
|
42
|
+
TABLE_SIZE_V1_0 = 32
|
|
43
|
+
|
|
44
|
+
# Version as 16.16 fixed-point (stored as int32)
|
|
45
|
+
# 0x00010000 for version 1.0 (TrueType)
|
|
46
|
+
# 0x00005000 for version 0.5 (CFF)
|
|
47
|
+
int32 :version_raw
|
|
48
|
+
|
|
49
|
+
# Total number of glyphs in the font
|
|
50
|
+
# Must be >= 1 (at minimum, .notdef must be present)
|
|
51
|
+
uint16 :num_glyphs
|
|
52
|
+
|
|
53
|
+
# The following fields are only present in version 1.0 (TrueType fonts)
|
|
54
|
+
|
|
55
|
+
# Maximum points in a non-composite glyph
|
|
56
|
+
uint16 :max_points, onlyif: -> { version_raw == VERSION_1_0 }
|
|
57
|
+
|
|
58
|
+
# Maximum contours in a non-composite glyph
|
|
59
|
+
uint16 :max_contours, onlyif: -> { version_raw == VERSION_1_0 }
|
|
60
|
+
|
|
61
|
+
# Maximum points in a composite glyph
|
|
62
|
+
uint16 :max_composite_points, onlyif: -> { version_raw == VERSION_1_0 }
|
|
63
|
+
|
|
64
|
+
# Maximum contours in a composite glyph
|
|
65
|
+
uint16 :max_composite_contours, onlyif: -> { version_raw == VERSION_1_0 }
|
|
66
|
+
|
|
67
|
+
# Maximum zones (1 or 2, depending on instructions)
|
|
68
|
+
# 1 = no twilight zone, 2 = twilight zone present
|
|
69
|
+
uint16 :max_zones, onlyif: -> { version_raw == VERSION_1_0 }
|
|
70
|
+
|
|
71
|
+
# Maximum points used in twilight zone (Z0)
|
|
72
|
+
uint16 :max_twilight_points, onlyif: -> { version_raw == VERSION_1_0 }
|
|
73
|
+
|
|
74
|
+
# Maximum storage area locations
|
|
75
|
+
uint16 :max_storage, onlyif: -> { version_raw == VERSION_1_0 }
|
|
76
|
+
|
|
77
|
+
# Maximum function definitions
|
|
78
|
+
uint16 :max_function_defs, onlyif: -> { version_raw == VERSION_1_0 }
|
|
79
|
+
|
|
80
|
+
# Maximum instruction definitions
|
|
81
|
+
uint16 :max_instruction_defs, onlyif: -> { version_raw == VERSION_1_0 }
|
|
82
|
+
|
|
83
|
+
# Maximum stack depth
|
|
84
|
+
uint16 :max_stack_elements, onlyif: -> { version_raw == VERSION_1_0 }
|
|
85
|
+
|
|
86
|
+
# Maximum byte count for glyph instructions
|
|
87
|
+
uint16 :max_size_of_instructions, onlyif: -> {
|
|
88
|
+
version_raw == VERSION_1_0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Maximum component elements in a composite glyph
|
|
92
|
+
uint16 :max_component_elements, onlyif: -> { version_raw == VERSION_1_0 }
|
|
93
|
+
|
|
94
|
+
# Maximum levels of recursion in composite glyphs
|
|
95
|
+
# 0 if font has no composite glyphs
|
|
96
|
+
uint16 :max_component_depth, onlyif: -> { version_raw == VERSION_1_0 }
|
|
97
|
+
|
|
98
|
+
# Convert version from fixed-point to float
|
|
99
|
+
#
|
|
100
|
+
# Version 0.5 (0x00005000) is a special case, not standard 16.16 fixed-point
|
|
101
|
+
#
|
|
102
|
+
# @return [Float] Version number (1.0 or 0.5)
|
|
103
|
+
def version
|
|
104
|
+
case version_raw
|
|
105
|
+
when VERSION_0_5
|
|
106
|
+
0.5
|
|
107
|
+
when VERSION_1_0
|
|
108
|
+
1.0
|
|
109
|
+
else
|
|
110
|
+
fixed_to_float(version_raw)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if this is version 1.0 (TrueType)
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] True if version 1.0, false otherwise
|
|
117
|
+
def version_1_0?
|
|
118
|
+
version_raw == VERSION_1_0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Check if this is version 0.5 (CFF)
|
|
122
|
+
#
|
|
123
|
+
# @return [Boolean] True if version 0.5, false otherwise
|
|
124
|
+
def version_0_5?
|
|
125
|
+
version_raw == VERSION_0_5
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Check if this is a TrueType font (alias for version_1_0?)
|
|
129
|
+
#
|
|
130
|
+
# @return [Boolean] True if TrueType font, false otherwise
|
|
131
|
+
def truetype?
|
|
132
|
+
version_1_0?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check if this is a CFF font (alias for version_0_5?)
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean] True if CFF font, false otherwise
|
|
138
|
+
def cff?
|
|
139
|
+
version_0_5?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Check if the table is valid
|
|
143
|
+
#
|
|
144
|
+
# @return [Boolean] True if valid, false otherwise
|
|
145
|
+
def valid?
|
|
146
|
+
# Version must be either 0.5 or 1.0
|
|
147
|
+
return false unless version_0_5? || version_1_0?
|
|
148
|
+
|
|
149
|
+
# Number of glyphs must be at least 1
|
|
150
|
+
return false unless num_glyphs >= 1
|
|
151
|
+
|
|
152
|
+
# For version 1.0, maxZones must be 1 or 2
|
|
153
|
+
if version_1_0? && max_zones && !(max_zones >= 1 && max_zones <= 2)
|
|
154
|
+
return false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
true
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Validate the table and raise error if invalid
|
|
161
|
+
#
|
|
162
|
+
# @raise [Fontisan::CorruptedTableError] If table is invalid
|
|
163
|
+
def validate_structure!
|
|
164
|
+
unless version_0_5? || version_1_0?
|
|
165
|
+
raise Fontisan::CorruptedTableError,
|
|
166
|
+
"Invalid maxp version: expected 0x00005000 (0.5) or " \
|
|
167
|
+
"0x00010000 (1.0), got 0x#{version_raw.to_s(16).upcase}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
unless num_glyphs >= 1
|
|
171
|
+
raise Fontisan::CorruptedTableError,
|
|
172
|
+
"Invalid number of glyphs: must be >= 1, got #{num_glyphs}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if version_1_0? && max_zones && (max_zones < 1 || max_zones > 2)
|
|
176
|
+
raise Fontisan::CorruptedTableError,
|
|
177
|
+
"Invalid maxZones: must be 1 or 2, got #{max_zones}"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Alias for compatibility
|
|
182
|
+
alias validate! validate_structure!
|
|
183
|
+
|
|
184
|
+
# Get the expected table size based on version
|
|
185
|
+
#
|
|
186
|
+
# @return [Integer] Expected size in bytes
|
|
187
|
+
def expected_size
|
|
188
|
+
version_1_0? ? TABLE_SIZE_V1_0 : TABLE_SIZE_V0_5
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|