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,591 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../../binary/base_record"
|
|
5
|
+
require_relative "../cff/charstring"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Tables
|
|
9
|
+
class Cff2
|
|
10
|
+
# Type 2 CharString parser for CFF2 (variable fonts)
|
|
11
|
+
#
|
|
12
|
+
# CFF2 CharStrings extend Type 2 CharStrings with the blend operator
|
|
13
|
+
# for variation support. The blend operator applies variation deltas
|
|
14
|
+
# to base values based on design space coordinates.
|
|
15
|
+
#
|
|
16
|
+
# Blend Operator (operator 16):
|
|
17
|
+
# - Takes N*K+1 operands where:
|
|
18
|
+
# - N = number of design variation axes
|
|
19
|
+
# - K = number of values to blend
|
|
20
|
+
# - Format: [v1, Δv1_axis1, Δv1_axis2, ..., v2, Δv2_axis1, ..., K, N, blend]
|
|
21
|
+
# - Produces K blended values on the stack
|
|
22
|
+
#
|
|
23
|
+
# Example for 2 axes (wght, wdth) blending 3 values:
|
|
24
|
+
# Input: [100, 10, 5, 200, 20, 10, 50, 5, 2, 3, 2, blend]
|
|
25
|
+
# - v1=100 with deltas [10, 5]
|
|
26
|
+
# - v2=200 with deltas [20, 10]
|
|
27
|
+
# - v3=50 with deltas [5, 2]
|
|
28
|
+
# - K=3 (number of values), N=2 (number of axes)
|
|
29
|
+
#
|
|
30
|
+
# Reference: Adobe Technical Note #5177 (CFF2 specification)
|
|
31
|
+
#
|
|
32
|
+
# @example Parsing a CFF2 CharString with blend
|
|
33
|
+
# parser = Fontisan::Tables::Cff2::CharstringParser.new(
|
|
34
|
+
# data, num_axes, variation_store
|
|
35
|
+
# )
|
|
36
|
+
# charstring = parser.parse
|
|
37
|
+
# puts charstring.path
|
|
38
|
+
# puts charstring.blend_data
|
|
39
|
+
class CharstringParser
|
|
40
|
+
# @return [String] Binary CharString data
|
|
41
|
+
attr_reader :data
|
|
42
|
+
|
|
43
|
+
# @return [Integer] Number of variation axes
|
|
44
|
+
attr_reader :num_axes
|
|
45
|
+
|
|
46
|
+
# @return [Array<Hash>] Parsed path commands
|
|
47
|
+
attr_reader :path
|
|
48
|
+
|
|
49
|
+
# @return [Array<Hash>] Blend operator data
|
|
50
|
+
attr_reader :blend_data
|
|
51
|
+
|
|
52
|
+
# @return [Float] Current X coordinate
|
|
53
|
+
attr_reader :x
|
|
54
|
+
|
|
55
|
+
# @return [Float] Current Y coordinate
|
|
56
|
+
attr_reader :y
|
|
57
|
+
|
|
58
|
+
# @return [Integer, nil] Glyph width
|
|
59
|
+
attr_reader :width
|
|
60
|
+
|
|
61
|
+
# CFF2-specific operators
|
|
62
|
+
BLEND_OPERATOR = 16
|
|
63
|
+
|
|
64
|
+
# Initialize parser
|
|
65
|
+
#
|
|
66
|
+
# @param data [String] Binary CharString data
|
|
67
|
+
# @param num_axes [Integer] Number of variation axes (from fvar)
|
|
68
|
+
# @param global_subrs [Cff::Index, nil] Global subroutines INDEX
|
|
69
|
+
# @param local_subrs [Cff::Index, nil] Local subroutines INDEX
|
|
70
|
+
# @param vsindex [Integer] Variation store index (default 0)
|
|
71
|
+
def initialize(data, num_axes = 0, global_subrs = nil, local_subrs = nil, vsindex = 0)
|
|
72
|
+
@data = data
|
|
73
|
+
@num_axes = num_axes
|
|
74
|
+
@global_subrs = global_subrs
|
|
75
|
+
@local_subrs = local_subrs
|
|
76
|
+
@vsindex = vsindex
|
|
77
|
+
|
|
78
|
+
@path = []
|
|
79
|
+
@blend_data = []
|
|
80
|
+
@x = 0.0
|
|
81
|
+
@y = 0.0
|
|
82
|
+
@width = nil
|
|
83
|
+
@stems = 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Parse the CharString
|
|
87
|
+
#
|
|
88
|
+
# @return [self]
|
|
89
|
+
def parse
|
|
90
|
+
return self if @parsed
|
|
91
|
+
|
|
92
|
+
@stack = []
|
|
93
|
+
@io = StringIO.new(@data)
|
|
94
|
+
@io.set_encoding(Encoding::BINARY)
|
|
95
|
+
|
|
96
|
+
parse_charstring_program
|
|
97
|
+
|
|
98
|
+
@parsed = true
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get blended values for a specific set of coordinates
|
|
103
|
+
#
|
|
104
|
+
# @param coordinates [Hash<String, Float>] Axis coordinates
|
|
105
|
+
# @return [Array<Float>] Blended values
|
|
106
|
+
def blend_values(coordinates)
|
|
107
|
+
return [] if @blend_data.empty?
|
|
108
|
+
|
|
109
|
+
# Apply blend operations with coordinates
|
|
110
|
+
@blend_data.map do |blend_op|
|
|
111
|
+
apply_blend(blend_op, coordinates)
|
|
112
|
+
end.flatten
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Convert path to drawing commands
|
|
116
|
+
#
|
|
117
|
+
# @return [Array<Array>] Array of command arrays
|
|
118
|
+
def to_commands
|
|
119
|
+
@path.map do |cmd|
|
|
120
|
+
case cmd[:type]
|
|
121
|
+
when :move_to
|
|
122
|
+
[:move_to, cmd[:x], cmd[:y]]
|
|
123
|
+
when :line_to
|
|
124
|
+
[:line_to, cmd[:x], cmd[:y]]
|
|
125
|
+
when :curve_to
|
|
126
|
+
[:curve_to, cmd[:x1], cmd[:y1], cmd[:x2], cmd[:y2], cmd[:x], cmd[:y]]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# Parse the CharString program
|
|
134
|
+
def parse_charstring_program
|
|
135
|
+
until @io.eof?
|
|
136
|
+
byte = @io.getbyte
|
|
137
|
+
|
|
138
|
+
if operator_byte?(byte)
|
|
139
|
+
operator = read_operator(byte)
|
|
140
|
+
execute_operator(operator)
|
|
141
|
+
else
|
|
142
|
+
# Operand byte
|
|
143
|
+
@io.pos -= 1
|
|
144
|
+
number = read_number
|
|
145
|
+
@stack << number
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
raise CorruptedTableError, "Failed to parse CFF2 CharString: #{e.message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Check if byte is an operator
|
|
153
|
+
#
|
|
154
|
+
# @param byte [Integer] Byte value
|
|
155
|
+
# @return [Boolean] True if operator
|
|
156
|
+
def operator_byte?(byte)
|
|
157
|
+
byte <= 31 && byte != 28
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Read an operator from the CharString
|
|
161
|
+
#
|
|
162
|
+
# @param first_byte [Integer] First operator byte
|
|
163
|
+
# @return [Integer, Array<Integer>] Operator code
|
|
164
|
+
def read_operator(first_byte)
|
|
165
|
+
if first_byte == 12
|
|
166
|
+
# Two-byte operator
|
|
167
|
+
second_byte = @io.getbyte
|
|
168
|
+
raise CorruptedTableError, "Unexpected end of CharString" if second_byte.nil?
|
|
169
|
+
|
|
170
|
+
[12, second_byte]
|
|
171
|
+
else
|
|
172
|
+
# Single-byte operator
|
|
173
|
+
first_byte
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Read a number from the CharString
|
|
178
|
+
#
|
|
179
|
+
# @return [Integer, Float] The number value
|
|
180
|
+
def read_number
|
|
181
|
+
byte = @io.getbyte
|
|
182
|
+
raise CorruptedTableError, "Unexpected end of CharString" if byte.nil?
|
|
183
|
+
|
|
184
|
+
case byte
|
|
185
|
+
when 28
|
|
186
|
+
# 3-byte signed integer (16-bit)
|
|
187
|
+
b1 = @io.getbyte
|
|
188
|
+
b2 = @io.getbyte
|
|
189
|
+
value = (b1 << 8) | b2
|
|
190
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
191
|
+
when 32..246
|
|
192
|
+
# Small integer: -107 to +107
|
|
193
|
+
byte - 139
|
|
194
|
+
when 247..250
|
|
195
|
+
# Positive 2-byte integer: +108 to +1131
|
|
196
|
+
b2 = @io.getbyte
|
|
197
|
+
(byte - 247) * 256 + b2 + 108
|
|
198
|
+
when 251..254
|
|
199
|
+
# Negative 2-byte integer: -108 to -1131
|
|
200
|
+
b2 = @io.getbyte
|
|
201
|
+
-(byte - 251) * 256 - b2 - 108
|
|
202
|
+
when 255
|
|
203
|
+
# 5-byte signed integer (32-bit) as fixed-point 16.16
|
|
204
|
+
bytes = @io.read(4)
|
|
205
|
+
value = bytes.unpack1("l>") # Signed 32-bit big-endian
|
|
206
|
+
value / 65536.0 # Convert to float
|
|
207
|
+
else
|
|
208
|
+
raise CorruptedTableError, "Invalid CharString number byte: #{byte}"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Execute a CharString operator
|
|
213
|
+
#
|
|
214
|
+
# @param operator [Integer, Array<Integer>] Operator code
|
|
215
|
+
def execute_operator(operator)
|
|
216
|
+
case operator
|
|
217
|
+
when BLEND_OPERATOR
|
|
218
|
+
execute_blend
|
|
219
|
+
when 21 # rmoveto
|
|
220
|
+
rmoveto
|
|
221
|
+
when 22 # hmoveto
|
|
222
|
+
hmoveto
|
|
223
|
+
when 4 # vmoveto
|
|
224
|
+
vmoveto
|
|
225
|
+
when 5 # rlineto
|
|
226
|
+
rlineto
|
|
227
|
+
when 6 # hlineto
|
|
228
|
+
hlineto
|
|
229
|
+
when 7 # vlineto
|
|
230
|
+
vlineto
|
|
231
|
+
when 8 # rrcurveto
|
|
232
|
+
rrcurveto
|
|
233
|
+
when 27 # hhcurveto
|
|
234
|
+
hhcurveto
|
|
235
|
+
when 26 # vvcurveto
|
|
236
|
+
vvcurveto
|
|
237
|
+
when 31 # hvcurveto
|
|
238
|
+
hvcurveto
|
|
239
|
+
when 30 # vhcurveto
|
|
240
|
+
vhcurveto
|
|
241
|
+
when 14 # endchar
|
|
242
|
+
endchar
|
|
243
|
+
when 1, 3, 18, 23 # hstem, vstem, hstemhm, vstemhm
|
|
244
|
+
hint_operator
|
|
245
|
+
when 19, 20 # hintmask, cntrmask
|
|
246
|
+
hintmask_operator
|
|
247
|
+
when 10 # callsubr
|
|
248
|
+
callsubr
|
|
249
|
+
when 29 # callgsubr
|
|
250
|
+
callgsubr
|
|
251
|
+
else
|
|
252
|
+
# Unknown operator - clear stack
|
|
253
|
+
@stack.clear
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Execute blend operator
|
|
258
|
+
#
|
|
259
|
+
# Stack: v1 Δv1_1 ... Δv1_N v2 Δv2_1 ... Δv2_N ... K N blend
|
|
260
|
+
# Result: blended_v1 blended_v2 ... blended_vK
|
|
261
|
+
def execute_blend
|
|
262
|
+
return if @stack.size < 2
|
|
263
|
+
|
|
264
|
+
# Pop N (number of axes) and K (number of values to blend)
|
|
265
|
+
n = @stack.pop.to_i
|
|
266
|
+
k = @stack.pop.to_i
|
|
267
|
+
|
|
268
|
+
# Validate we have enough operands: K * (N + 1)
|
|
269
|
+
required_operands = k * (n + 1)
|
|
270
|
+
if @stack.size < required_operands
|
|
271
|
+
warn "Blend operator requires #{required_operands} operands, got #{@stack.size}"
|
|
272
|
+
@stack.clear
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Extract base values and deltas
|
|
277
|
+
blend_operands = @stack.pop(required_operands)
|
|
278
|
+
blends = []
|
|
279
|
+
|
|
280
|
+
k.times do |i|
|
|
281
|
+
offset = i * (n + 1)
|
|
282
|
+
base_value = blend_operands[offset]
|
|
283
|
+
deltas = blend_operands[offset + 1, n] || []
|
|
284
|
+
|
|
285
|
+
blends << {
|
|
286
|
+
base: base_value,
|
|
287
|
+
deltas: deltas,
|
|
288
|
+
num_axes: n,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# For now, push base value back (will be blended later with coordinates)
|
|
292
|
+
@stack << base_value
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Store blend data for later application
|
|
296
|
+
@blend_data << {
|
|
297
|
+
num_values: k,
|
|
298
|
+
num_axes: n,
|
|
299
|
+
blends: blends,
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Apply blend operation with coordinates
|
|
304
|
+
#
|
|
305
|
+
# @param blend_op [Hash] Blend operation data
|
|
306
|
+
# @param coordinates [Hash<String, Float>] Axis coordinates
|
|
307
|
+
# @return [Array<Float>] Blended values
|
|
308
|
+
def apply_blend(blend_op, coordinates)
|
|
309
|
+
blend_op[:blends].map do |blend|
|
|
310
|
+
base = blend[:base]
|
|
311
|
+
deltas = blend[:deltas]
|
|
312
|
+
|
|
313
|
+
# Apply deltas based on coordinates
|
|
314
|
+
# This will be enhanced when we have proper coordinate interpolation
|
|
315
|
+
blended_value = base
|
|
316
|
+
deltas.each_with_index do |delta, axis_index|
|
|
317
|
+
# Placeholder: use normalized coordinate (will be replaced with proper interpolation)
|
|
318
|
+
scalar = 0.0 # Will be calculated by interpolator
|
|
319
|
+
blended_value += delta * scalar
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
blended_value
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Path construction operators (simplified implementations)
|
|
327
|
+
|
|
328
|
+
def rmoveto
|
|
329
|
+
return if @stack.size < 2
|
|
330
|
+
|
|
331
|
+
dy = @stack.pop
|
|
332
|
+
dx = @stack.pop
|
|
333
|
+
@x += dx
|
|
334
|
+
@y += dy
|
|
335
|
+
@path << { type: :move_to, x: @x, y: @y }
|
|
336
|
+
@stack.clear
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def hmoveto
|
|
340
|
+
return if @stack.empty?
|
|
341
|
+
|
|
342
|
+
dx = @stack.pop
|
|
343
|
+
@x += dx
|
|
344
|
+
@path << { type: :move_to, x: @x, y: @y }
|
|
345
|
+
@stack.clear
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def vmoveto
|
|
349
|
+
return if @stack.empty?
|
|
350
|
+
|
|
351
|
+
dy = @stack.pop
|
|
352
|
+
@y += dy
|
|
353
|
+
@path << { type: :move_to, x: @x, y: @y }
|
|
354
|
+
@stack.clear
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def rlineto
|
|
358
|
+
while @stack.size >= 2
|
|
359
|
+
dx = @stack.shift
|
|
360
|
+
dy = @stack.shift
|
|
361
|
+
@x += dx
|
|
362
|
+
@y += dy
|
|
363
|
+
@path << { type: :line_to, x: @x, y: @y }
|
|
364
|
+
end
|
|
365
|
+
@stack.clear
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def hlineto
|
|
369
|
+
horizontal = true
|
|
370
|
+
while @stack.any?
|
|
371
|
+
delta = @stack.shift
|
|
372
|
+
if horizontal
|
|
373
|
+
@x += delta
|
|
374
|
+
else
|
|
375
|
+
@y += delta
|
|
376
|
+
end
|
|
377
|
+
@path << { type: :line_to, x: @x, y: @y }
|
|
378
|
+
horizontal = !horizontal
|
|
379
|
+
end
|
|
380
|
+
@stack.clear
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def vlineto
|
|
384
|
+
vertical = true
|
|
385
|
+
while @stack.any?
|
|
386
|
+
delta = @stack.shift
|
|
387
|
+
if vertical
|
|
388
|
+
@y += delta
|
|
389
|
+
else
|
|
390
|
+
@x += delta
|
|
391
|
+
end
|
|
392
|
+
@path << { type: :line_to, x: @x, y: @y }
|
|
393
|
+
vertical = !vertical
|
|
394
|
+
end
|
|
395
|
+
@stack.clear
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def rrcurveto
|
|
399
|
+
while @stack.size >= 6
|
|
400
|
+
dx1 = @stack.shift
|
|
401
|
+
dy1 = @stack.shift
|
|
402
|
+
dx2 = @stack.shift
|
|
403
|
+
dy2 = @stack.shift
|
|
404
|
+
dx3 = @stack.shift
|
|
405
|
+
dy3 = @stack.shift
|
|
406
|
+
|
|
407
|
+
x1 = @x + dx1
|
|
408
|
+
y1 = @y + dy1
|
|
409
|
+
x2 = x1 + dx2
|
|
410
|
+
y2 = y1 + dy2
|
|
411
|
+
@x = x2 + dx3
|
|
412
|
+
@y = y2 + dy3
|
|
413
|
+
|
|
414
|
+
@path << {
|
|
415
|
+
type: :curve_to,
|
|
416
|
+
x1: x1, y1: y1,
|
|
417
|
+
x2: x2, y2: y2,
|
|
418
|
+
x: @x, y: @y
|
|
419
|
+
}
|
|
420
|
+
end
|
|
421
|
+
@stack.clear
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def hhcurveto
|
|
425
|
+
if @stack.size.odd?
|
|
426
|
+
@y += @stack.shift
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
while @stack.size >= 4
|
|
430
|
+
dx1 = @stack.shift
|
|
431
|
+
dx2 = @stack.shift
|
|
432
|
+
dy2 = @stack.shift
|
|
433
|
+
dx3 = @stack.shift
|
|
434
|
+
|
|
435
|
+
x1 = @x + dx1
|
|
436
|
+
y1 = @y
|
|
437
|
+
x2 = x1 + dx2
|
|
438
|
+
y2 = y1 + dy2
|
|
439
|
+
@x = x2 + dx3
|
|
440
|
+
@y = y2
|
|
441
|
+
|
|
442
|
+
@path << {
|
|
443
|
+
type: :curve_to,
|
|
444
|
+
x1: x1, y1: y1,
|
|
445
|
+
x2: x2, y2: y2,
|
|
446
|
+
x: @x, y: @y
|
|
447
|
+
}
|
|
448
|
+
end
|
|
449
|
+
@stack.clear
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def vvcurveto
|
|
453
|
+
if @stack.size.odd?
|
|
454
|
+
@x += @stack.shift
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
while @stack.size >= 4
|
|
458
|
+
dy1 = @stack.shift
|
|
459
|
+
dx2 = @stack.shift
|
|
460
|
+
dy2 = @stack.shift
|
|
461
|
+
dy3 = @stack.shift
|
|
462
|
+
|
|
463
|
+
x1 = @x
|
|
464
|
+
y1 = @y + dy1
|
|
465
|
+
x2 = x1 + dx2
|
|
466
|
+
y2 = y1 + dy2
|
|
467
|
+
@x = x2
|
|
468
|
+
@y = y2 + dy3
|
|
469
|
+
|
|
470
|
+
@path << {
|
|
471
|
+
type: :curve_to,
|
|
472
|
+
x1: x1, y1: y1,
|
|
473
|
+
x2: x2, y2: y2,
|
|
474
|
+
x: @x, y: @y
|
|
475
|
+
}
|
|
476
|
+
end
|
|
477
|
+
@stack.clear
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def hvcurveto
|
|
481
|
+
horizontal_first = true
|
|
482
|
+
while @stack.size >= 4
|
|
483
|
+
if horizontal_first
|
|
484
|
+
dx1 = @stack.shift
|
|
485
|
+
dx2 = @stack.shift
|
|
486
|
+
dy2 = @stack.shift
|
|
487
|
+
dy3 = @stack.shift
|
|
488
|
+
dx3 = @stack.size == 1 ? @stack.shift : 0
|
|
489
|
+
|
|
490
|
+
x1 = @x + dx1
|
|
491
|
+
y1 = @y
|
|
492
|
+
else
|
|
493
|
+
dy1 = @stack.shift
|
|
494
|
+
dx2 = @stack.shift
|
|
495
|
+
dy2 = @stack.shift
|
|
496
|
+
dx3 = @stack.shift
|
|
497
|
+
dy3 = @stack.size == 1 ? @stack.shift : 0
|
|
498
|
+
|
|
499
|
+
x1 = @x
|
|
500
|
+
y1 = @y + dy1
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
x2 = x1 + dx2
|
|
504
|
+
y2 = y1 + dy2
|
|
505
|
+
@x = x2 + dx3
|
|
506
|
+
@y = y2 + dy3
|
|
507
|
+
|
|
508
|
+
@path << {
|
|
509
|
+
type: :curve_to,
|
|
510
|
+
x1: x1, y1: y1,
|
|
511
|
+
x2: x2, y2: y2,
|
|
512
|
+
x: @x, y: @y
|
|
513
|
+
}
|
|
514
|
+
horizontal_first = !horizontal_first
|
|
515
|
+
end
|
|
516
|
+
@stack.clear
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def vhcurveto
|
|
520
|
+
vertical_first = true
|
|
521
|
+
while @stack.size >= 4
|
|
522
|
+
if vertical_first
|
|
523
|
+
dy1 = @stack.shift
|
|
524
|
+
dx2 = @stack.shift
|
|
525
|
+
dy2 = @stack.shift
|
|
526
|
+
dx3 = @stack.shift
|
|
527
|
+
dy3 = @stack.size == 1 ? @stack.shift : 0
|
|
528
|
+
|
|
529
|
+
x1 = @x
|
|
530
|
+
y1 = @y + dy1
|
|
531
|
+
else
|
|
532
|
+
dx1 = @stack.shift
|
|
533
|
+
dx2 = @stack.shift
|
|
534
|
+
dy2 = @stack.shift
|
|
535
|
+
dy3 = @stack.shift
|
|
536
|
+
dx3 = @stack.size == 1 ? @stack.shift : 0
|
|
537
|
+
|
|
538
|
+
x1 = @x + dx1
|
|
539
|
+
y1 = @y
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
x2 = x1 + dx2
|
|
543
|
+
y2 = y1 + dy2
|
|
544
|
+
@x = x2 + dx3
|
|
545
|
+
@y = y2 + dy3
|
|
546
|
+
|
|
547
|
+
@path << {
|
|
548
|
+
type: :curve_to,
|
|
549
|
+
x1: x1, y1: y1,
|
|
550
|
+
x2: x2, y2: y2,
|
|
551
|
+
x: @x, y: @y
|
|
552
|
+
}
|
|
553
|
+
vertical_first = !vertical_first
|
|
554
|
+
end
|
|
555
|
+
@stack.clear
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def endchar
|
|
559
|
+
@stack.clear
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def hint_operator
|
|
563
|
+
@stems += @stack.size / 2
|
|
564
|
+
@stack.clear
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def hintmask_operator
|
|
568
|
+
hint_bytes = (@stems + 7) / 8
|
|
569
|
+
@io.read(hint_bytes)
|
|
570
|
+
@stack.clear
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def callsubr
|
|
574
|
+
return if @local_subrs.nil? || @stack.empty?
|
|
575
|
+
|
|
576
|
+
subr_index = @stack.pop
|
|
577
|
+
# Implement subroutine call (placeholder)
|
|
578
|
+
@stack.clear
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def callgsubr
|
|
582
|
+
return if @global_subrs.nil? || @stack.empty?
|
|
583
|
+
|
|
584
|
+
subr_index = @stack.pop
|
|
585
|
+
# Implement global subroutine call (placeholder)
|
|
586
|
+
@stack.clear
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|