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,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff2
|
|
6
|
+
# Operand stack manager for CFF2 CharStrings
|
|
7
|
+
#
|
|
8
|
+
# This class manages the operand stack for CFF2 CharStrings, with special
|
|
9
|
+
# handling for blend operations that mix base values and deltas.
|
|
10
|
+
#
|
|
11
|
+
# In CFF2, the blend operator takes operands in the format:
|
|
12
|
+
# [base1, delta1_axis1, delta1_axis2, ..., base2, delta2_axis1, ..., K, N]
|
|
13
|
+
#
|
|
14
|
+
# Where:
|
|
15
|
+
# - K = number of values to blend
|
|
16
|
+
# - N = number of variation axes
|
|
17
|
+
#
|
|
18
|
+
# The stack manager separates base values from deltas and applies blend
|
|
19
|
+
# operations to produce final values based on variation coordinates.
|
|
20
|
+
#
|
|
21
|
+
# @example Managing a blend operation
|
|
22
|
+
# stack = OperandStack.new(num_axes: 2)
|
|
23
|
+
# stack.push(100, 10, 5) # base=100, deltas=[10, 5]
|
|
24
|
+
# stack.push(200, 20, 10) # base=200, deltas=[20, 10]
|
|
25
|
+
# blended = stack.apply_blend(k: 2, coordinates: { "wght" => 0.5, "wdth" => 0.3 })
|
|
26
|
+
# # => [105.0, 206.0] # base + (delta * scalar)
|
|
27
|
+
class OperandStack
|
|
28
|
+
# @return [Array<Numeric>] The operand stack
|
|
29
|
+
attr_reader :stack
|
|
30
|
+
|
|
31
|
+
# @return [Integer] Number of variation axes
|
|
32
|
+
attr_reader :num_axes
|
|
33
|
+
|
|
34
|
+
# @return [Array<Hash>] Blend values (base + deltas)
|
|
35
|
+
attr_reader :blend_values
|
|
36
|
+
|
|
37
|
+
# Initialize operand stack
|
|
38
|
+
#
|
|
39
|
+
# @param num_axes [Integer] Number of variation axes (default 0)
|
|
40
|
+
def initialize(num_axes: 0)
|
|
41
|
+
@stack = []
|
|
42
|
+
@num_axes = num_axes
|
|
43
|
+
@blend_values = []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Push a value onto the stack
|
|
47
|
+
#
|
|
48
|
+
# @param values [Numeric] Values to push
|
|
49
|
+
def push(*values)
|
|
50
|
+
@stack.concat(values)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Pop a value from the stack
|
|
54
|
+
#
|
|
55
|
+
# @return [Numeric, nil] Popped value or nil if empty
|
|
56
|
+
def pop
|
|
57
|
+
@stack.pop
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Pop multiple values from the stack
|
|
61
|
+
#
|
|
62
|
+
# @param count [Integer] Number of values to pop
|
|
63
|
+
# @return [Array<Numeric>] Popped values
|
|
64
|
+
def pop_many(count)
|
|
65
|
+
return [] if count <= 0 || @stack.empty?
|
|
66
|
+
|
|
67
|
+
@stack.pop(count)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Shift a value from the front of the stack
|
|
71
|
+
#
|
|
72
|
+
# @return [Numeric, nil] Shifted value or nil if empty
|
|
73
|
+
def shift
|
|
74
|
+
@stack.shift
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get the top value without popping
|
|
78
|
+
#
|
|
79
|
+
# @return [Numeric, nil] Top value or nil if empty
|
|
80
|
+
def peek
|
|
81
|
+
@stack.last
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get stack size
|
|
85
|
+
#
|
|
86
|
+
# @return [Integer] Number of values on stack
|
|
87
|
+
def size
|
|
88
|
+
@stack.size
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check if stack is empty
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean] True if empty
|
|
94
|
+
def empty?
|
|
95
|
+
@stack.empty?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Clear the stack
|
|
99
|
+
def clear
|
|
100
|
+
@stack.clear
|
|
101
|
+
@blend_values.clear
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Apply blend operation
|
|
105
|
+
#
|
|
106
|
+
# This pops K * (N + 1) + 2 operands from the stack, where:
|
|
107
|
+
# - K = number of values to blend
|
|
108
|
+
# - N = number of axes
|
|
109
|
+
# - Last 2 values are K and N themselves
|
|
110
|
+
#
|
|
111
|
+
# @param scalars [Array<Float>] Variation scalars for each axis
|
|
112
|
+
# @return [Array<Float>] Blended values
|
|
113
|
+
def apply_blend(scalars = [])
|
|
114
|
+
# Pop N and K
|
|
115
|
+
n = pop.to_i
|
|
116
|
+
k = pop.to_i
|
|
117
|
+
|
|
118
|
+
# Validate
|
|
119
|
+
required_operands = k * (n + 1)
|
|
120
|
+
if size < required_operands
|
|
121
|
+
warn "Blend requires #{required_operands} operands, got #{size}"
|
|
122
|
+
clear
|
|
123
|
+
return []
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Extract operands (base + deltas for each value)
|
|
127
|
+
blend_operands = pop_many(required_operands).reverse
|
|
128
|
+
|
|
129
|
+
# Process each value to blend
|
|
130
|
+
blended_values = []
|
|
131
|
+
k.times do |i|
|
|
132
|
+
offset = i * (n + 1)
|
|
133
|
+
base = blend_operands[offset]
|
|
134
|
+
deltas = blend_operands[offset + 1, n] || []
|
|
135
|
+
|
|
136
|
+
# Apply blend: result = base + sum(delta[i] * scalar[i])
|
|
137
|
+
blended = base.to_f
|
|
138
|
+
deltas.each_with_index do |delta, axis_index|
|
|
139
|
+
scalar = scalars[axis_index] || 0.0
|
|
140
|
+
blended += delta.to_f * scalar
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Store blend info for debugging/inspection
|
|
144
|
+
@blend_values << {
|
|
145
|
+
base: base,
|
|
146
|
+
deltas: deltas,
|
|
147
|
+
blended: blended,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
blended_values << blended
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Push blended values back onto stack
|
|
154
|
+
push(*blended_values)
|
|
155
|
+
|
|
156
|
+
blended_values
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Extract blend data without applying
|
|
160
|
+
#
|
|
161
|
+
# This is used when we need to store blend operations for later
|
|
162
|
+
# application with specific coordinates.
|
|
163
|
+
#
|
|
164
|
+
# @return [Hash] Blend operation data
|
|
165
|
+
def extract_blend_data
|
|
166
|
+
# Pop N and K
|
|
167
|
+
n = pop.to_i
|
|
168
|
+
k = pop.to_i
|
|
169
|
+
|
|
170
|
+
# Validate
|
|
171
|
+
required_operands = k * (n + 1)
|
|
172
|
+
if size < required_operands
|
|
173
|
+
warn "Blend requires #{required_operands} operands, got #{size}"
|
|
174
|
+
clear
|
|
175
|
+
return nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Extract operands
|
|
179
|
+
blend_operands = pop_many(required_operands).reverse
|
|
180
|
+
|
|
181
|
+
# Parse into base + deltas structure
|
|
182
|
+
blends = []
|
|
183
|
+
k.times do |i|
|
|
184
|
+
offset = i * (n + 1)
|
|
185
|
+
base = blend_operands[offset]
|
|
186
|
+
deltas = blend_operands[offset + 1, n] || []
|
|
187
|
+
|
|
188
|
+
blends << {
|
|
189
|
+
base: base,
|
|
190
|
+
deltas: deltas,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Push base value back (will be blended later)
|
|
194
|
+
push(base)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
num_values: k,
|
|
199
|
+
num_axes: n,
|
|
200
|
+
blends: blends,
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Get all values on the stack
|
|
205
|
+
#
|
|
206
|
+
# @return [Array<Numeric>] Stack contents
|
|
207
|
+
def to_a
|
|
208
|
+
@stack.dup
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Get string representation for debugging
|
|
212
|
+
#
|
|
213
|
+
# @return [String] Stack contents as string
|
|
214
|
+
def inspect
|
|
215
|
+
"#<OperandStack size=#{size} values=#{@stack.inspect}>"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Get blend value history
|
|
219
|
+
#
|
|
220
|
+
# @return [Array<Hash>] Blend values that have been calculated
|
|
221
|
+
def blend_history
|
|
222
|
+
@blend_values.dup
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Reset blend history
|
|
226
|
+
def reset_blend_history
|
|
227
|
+
@blend_values.clear
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../binary/base_record"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# Parser for the 'CFF2' (Compact Font Format 2) table
|
|
9
|
+
#
|
|
10
|
+
# CFF2 is used primarily in variable fonts with PostScript outlines.
|
|
11
|
+
# Key differences from CFF:
|
|
12
|
+
# - No Name INDEX (font names come from name table)
|
|
13
|
+
# - No Encoding or Charset (use cmap table instead)
|
|
14
|
+
# - Support for blend operators in CharStrings for variations
|
|
15
|
+
# - Different default values in DICTs
|
|
16
|
+
#
|
|
17
|
+
# Reference: Adobe Technical Note #5177
|
|
18
|
+
#
|
|
19
|
+
# @example Reading a CFF2 table
|
|
20
|
+
# data = font.table_data("CFF2")
|
|
21
|
+
# cff2 = Fontisan::Tables::Cff2.read(data)
|
|
22
|
+
# num_glyphs = cff2.glyph_count
|
|
23
|
+
class Cff2 < Binary::BaseRecord
|
|
24
|
+
# CFF2 header structure
|
|
25
|
+
class Header < Binary::BaseRecord
|
|
26
|
+
uint8 :major_version
|
|
27
|
+
uint8 :minor_version
|
|
28
|
+
uint8 :header_size
|
|
29
|
+
uint16 :top_dict_length
|
|
30
|
+
|
|
31
|
+
# Check if version is valid
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] True if version is 2.0
|
|
34
|
+
def valid?
|
|
35
|
+
major_version == 2 && minor_version.zero?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Parse the CFF2 table
|
|
40
|
+
#
|
|
41
|
+
# @return [self]
|
|
42
|
+
def parse
|
|
43
|
+
return self if @parsed
|
|
44
|
+
|
|
45
|
+
@header = parse_header
|
|
46
|
+
@global_subr_index = parse_global_subr_index
|
|
47
|
+
@top_dict = parse_top_dict
|
|
48
|
+
@charstrings_index = parse_charstrings_index
|
|
49
|
+
|
|
50
|
+
@parsed = true
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get the CFF2 header
|
|
55
|
+
#
|
|
56
|
+
# @return [Header] Header structure
|
|
57
|
+
def header
|
|
58
|
+
parse unless @parsed
|
|
59
|
+
@header
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get glyph count from font's maxp table
|
|
63
|
+
#
|
|
64
|
+
# CFF2 doesn't store glyph count internally - it relies on the maxp table
|
|
65
|
+
#
|
|
66
|
+
# @return [Integer] Number of glyphs (requires access to font's maxp)
|
|
67
|
+
def glyph_count
|
|
68
|
+
# This needs to be set externally or retrieved from maxp table
|
|
69
|
+
# For now, return a default that indicates it needs to be set
|
|
70
|
+
@glyph_count || 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Set glyph count (from maxp table)
|
|
74
|
+
#
|
|
75
|
+
# @param count [Integer] Number of glyphs
|
|
76
|
+
def glyph_count=(count)
|
|
77
|
+
@glyph_count = count
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Set number of variation axes (from fvar table)
|
|
81
|
+
#
|
|
82
|
+
# @param count [Integer] Number of axes
|
|
83
|
+
def num_axes=(count)
|
|
84
|
+
@num_axes = count
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get number of variation axes
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer] Number of axes
|
|
90
|
+
def num_axes
|
|
91
|
+
@num_axes || 0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get CharString for a specific glyph
|
|
95
|
+
#
|
|
96
|
+
# @param glyph_id [Integer] Glyph ID
|
|
97
|
+
# @return [CharstringParser, nil] CharString object or nil
|
|
98
|
+
def charstring_for_glyph(glyph_id)
|
|
99
|
+
parse unless @parsed
|
|
100
|
+
return nil if @charstrings_index.nil?
|
|
101
|
+
return nil if glyph_id >= @charstrings_index.count
|
|
102
|
+
|
|
103
|
+
# Get CharString data from INDEX
|
|
104
|
+
charstring_data = @charstrings_index[glyph_id]
|
|
105
|
+
return nil if charstring_data.nil?
|
|
106
|
+
|
|
107
|
+
# Parse with CFF2 CharString parser
|
|
108
|
+
require_relative "cff2/charstring_parser"
|
|
109
|
+
CharstringParser.new(
|
|
110
|
+
charstring_data,
|
|
111
|
+
@num_axes,
|
|
112
|
+
@global_subr_index,
|
|
113
|
+
nil, # local subrs (CFF2 may not have them)
|
|
114
|
+
0 # vsindex
|
|
115
|
+
).parse
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get all CharStrings
|
|
119
|
+
#
|
|
120
|
+
# @return [Array<CharstringParser>] Array of parsed CharStrings
|
|
121
|
+
def charstrings
|
|
122
|
+
return [] unless @charstrings_index
|
|
123
|
+
|
|
124
|
+
@charstrings_index.count.times.map do |glyph_id|
|
|
125
|
+
charstring_for_glyph(glyph_id)
|
|
126
|
+
end.compact
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if table is valid
|
|
130
|
+
#
|
|
131
|
+
# @return [Boolean] True if valid CFF2 table
|
|
132
|
+
def valid?
|
|
133
|
+
header.valid?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# Parse CFF2 header
|
|
139
|
+
#
|
|
140
|
+
# @return [Header] Parsed header
|
|
141
|
+
def parse_header
|
|
142
|
+
data = raw_data
|
|
143
|
+
return nil if data.nil? || data.bytesize < 5
|
|
144
|
+
|
|
145
|
+
Header.read(data.byteslice(0, 5))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Parse Global Subr INDEX
|
|
149
|
+
#
|
|
150
|
+
# @return [Cff::Index] Global subroutines INDEX
|
|
151
|
+
def parse_global_subr_index
|
|
152
|
+
# CFF2 has a Global Subr INDEX after the header
|
|
153
|
+
data = raw_data
|
|
154
|
+
return nil unless @header
|
|
155
|
+
|
|
156
|
+
offset = @header.header_size
|
|
157
|
+
|
|
158
|
+
# Global Subr INDEX follows header
|
|
159
|
+
io = StringIO.new(data)
|
|
160
|
+
io.seek(offset)
|
|
161
|
+
|
|
162
|
+
require_relative "cff/index"
|
|
163
|
+
Cff::Index.new(io, start_offset: offset)
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
warn "Failed to parse Global Subr INDEX: #{e.message}"
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Parse Top DICT
|
|
170
|
+
#
|
|
171
|
+
# @return [Hash] Top DICT data
|
|
172
|
+
def parse_top_dict
|
|
173
|
+
# CFF2 Top DICT follows the header (length specified in header)
|
|
174
|
+
data = raw_data
|
|
175
|
+
return {} unless @header
|
|
176
|
+
|
|
177
|
+
offset = @header.header_size
|
|
178
|
+
length = @header.top_dict_length
|
|
179
|
+
|
|
180
|
+
return {} if offset + length > data.bytesize
|
|
181
|
+
|
|
182
|
+
top_dict_data = data.byteslice(offset, length)
|
|
183
|
+
|
|
184
|
+
# Parse Top DICT (simplified for now)
|
|
185
|
+
# Full implementation would parse DICT operators
|
|
186
|
+
parse_dict(top_dict_data)
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
warn "Failed to parse Top DICT: #{e.message}"
|
|
189
|
+
{}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Parse CharStrings INDEX
|
|
193
|
+
#
|
|
194
|
+
# @return [Cff::Index, nil] CharStrings INDEX
|
|
195
|
+
def parse_charstrings_index
|
|
196
|
+
# CharStrings INDEX location is specified in Top DICT
|
|
197
|
+
# For now, we'll try to find it after Global Subr INDEX
|
|
198
|
+
data = raw_data
|
|
199
|
+
return nil unless @header
|
|
200
|
+
|
|
201
|
+
# Calculate offset after header + global subr
|
|
202
|
+
offset = @header.header_size
|
|
203
|
+
|
|
204
|
+
# Skip Global Subr INDEX
|
|
205
|
+
if @global_subr_index
|
|
206
|
+
offset += calculate_index_size(@global_subr_index)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Skip Top DICT
|
|
210
|
+
offset += @header.top_dict_length
|
|
211
|
+
|
|
212
|
+
io = StringIO.new(data)
|
|
213
|
+
io.seek(offset)
|
|
214
|
+
|
|
215
|
+
require_relative "cff/index"
|
|
216
|
+
Cff::Index.new(io, start_offset: offset)
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
warn "Failed to parse CharStrings INDEX: #{e.message}"
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Parse a DICT structure
|
|
223
|
+
#
|
|
224
|
+
# @param data [String] DICT data
|
|
225
|
+
# @return [Hash] Parsed operators and values
|
|
226
|
+
def parse_dict(data)
|
|
227
|
+
dict = {}
|
|
228
|
+
io = StringIO.new(data)
|
|
229
|
+
io.set_encoding(Encoding::BINARY)
|
|
230
|
+
|
|
231
|
+
operands = []
|
|
232
|
+
|
|
233
|
+
until io.eof?
|
|
234
|
+
byte = io.getbyte
|
|
235
|
+
|
|
236
|
+
if byte <= 21 && ![12, 28, 29, 30, 31].include?(byte)
|
|
237
|
+
# Operator
|
|
238
|
+
operator = byte
|
|
239
|
+
if operator == 12
|
|
240
|
+
operator = [12, io.getbyte]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
dict[operator] = operands.dup
|
|
244
|
+
operands.clear
|
|
245
|
+
else
|
|
246
|
+
# Operand (number)
|
|
247
|
+
io.pos -= 1
|
|
248
|
+
operands << read_dict_number(io)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
dict
|
|
253
|
+
rescue StandardError
|
|
254
|
+
{}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Read a number from DICT data
|
|
258
|
+
#
|
|
259
|
+
# @param io [StringIO] Input stream
|
|
260
|
+
# @return [Integer, Float] Number value
|
|
261
|
+
def read_dict_number(io)
|
|
262
|
+
byte = io.getbyte
|
|
263
|
+
|
|
264
|
+
case byte
|
|
265
|
+
when 28
|
|
266
|
+
# 3-byte signed integer
|
|
267
|
+
b1 = io.getbyte
|
|
268
|
+
b2 = io.getbyte
|
|
269
|
+
value = (b1 << 8) | b2
|
|
270
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
271
|
+
when 29
|
|
272
|
+
# 5-byte signed integer
|
|
273
|
+
bytes = io.read(4)
|
|
274
|
+
bytes.unpack1("l>")
|
|
275
|
+
when 30
|
|
276
|
+
# Real number (nibble-based)
|
|
277
|
+
read_real_number(io)
|
|
278
|
+
when 32..246
|
|
279
|
+
byte - 139
|
|
280
|
+
when 247..250
|
|
281
|
+
b2 = io.getbyte
|
|
282
|
+
(byte - 247) * 256 + b2 + 108
|
|
283
|
+
when 251..254
|
|
284
|
+
b2 = io.getbyte
|
|
285
|
+
-(byte - 251) * 256 - b2 - 108
|
|
286
|
+
else
|
|
287
|
+
0
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Read a real number from DICT
|
|
292
|
+
#
|
|
293
|
+
# @param io [StringIO] Input stream
|
|
294
|
+
# @return [Float] Real number
|
|
295
|
+
def read_real_number(io)
|
|
296
|
+
nibbles = []
|
|
297
|
+
loop do
|
|
298
|
+
byte = io.getbyte
|
|
299
|
+
nibbles << ((byte >> 4) & 0x0F)
|
|
300
|
+
nibbles << (byte & 0x0F)
|
|
301
|
+
break if (byte & 0x0F) == 0x0F
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Convert nibbles to string
|
|
305
|
+
str = ""
|
|
306
|
+
nibbles.each do |nibble|
|
|
307
|
+
case nibble
|
|
308
|
+
when 0..9 then str << nibble.to_s
|
|
309
|
+
when 0x0A then str << "."
|
|
310
|
+
when 0x0B then str << "E"
|
|
311
|
+
when 0x0C then str << "E-"
|
|
312
|
+
when 0x0E then str << "-"
|
|
313
|
+
when 0x0F then break
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
str.to_f
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Calculate size of an INDEX structure
|
|
321
|
+
#
|
|
322
|
+
# @param index [Cff::Index] INDEX structure
|
|
323
|
+
# @return [Integer] Size in bytes
|
|
324
|
+
def calculate_index_size(index)
|
|
325
|
+
return 2 if index.count.zero? # Just count field
|
|
326
|
+
|
|
327
|
+
# count (2) + offSize (1) + offsets + data
|
|
328
|
+
count = index.count
|
|
329
|
+
data_size = index.instance_variable_get(:@data_size) || 0
|
|
330
|
+
off_size = index.instance_variable_get(:@off_size) || 4
|
|
331
|
+
|
|
332
|
+
2 + 1 + ((count + 1) * off_size) + data_size
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Load CFF2 subcomponents
|
|
339
|
+
require_relative "cff2/charstring_parser"
|
|
340
|
+
require_relative "cff2/blend_operator"
|
|
341
|
+
require_relative "cff2/operand_stack"
|