fontisan 0.1.0 → 0.2.1
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 +672 -69
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1477 -297
- data/Rakefile +63 -41
- 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 +364 -4
- data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +24 -1
- data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +408 -0
- data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
- data/lib/fontisan/font_writer.rb +302 -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 +310 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
- data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- 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/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +154 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -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 +934 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -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 +257 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -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/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +489 -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/private_dict_blend_handler.rb +246 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +346 -0
- data/lib/fontisan/tables/cvar.rb +203 -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 +231 -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 +321 -20
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +60 -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/validation/variable_font_validator.rb +218 -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 +375 -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/instance_writer.rb +341 -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/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/variation/variation_preserver.rb +288 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +717 -0
- data/lib/fontisan/woff_font.rb +488 -0
- data/lib/fontisan.rb +132 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +234 -4
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# Type 2 CharString builder/encoder
|
|
9
|
+
#
|
|
10
|
+
# [`CharStringBuilder`](lib/fontisan/tables/cff/charstring_builder.rb)
|
|
11
|
+
# encodes glyph outlines into Type 2 CharString binary format. It takes
|
|
12
|
+
# high-level outline commands and produces the stack-based CharString
|
|
13
|
+
# operators used in CFF fonts.
|
|
14
|
+
#
|
|
15
|
+
# Type 2 CharString encoding:
|
|
16
|
+
# - Numbers are encoded in various compact formats
|
|
17
|
+
# - Operators are single or two-byte commands
|
|
18
|
+
# - All coordinates are relative (dx, dy format)
|
|
19
|
+
# - Current point tracking for relative calculations
|
|
20
|
+
#
|
|
21
|
+
# Operator optimization:
|
|
22
|
+
# - Use specialized operators (hlineto, vlineto) when possible
|
|
23
|
+
# - Merge sequential operators of same type
|
|
24
|
+
# - Minimize operator bytes
|
|
25
|
+
#
|
|
26
|
+
# Reference: Adobe Type 2 CharString Format
|
|
27
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf
|
|
28
|
+
#
|
|
29
|
+
# @example Building a CharString from outline
|
|
30
|
+
# builder = Fontisan::Tables::Cff::CharStringBuilder.new
|
|
31
|
+
# charstring_data = builder.build(outline, width: 500)
|
|
32
|
+
class CharStringBuilder
|
|
33
|
+
# Type 2 CharString operators (opposite of parser)
|
|
34
|
+
OPERATORS = {
|
|
35
|
+
hstem: 1,
|
|
36
|
+
vstem: 3,
|
|
37
|
+
vmoveto: 4,
|
|
38
|
+
rlineto: 5,
|
|
39
|
+
hlineto: 6,
|
|
40
|
+
vlineto: 7,
|
|
41
|
+
rrcurveto: 8,
|
|
42
|
+
callsubr: 10,
|
|
43
|
+
return: 11,
|
|
44
|
+
endchar: 14,
|
|
45
|
+
hstemhm: 18,
|
|
46
|
+
hintmask: 19,
|
|
47
|
+
cntrmask: 20,
|
|
48
|
+
rmoveto: 21,
|
|
49
|
+
hmoveto: 22,
|
|
50
|
+
vstemhm: 23,
|
|
51
|
+
rcurveline: 24,
|
|
52
|
+
rlinecurve: 25,
|
|
53
|
+
vvcurveto: 26,
|
|
54
|
+
hhcurveto: 27,
|
|
55
|
+
shortint: 28,
|
|
56
|
+
callgsubr: 29,
|
|
57
|
+
vhcurveto: 30,
|
|
58
|
+
hvcurveto: 31,
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Two-byte operators (12 prefix)
|
|
62
|
+
TWO_BYTE_OPERATORS = {
|
|
63
|
+
and: [12, 3],
|
|
64
|
+
or: [12, 4],
|
|
65
|
+
not: [12, 5],
|
|
66
|
+
abs: [12, 9],
|
|
67
|
+
add: [12, 10],
|
|
68
|
+
sub: [12, 11],
|
|
69
|
+
div: [12, 12],
|
|
70
|
+
neg: [12, 14],
|
|
71
|
+
eq: [12, 15],
|
|
72
|
+
drop: [12, 18],
|
|
73
|
+
put: [12, 20],
|
|
74
|
+
get: [12, 21],
|
|
75
|
+
ifelse: [12, 22],
|
|
76
|
+
random: [12, 23],
|
|
77
|
+
mul: [12, 24],
|
|
78
|
+
sqrt: [12, 26],
|
|
79
|
+
dup: [12, 27],
|
|
80
|
+
exch: [12, 28],
|
|
81
|
+
index: [12, 29],
|
|
82
|
+
roll: [12, 30],
|
|
83
|
+
hflex: [12, 34],
|
|
84
|
+
flex: [12, 35],
|
|
85
|
+
hflex1: [12, 36],
|
|
86
|
+
flex1: [12, 37],
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
# Build a CharString from an outline
|
|
90
|
+
#
|
|
91
|
+
# @param outline [Models::Outline] Universal outline object
|
|
92
|
+
# @param width [Integer, nil] Glyph width (optional)
|
|
93
|
+
# @return [String] Binary CharString data
|
|
94
|
+
def build(outline, width: nil)
|
|
95
|
+
@output = StringIO.new("".b)
|
|
96
|
+
@current_x = 0.0
|
|
97
|
+
@current_y = 0.0
|
|
98
|
+
@first_move = true
|
|
99
|
+
|
|
100
|
+
# Convert outline to CFF commands
|
|
101
|
+
commands = outline.to_cff_commands
|
|
102
|
+
|
|
103
|
+
# Encode width if provided (before first move)
|
|
104
|
+
if width && !commands.empty?
|
|
105
|
+
# Width is encoded as first operator before first move
|
|
106
|
+
# For now, we'll add it before the first moveto
|
|
107
|
+
encode_width(width)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Encode each command
|
|
111
|
+
commands.each do |cmd|
|
|
112
|
+
encode_command(cmd)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# End character
|
|
116
|
+
write_operator(:endchar)
|
|
117
|
+
|
|
118
|
+
@output.string
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Build a CharString from operation list
|
|
122
|
+
#
|
|
123
|
+
# This method takes operations from CharStringParser and encodes them
|
|
124
|
+
# back to binary CharString format. Useful for CharString modification.
|
|
125
|
+
#
|
|
126
|
+
# @param operations [Array<Hash>] Array of operation hashes from parser
|
|
127
|
+
# @return [String] Binary CharString data
|
|
128
|
+
def self.build_from_operations(operations)
|
|
129
|
+
new.build_from_operations(operations)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Instance method for building from operations
|
|
133
|
+
#
|
|
134
|
+
# @param operations [Array<Hash>] Array of operation hashes
|
|
135
|
+
# @return [String] Binary CharString data
|
|
136
|
+
def build_from_operations(operations)
|
|
137
|
+
@output = StringIO.new("".b)
|
|
138
|
+
|
|
139
|
+
operations.each do |op|
|
|
140
|
+
# Write operands
|
|
141
|
+
op[:operands].each { |operand| write_number(operand) }
|
|
142
|
+
|
|
143
|
+
# Write operator
|
|
144
|
+
write_operator(op[:name])
|
|
145
|
+
|
|
146
|
+
# Write hint data if present (for hintmask/cntrmask)
|
|
147
|
+
if op[:hint_data]
|
|
148
|
+
@output.write(op[:hint_data])
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
@output.string
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Build an empty CharString (for .notdef or empty glyphs)
|
|
156
|
+
#
|
|
157
|
+
# @param width [Integer, nil] Glyph width
|
|
158
|
+
# @return [String] Binary CharString data
|
|
159
|
+
def build_empty(width: nil)
|
|
160
|
+
@output = StringIO.new("".b)
|
|
161
|
+
|
|
162
|
+
# Encode width if provided
|
|
163
|
+
encode_width(width) if width
|
|
164
|
+
|
|
165
|
+
# Just endchar for empty glyph
|
|
166
|
+
write_operator(:endchar)
|
|
167
|
+
|
|
168
|
+
@output.string
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
# Encode a width value
|
|
174
|
+
#
|
|
175
|
+
# Width is encoded as a delta from nominal width
|
|
176
|
+
# For simplicity, we encode as-is (assuming nominal width is 0)
|
|
177
|
+
#
|
|
178
|
+
# @param width [Integer] Width value
|
|
179
|
+
def encode_width(width)
|
|
180
|
+
write_number(width)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Encode a single command
|
|
184
|
+
#
|
|
185
|
+
# @param cmd [Hash] Command hash with :type and coordinates
|
|
186
|
+
def encode_command(cmd)
|
|
187
|
+
case cmd[:type]
|
|
188
|
+
when :move_to
|
|
189
|
+
encode_moveto(cmd)
|
|
190
|
+
when :line_to
|
|
191
|
+
encode_lineto(cmd)
|
|
192
|
+
when :curve_to
|
|
193
|
+
encode_curveto(cmd)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Encode a moveto command
|
|
198
|
+
#
|
|
199
|
+
# Uses rmoveto (relative move) with dx, dy
|
|
200
|
+
# For first move, can optimize to hmoveto/vmoveto if one delta is 0
|
|
201
|
+
#
|
|
202
|
+
# @param cmd [Hash] Command with :x, :y
|
|
203
|
+
def encode_moveto(cmd)
|
|
204
|
+
dx = cmd[:x] - @current_x
|
|
205
|
+
dy = cmd[:y] - @current_y
|
|
206
|
+
|
|
207
|
+
if @first_move
|
|
208
|
+
# First move - can optimize
|
|
209
|
+
if dx.zero?
|
|
210
|
+
write_number(dy.round)
|
|
211
|
+
write_operator(:vmoveto)
|
|
212
|
+
elsif dy.zero?
|
|
213
|
+
write_number(dx.round)
|
|
214
|
+
write_operator(:hmoveto)
|
|
215
|
+
else
|
|
216
|
+
write_number(dx.round)
|
|
217
|
+
write_number(dy.round)
|
|
218
|
+
write_operator(:rmoveto)
|
|
219
|
+
end
|
|
220
|
+
@first_move = false
|
|
221
|
+
else
|
|
222
|
+
# Subsequent moves
|
|
223
|
+
write_number(dx.round)
|
|
224
|
+
write_number(dy.round)
|
|
225
|
+
write_operator(:rmoveto)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@current_x = cmd[:x]
|
|
229
|
+
@current_y = cmd[:y]
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Encode a lineto command
|
|
233
|
+
#
|
|
234
|
+
# Uses rlineto with dx, dy
|
|
235
|
+
# Could optimize with hlineto/vlineto for horizontal/vertical lines
|
|
236
|
+
#
|
|
237
|
+
# @param cmd [Hash] Command with :x, :y
|
|
238
|
+
def encode_lineto(cmd)
|
|
239
|
+
dx = cmd[:x] - @current_x
|
|
240
|
+
dy = cmd[:y] - @current_y
|
|
241
|
+
|
|
242
|
+
# Simple encoding - could optimize for h/v lines
|
|
243
|
+
write_number(dx.round)
|
|
244
|
+
write_number(dy.round)
|
|
245
|
+
write_operator(:rlineto)
|
|
246
|
+
|
|
247
|
+
@current_x = cmd[:x]
|
|
248
|
+
@current_y = cmd[:y]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Encode a curveto command (cubic Bézier)
|
|
252
|
+
#
|
|
253
|
+
# Uses rrcurveto with 6 relative coordinates:
|
|
254
|
+
# dx1 dy1 dx2 dy2 dx3 dy3
|
|
255
|
+
#
|
|
256
|
+
# @param cmd [Hash] Command with :x1, :y1, :x2, :y2, :x, :y
|
|
257
|
+
def encode_curveto(cmd)
|
|
258
|
+
# Calculate relative coordinates for each control point
|
|
259
|
+
dx1 = cmd[:x1] - @current_x
|
|
260
|
+
dy1 = cmd[:y1] - @current_y
|
|
261
|
+
|
|
262
|
+
dx2 = cmd[:x2] - cmd[:x1]
|
|
263
|
+
dy2 = cmd[:y2] - cmd[:y1]
|
|
264
|
+
|
|
265
|
+
dx3 = cmd[:x] - cmd[:x2]
|
|
266
|
+
dy3 = cmd[:y] - cmd[:y2]
|
|
267
|
+
|
|
268
|
+
# Write operands
|
|
269
|
+
write_number(dx1.round)
|
|
270
|
+
write_number(dy1.round)
|
|
271
|
+
write_number(dx2.round)
|
|
272
|
+
write_number(dy2.round)
|
|
273
|
+
write_number(dx3.round)
|
|
274
|
+
write_number(dy3.round)
|
|
275
|
+
|
|
276
|
+
# Write operator
|
|
277
|
+
write_operator(:rrcurveto)
|
|
278
|
+
|
|
279
|
+
@current_x = cmd[:x]
|
|
280
|
+
@current_y = cmd[:y]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Write a number to the CharString
|
|
284
|
+
#
|
|
285
|
+
# Numbers are encoded in various formats based on their range:
|
|
286
|
+
# - -107 to +107: Single byte (32-246)
|
|
287
|
+
# - -1131 to +1131: Two bytes (247-254 + byte)
|
|
288
|
+
# - -32768 to +32767: Three bytes (28 + 2 bytes)
|
|
289
|
+
# - Otherwise: Five bytes (255 + 4 bytes as 16.16 fixed)
|
|
290
|
+
#
|
|
291
|
+
# @param value [Integer, Float] Number to encode
|
|
292
|
+
def write_number(value)
|
|
293
|
+
# Convert float to integer if it's effectively an integer
|
|
294
|
+
value = value.round if value.is_a?(Float) && value == value.round
|
|
295
|
+
|
|
296
|
+
if value.is_a?(Float)
|
|
297
|
+
# Real number - use 5-byte format (16.16 fixed point)
|
|
298
|
+
write_real(value)
|
|
299
|
+
elsif value >= -107 && value <= 107
|
|
300
|
+
# Single byte format: 32-246 represents -107 to +107
|
|
301
|
+
@output.putc(value + 139)
|
|
302
|
+
elsif value >= 108 && value <= 1131
|
|
303
|
+
# Positive two-byte format: 247-250
|
|
304
|
+
adjusted = value - 108
|
|
305
|
+
b0 = 247 + (adjusted / 256)
|
|
306
|
+
b1 = adjusted % 256
|
|
307
|
+
@output.putc(b0)
|
|
308
|
+
@output.putc(b1)
|
|
309
|
+
elsif value >= -1131 && value <= -108
|
|
310
|
+
# Negative two-byte format: 251-254
|
|
311
|
+
adjusted = -value - 108
|
|
312
|
+
b0 = 251 + (adjusted / 256)
|
|
313
|
+
b1 = adjusted % 256
|
|
314
|
+
@output.putc(b0)
|
|
315
|
+
@output.putc(b1)
|
|
316
|
+
elsif value >= -32768 && value <= 32767
|
|
317
|
+
# Three-byte signed integer
|
|
318
|
+
@output.putc(28)
|
|
319
|
+
@output.write([value].pack("s>")) # Signed 16-bit big-endian
|
|
320
|
+
else
|
|
321
|
+
# Five-byte signed integer (stored as 16.16 fixed point)
|
|
322
|
+
write_real(value.to_f)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Write a real number (5-byte format)
|
|
327
|
+
#
|
|
328
|
+
# @param value [Float] Real number
|
|
329
|
+
def write_real(value)
|
|
330
|
+
# Convert to 16.16 fixed point
|
|
331
|
+
fixed = (value * 65536.0).round
|
|
332
|
+
|
|
333
|
+
@output.putc(255)
|
|
334
|
+
@output.write([fixed].pack("l>")) # Signed 32-bit big-endian
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Write an operator to the CharString
|
|
338
|
+
#
|
|
339
|
+
# @param operator [Symbol] Operator name
|
|
340
|
+
def write_operator(operator)
|
|
341
|
+
if OPERATORS.key?(operator)
|
|
342
|
+
# Single-byte operator
|
|
343
|
+
@output.putc(OPERATORS[operator])
|
|
344
|
+
elsif TWO_BYTE_OPERATORS.key?(operator)
|
|
345
|
+
# Two-byte operator
|
|
346
|
+
bytes = TWO_BYTE_OPERATORS[operator]
|
|
347
|
+
@output.putc(bytes[0])
|
|
348
|
+
@output.putc(bytes[1])
|
|
349
|
+
else
|
|
350
|
+
raise ArgumentError, "Unknown operator: #{operator}"
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CharString parser that converts binary CharString data to operation list
|
|
9
|
+
#
|
|
10
|
+
# Unlike [`CharString`](lib/fontisan/tables/cff/charstring.rb) which
|
|
11
|
+
# interprets and executes CharStrings for rendering, CharStringParser
|
|
12
|
+
# parses CharStrings into a list of operations that can be modified and
|
|
13
|
+
# rebuilt. This enables CharString manipulation for hint injection,
|
|
14
|
+
# subroutine optimization, and other transformations.
|
|
15
|
+
#
|
|
16
|
+
# Operation Format:
|
|
17
|
+
# ```ruby
|
|
18
|
+
# {
|
|
19
|
+
# type: :operator,
|
|
20
|
+
# name: :rmoveto,
|
|
21
|
+
# operands: [100, 200]
|
|
22
|
+
# }
|
|
23
|
+
# ```
|
|
24
|
+
#
|
|
25
|
+
# Reference: Adobe Type 2 CharString Format
|
|
26
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf
|
|
27
|
+
#
|
|
28
|
+
# @example Parse a CharString
|
|
29
|
+
# parser = CharStringParser.new(charstring_data)
|
|
30
|
+
# operations = parser.parse
|
|
31
|
+
# operations.each { |op| puts "#{op[:name]} #{op[:operands]}" }
|
|
32
|
+
class CharStringParser
|
|
33
|
+
# Type 2 CharString operators (from CharString class)
|
|
34
|
+
OPERATORS = {
|
|
35
|
+
# Path construction operators
|
|
36
|
+
1 => :hstem,
|
|
37
|
+
3 => :vstem,
|
|
38
|
+
4 => :vmoveto,
|
|
39
|
+
5 => :rlineto,
|
|
40
|
+
6 => :hlineto,
|
|
41
|
+
7 => :vlineto,
|
|
42
|
+
8 => :rrcurveto,
|
|
43
|
+
10 => :callsubr,
|
|
44
|
+
11 => :return,
|
|
45
|
+
14 => :endchar,
|
|
46
|
+
18 => :hstemhm,
|
|
47
|
+
19 => :hintmask,
|
|
48
|
+
20 => :cntrmask,
|
|
49
|
+
21 => :rmoveto,
|
|
50
|
+
22 => :hmoveto,
|
|
51
|
+
23 => :vstemhm,
|
|
52
|
+
24 => :rcurveline,
|
|
53
|
+
25 => :rlinecurve,
|
|
54
|
+
26 => :vvcurveto,
|
|
55
|
+
27 => :hhcurveto,
|
|
56
|
+
28 => :shortint,
|
|
57
|
+
29 => :callgsubr,
|
|
58
|
+
30 => :vhcurveto,
|
|
59
|
+
31 => :hvcurveto,
|
|
60
|
+
# 12 prefix for two-byte operators
|
|
61
|
+
[12, 3] => :and,
|
|
62
|
+
[12, 4] => :or,
|
|
63
|
+
[12, 5] => :not,
|
|
64
|
+
[12, 9] => :abs,
|
|
65
|
+
[12, 10] => :add,
|
|
66
|
+
[12, 11] => :sub,
|
|
67
|
+
[12, 12] => :div,
|
|
68
|
+
[12, 14] => :neg,
|
|
69
|
+
[12, 15] => :eq,
|
|
70
|
+
[12, 18] => :drop,
|
|
71
|
+
[12, 20] => :put,
|
|
72
|
+
[12, 21] => :get,
|
|
73
|
+
[12, 22] => :ifelse,
|
|
74
|
+
[12, 23] => :random,
|
|
75
|
+
[12, 24] => :mul,
|
|
76
|
+
[12, 26] => :sqrt,
|
|
77
|
+
[12, 27] => :dup,
|
|
78
|
+
[12, 28] => :exch,
|
|
79
|
+
[12, 29] => :index,
|
|
80
|
+
[12, 30] => :roll,
|
|
81
|
+
[12, 34] => :hflex,
|
|
82
|
+
[12, 35] => :flex,
|
|
83
|
+
[12, 36] => :hflex1,
|
|
84
|
+
[12, 37] => :flex1,
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
# Operators that require hint mask bytes
|
|
88
|
+
HINTMASK_OPERATORS = %i[hintmask cntrmask].freeze
|
|
89
|
+
|
|
90
|
+
# @return [String] Binary CharString data
|
|
91
|
+
attr_reader :data
|
|
92
|
+
|
|
93
|
+
# @return [Array<Hash>] Parsed operations
|
|
94
|
+
attr_reader :operations
|
|
95
|
+
|
|
96
|
+
# Initialize parser with CharString data
|
|
97
|
+
#
|
|
98
|
+
# @param data [String] Binary CharString data
|
|
99
|
+
# @param stem_count [Integer] Number of stem hints (for hintmask)
|
|
100
|
+
def initialize(data, stem_count: 0)
|
|
101
|
+
@data = data
|
|
102
|
+
@stem_count = stem_count
|
|
103
|
+
@operations = []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Parse CharString to operation list
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<Hash>] Array of operation hashes
|
|
109
|
+
def parse
|
|
110
|
+
@operations = []
|
|
111
|
+
io = StringIO.new(@data)
|
|
112
|
+
operand_stack = []
|
|
113
|
+
|
|
114
|
+
until io.eof?
|
|
115
|
+
byte = io.getbyte
|
|
116
|
+
|
|
117
|
+
if operator_byte?(byte)
|
|
118
|
+
# Operator byte - read operator and create operation
|
|
119
|
+
operator = read_operator(io, byte)
|
|
120
|
+
|
|
121
|
+
# Read hint mask data if needed
|
|
122
|
+
hint_data = nil
|
|
123
|
+
if HINTMASK_OPERATORS.include?(operator)
|
|
124
|
+
hint_bytes = (@stem_count + 7) / 8
|
|
125
|
+
hint_data = io.read(hint_bytes) if hint_bytes.positive?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Create operation
|
|
129
|
+
@operations << {
|
|
130
|
+
type: :operator,
|
|
131
|
+
name: operator,
|
|
132
|
+
operands: operand_stack.dup,
|
|
133
|
+
hint_data: hint_data
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Clear operand stack
|
|
137
|
+
operand_stack.clear
|
|
138
|
+
else
|
|
139
|
+
# Operand byte - read number and push to stack
|
|
140
|
+
io.pos -= 1
|
|
141
|
+
number = read_number(io)
|
|
142
|
+
operand_stack << number
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
@operations
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
raise CorruptedTableError,
|
|
149
|
+
"Failed to parse CharString: #{e.message}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Update stem count (needed for hintmask operations)
|
|
153
|
+
#
|
|
154
|
+
# @param count [Integer] Number of stem hints
|
|
155
|
+
def stem_count=(count)
|
|
156
|
+
@stem_count = count
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# Check if byte is an operator
|
|
162
|
+
#
|
|
163
|
+
# @param byte [Integer] Byte value
|
|
164
|
+
# @return [Boolean] True if operator byte
|
|
165
|
+
def operator_byte?(byte)
|
|
166
|
+
(byte <= 31 && byte != 28) # Operators are 0-31 except 28 (shortint)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Read operator from CharString
|
|
170
|
+
#
|
|
171
|
+
# @param io [StringIO] Input stream
|
|
172
|
+
# @param first_byte [Integer] First operator byte
|
|
173
|
+
# @return [Symbol] Operator name
|
|
174
|
+
def read_operator(io, first_byte)
|
|
175
|
+
if first_byte == 12
|
|
176
|
+
# Two-byte operator
|
|
177
|
+
second_byte = io.getbyte
|
|
178
|
+
raise CorruptedTableError, "Unexpected end of CharString" if
|
|
179
|
+
second_byte.nil?
|
|
180
|
+
|
|
181
|
+
operator_key = [first_byte, second_byte]
|
|
182
|
+
OPERATORS[operator_key] || :unknown
|
|
183
|
+
else
|
|
184
|
+
# Single-byte operator
|
|
185
|
+
OPERATORS[first_byte] || :unknown
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Read a number (integer or real) from CharString
|
|
190
|
+
#
|
|
191
|
+
# @param io [StringIO] Input stream
|
|
192
|
+
# @return [Integer, Float] The number value
|
|
193
|
+
def read_number(io)
|
|
194
|
+
byte = io.getbyte
|
|
195
|
+
raise CorruptedTableError, "Unexpected end of CharString" if
|
|
196
|
+
byte.nil?
|
|
197
|
+
|
|
198
|
+
case byte
|
|
199
|
+
when 28
|
|
200
|
+
# 3-byte signed integer (16-bit)
|
|
201
|
+
b1 = io.getbyte
|
|
202
|
+
b2 = io.getbyte
|
|
203
|
+
raise CorruptedTableError, "Unexpected end of CharString reading shortint" if
|
|
204
|
+
b1.nil? || b2.nil?
|
|
205
|
+
value = (b1 << 8) | b2
|
|
206
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
207
|
+
when 32..246
|
|
208
|
+
# Small integer: -107 to +107
|
|
209
|
+
byte - 139
|
|
210
|
+
when 247..250
|
|
211
|
+
# Positive 2-byte integer: +108 to +1131
|
|
212
|
+
b2 = io.getbyte
|
|
213
|
+
raise CorruptedTableError, "Unexpected end of CharString reading positive integer" if
|
|
214
|
+
b2.nil?
|
|
215
|
+
(byte - 247) * 256 + b2 + 108
|
|
216
|
+
when 251..254
|
|
217
|
+
# Negative 2-byte integer: -108 to -1131
|
|
218
|
+
b2 = io.getbyte
|
|
219
|
+
raise CorruptedTableError, "Unexpected end of CharString reading negative integer" if
|
|
220
|
+
b2.nil?
|
|
221
|
+
-(byte - 251) * 256 - b2 - 108
|
|
222
|
+
when 255
|
|
223
|
+
# 5-byte signed integer (32-bit) as fixed-point 16.16
|
|
224
|
+
bytes = io.read(4)
|
|
225
|
+
raise CorruptedTableError, "Unexpected end of CharString reading fixed-point" if
|
|
226
|
+
bytes.nil? || bytes.length < 4
|
|
227
|
+
value = bytes.unpack1("l>") # Signed 32-bit big-endian
|
|
228
|
+
value / 65536.0 # Convert to float
|
|
229
|
+
else
|
|
230
|
+
raise CorruptedTableError,
|
|
231
|
+
"Invalid CharString number byte: #{byte}"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|