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,351 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../../binary/base_record"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
class Cff
|
|
9
|
+
# CFF DICT (Dictionary) structure parser
|
|
10
|
+
#
|
|
11
|
+
# DICTs in CFF use a compact operand-operator format similar to PostScript.
|
|
12
|
+
# Operands are pushed onto a stack, then an operator consumes them.
|
|
13
|
+
#
|
|
14
|
+
# Operand Encoding:
|
|
15
|
+
# - 32-247: Small integers (values -107 to +107)
|
|
16
|
+
# - 28: 3-byte signed integer follows
|
|
17
|
+
# - 29: 5-byte signed integer follows
|
|
18
|
+
# - 30: Real number (nibble-encoded)
|
|
19
|
+
# - 247-254: 2-byte signed integers
|
|
20
|
+
# - 255: Reserved
|
|
21
|
+
# - 0-21, 22-27: Operators (single or two-byte)
|
|
22
|
+
#
|
|
23
|
+
# Reference: CFF specification section 4 "DICT Data"
|
|
24
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
25
|
+
#
|
|
26
|
+
# @example Parsing a DICT
|
|
27
|
+
# data = top_dict_index[0]
|
|
28
|
+
# dict = Fontisan::Tables::Cff::Dict.new(data)
|
|
29
|
+
# puts dict[:charset] # => offset to charset
|
|
30
|
+
# puts dict[:version] # => version SID
|
|
31
|
+
class Dict
|
|
32
|
+
# Common DICT operators shared across Top DICT and Private DICT
|
|
33
|
+
#
|
|
34
|
+
# Key: operator byte(s), Value: operator name symbol
|
|
35
|
+
OPERATORS = {
|
|
36
|
+
0 => :version,
|
|
37
|
+
1 => :notice,
|
|
38
|
+
2 => :full_name,
|
|
39
|
+
3 => :family_name,
|
|
40
|
+
4 => :weight,
|
|
41
|
+
[12, 0] => :copyright,
|
|
42
|
+
[12, 1] => :is_fixed_pitch,
|
|
43
|
+
[12, 2] => :italic_angle,
|
|
44
|
+
[12, 3] => :underline_position,
|
|
45
|
+
[12, 4] => :underline_thickness,
|
|
46
|
+
[12, 5] => :paint_type,
|
|
47
|
+
[12, 6] => :charstring_type,
|
|
48
|
+
[12, 7] => :font_matrix,
|
|
49
|
+
[12, 8] => :stroke_width,
|
|
50
|
+
[12, 20] => :synthetic_base,
|
|
51
|
+
[12, 21] => :postscript,
|
|
52
|
+
[12, 22] => :base_font_name,
|
|
53
|
+
[12, 23] => :base_font_blend,
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
# @return [Hash] Parsed dictionary as key-value pairs
|
|
57
|
+
attr_reader :dict
|
|
58
|
+
|
|
59
|
+
# @return [String] Raw binary data of the DICT
|
|
60
|
+
attr_reader :data
|
|
61
|
+
|
|
62
|
+
# Initialize and parse a DICT from binary data
|
|
63
|
+
#
|
|
64
|
+
# @param data [String, IO, StringIO] Binary DICT data
|
|
65
|
+
def initialize(data)
|
|
66
|
+
@data = data.is_a?(String) ? data : data.read
|
|
67
|
+
@dict = {}
|
|
68
|
+
@io = StringIO.new(@data)
|
|
69
|
+
parse!
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get a value from the dictionary by operator name
|
|
73
|
+
#
|
|
74
|
+
# @param key [Symbol] Operator name (e.g., :charset, :encoding)
|
|
75
|
+
# @return [Object, nil] Value for the operator, or nil if not present
|
|
76
|
+
def [](key)
|
|
77
|
+
@dict[key]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Set a value in the dictionary
|
|
81
|
+
#
|
|
82
|
+
# @param key [Symbol] Operator name
|
|
83
|
+
# @param value [Object] Value to set
|
|
84
|
+
def []=(key, value)
|
|
85
|
+
@dict[key] = value
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if the dictionary contains a specific operator
|
|
89
|
+
#
|
|
90
|
+
# @param key [Symbol] Operator name
|
|
91
|
+
# @return [Boolean] True if operator is present
|
|
92
|
+
def has_key?(key)
|
|
93
|
+
@dict.key?(key)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get all operator names in this DICT
|
|
97
|
+
#
|
|
98
|
+
# @return [Array<Symbol>] Array of operator names
|
|
99
|
+
def keys
|
|
100
|
+
@dict.keys
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get all values in this DICT
|
|
104
|
+
#
|
|
105
|
+
# @return [Array<Object>] Array of values
|
|
106
|
+
def values
|
|
107
|
+
@dict.values
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Convert DICT to Hash
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash] Dictionary as hash
|
|
113
|
+
def to_h
|
|
114
|
+
@dict.dup
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Number of entries in the DICT
|
|
118
|
+
#
|
|
119
|
+
# @return [Integer] Entry count
|
|
120
|
+
def size
|
|
121
|
+
@dict.size
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if DICT is empty
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean] True if no entries
|
|
127
|
+
def empty?
|
|
128
|
+
@dict.empty?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
# Parse the DICT structure
|
|
134
|
+
#
|
|
135
|
+
# DICTs use a stack-based format:
|
|
136
|
+
# 1. Read operands and push onto operand stack
|
|
137
|
+
# 2. When operator is encountered, pop operands and process
|
|
138
|
+
# 3. Store result in dictionary
|
|
139
|
+
def parse!
|
|
140
|
+
operand_stack = []
|
|
141
|
+
|
|
142
|
+
until @io.eof?
|
|
143
|
+
byte = read_byte
|
|
144
|
+
|
|
145
|
+
if operator?(byte)
|
|
146
|
+
# Process operator with current operand stack
|
|
147
|
+
operator = read_operator(byte)
|
|
148
|
+
process_operator(operator, operand_stack)
|
|
149
|
+
operand_stack.clear
|
|
150
|
+
else
|
|
151
|
+
# Read operand and push onto stack
|
|
152
|
+
@io.pos -= 1 # Unread the byte
|
|
153
|
+
operand = read_operand
|
|
154
|
+
operand_stack << operand
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if a byte is an operator
|
|
160
|
+
#
|
|
161
|
+
# @param byte [Integer] Byte value
|
|
162
|
+
# @return [Boolean] True if operator byte
|
|
163
|
+
def operator?(byte)
|
|
164
|
+
# Operators are 0-21 or escape (12) followed by another byte
|
|
165
|
+
byte <= 21 || byte == 12
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Read an operator (single or two-byte)
|
|
169
|
+
#
|
|
170
|
+
# @param first_byte [Integer] First operator byte
|
|
171
|
+
# @return [Integer, Array<Integer>] Operator identifier
|
|
172
|
+
def read_operator(first_byte)
|
|
173
|
+
if first_byte == 12
|
|
174
|
+
# Two-byte operator (escape operator)
|
|
175
|
+
second_byte = read_byte
|
|
176
|
+
[first_byte, second_byte]
|
|
177
|
+
else
|
|
178
|
+
# Single-byte operator
|
|
179
|
+
first_byte
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Process an operator with its operands
|
|
184
|
+
#
|
|
185
|
+
# @param operator [Integer, Array<Integer>] Operator identifier
|
|
186
|
+
# @param operands [Array] Operand stack
|
|
187
|
+
def process_operator(operator, operands)
|
|
188
|
+
operator_name = operator_name_for(operator)
|
|
189
|
+
return unless operator_name
|
|
190
|
+
|
|
191
|
+
# Store the operand(s) in the dictionary
|
|
192
|
+
# Most operators take a single operand, some take arrays
|
|
193
|
+
value = operands.size == 1 ? operands.first : operands.dup
|
|
194
|
+
@dict[operator_name] = value
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Get the operator name for an operator byte(s)
|
|
198
|
+
#
|
|
199
|
+
# @param operator [Integer, Array<Integer>] Operator identifier
|
|
200
|
+
# @return [Symbol, nil] Operator name or nil if unknown
|
|
201
|
+
def operator_name_for(operator)
|
|
202
|
+
# Check in the OPERATORS table (common operators)
|
|
203
|
+
self.class::OPERATORS[operator] || derived_operators[operator]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Get derived class-specific operators
|
|
207
|
+
#
|
|
208
|
+
# Subclasses override this to add their specific operators
|
|
209
|
+
#
|
|
210
|
+
# @return [Hash] Additional operators for this DICT type
|
|
211
|
+
def derived_operators
|
|
212
|
+
{}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Read a single operand from the DICT data
|
|
216
|
+
#
|
|
217
|
+
# Operands can be:
|
|
218
|
+
# - Small integers (1 byte: 32-246 or 247-254 with next byte)
|
|
219
|
+
# - Medium integers (3 bytes: 28 + 2 bytes)
|
|
220
|
+
# - Large integers (5 bytes: 29 + 4 bytes)
|
|
221
|
+
# - Real numbers (30 + nibble-encoded decimal)
|
|
222
|
+
#
|
|
223
|
+
# @return [Integer, Float] The operand value
|
|
224
|
+
def read_operand
|
|
225
|
+
byte = read_byte
|
|
226
|
+
|
|
227
|
+
case byte
|
|
228
|
+
when 28
|
|
229
|
+
# 3-byte signed integer
|
|
230
|
+
read_int16
|
|
231
|
+
when 29
|
|
232
|
+
# 5-byte signed integer
|
|
233
|
+
read_int32
|
|
234
|
+
when 30
|
|
235
|
+
# Real number (nibble-encoded)
|
|
236
|
+
read_real
|
|
237
|
+
when 32..246
|
|
238
|
+
# Small integer: -107 to +107
|
|
239
|
+
byte - 139
|
|
240
|
+
when 247..250
|
|
241
|
+
# Positive 2-byte integer
|
|
242
|
+
second_byte = read_byte
|
|
243
|
+
(byte - 247) * 256 + second_byte + 108
|
|
244
|
+
when 251..254
|
|
245
|
+
# Negative 2-byte integer
|
|
246
|
+
second_byte = read_byte
|
|
247
|
+
-(byte - 251) * 256 - second_byte - 108
|
|
248
|
+
else
|
|
249
|
+
raise CorruptedTableError,
|
|
250
|
+
"Invalid DICT operand byte: #{byte}"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Read a 16-bit signed integer (big-endian)
|
|
255
|
+
#
|
|
256
|
+
# @return [Integer] Signed 16-bit value
|
|
257
|
+
def read_int16
|
|
258
|
+
bytes = @io.read(2)
|
|
259
|
+
if bytes.nil? || bytes.bytesize < 2
|
|
260
|
+
raise CorruptedTableError,
|
|
261
|
+
"Unexpected end of DICT"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
value = bytes.unpack1("n") # Unsigned 16-bit big-endian
|
|
265
|
+
# Convert to signed
|
|
266
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Read a 32-bit signed integer (big-endian)
|
|
270
|
+
#
|
|
271
|
+
# @return [Integer] Signed 32-bit value
|
|
272
|
+
def read_int32
|
|
273
|
+
bytes = @io.read(4)
|
|
274
|
+
if bytes.nil? || bytes.bytesize < 4
|
|
275
|
+
raise CorruptedTableError,
|
|
276
|
+
"Unexpected end of DICT"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
value = bytes.unpack1("N") # Unsigned 32-bit big-endian
|
|
280
|
+
# Convert to signed
|
|
281
|
+
value > 0x7FFFFFFF ? value - 0x100000000 : value
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Read a real number (nibble-encoded)
|
|
285
|
+
#
|
|
286
|
+
# Real numbers in CFF are encoded as a sequence of nibbles (4-bit values)
|
|
287
|
+
# where each nibble represents a digit or special character.
|
|
288
|
+
#
|
|
289
|
+
# Nibble values:
|
|
290
|
+
# - 0-9: Decimal digits
|
|
291
|
+
# - a (10): Decimal point
|
|
292
|
+
# - b (11): Positive exponent (E)
|
|
293
|
+
# - c (12): Negative exponent (E-)
|
|
294
|
+
# - d (13): Reserved
|
|
295
|
+
# - e (14): Minus sign
|
|
296
|
+
# - f (15): End of number
|
|
297
|
+
#
|
|
298
|
+
# @return [Float] The decoded real number
|
|
299
|
+
def read_real
|
|
300
|
+
nibbles = []
|
|
301
|
+
|
|
302
|
+
loop do
|
|
303
|
+
byte = read_byte
|
|
304
|
+
high_nibble = (byte >> 4) & 0x0F
|
|
305
|
+
low_nibble = byte & 0x0F
|
|
306
|
+
|
|
307
|
+
break if high_nibble == 0xF
|
|
308
|
+
|
|
309
|
+
nibbles << high_nibble
|
|
310
|
+
|
|
311
|
+
break if low_nibble == 0xF
|
|
312
|
+
|
|
313
|
+
nibbles << low_nibble
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Convert nibbles to string representation
|
|
317
|
+
str = +""
|
|
318
|
+
nibbles.each do |nibble|
|
|
319
|
+
case nibble
|
|
320
|
+
when 0..9
|
|
321
|
+
str << nibble.to_s
|
|
322
|
+
when 0xa # Decimal point
|
|
323
|
+
str << "."
|
|
324
|
+
when 0xb # Positive exponent (E)
|
|
325
|
+
str << "e"
|
|
326
|
+
when 0xc # Negative exponent (E-)
|
|
327
|
+
str << "e-"
|
|
328
|
+
when 0xe # Minus sign
|
|
329
|
+
str << "-"
|
|
330
|
+
when 0xd, 0xf # Reserved or end marker
|
|
331
|
+
# Skip
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Convert to float
|
|
336
|
+
str.to_f
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Read a single byte from the IO
|
|
340
|
+
#
|
|
341
|
+
# @return [Integer] Byte value (0-255)
|
|
342
|
+
def read_byte
|
|
343
|
+
byte = @io.getbyte
|
|
344
|
+
raise CorruptedTableError, "Unexpected end of DICT" if byte.nil?
|
|
345
|
+
|
|
346
|
+
byte
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CFF DICT (Dictionary) structure builder
|
|
9
|
+
#
|
|
10
|
+
# [`DictBuilder`](lib/fontisan/tables/cff/dict_builder.rb) constructs
|
|
11
|
+
# binary DICT structures from hash representations. DICTs in CFF use a
|
|
12
|
+
# compact operand-operator format similar to PostScript.
|
|
13
|
+
#
|
|
14
|
+
# The builder encodes operands in various compact formats and writes
|
|
15
|
+
# operators according to the CFF specification.
|
|
16
|
+
#
|
|
17
|
+
# Operand Encoding:
|
|
18
|
+
# - Small integers (-107 to +107): Single byte (32-246)
|
|
19
|
+
# - Medium integers (108 to 1131): Two bytes (247-250 + byte)
|
|
20
|
+
# - Medium integers (-1131 to -108): Two bytes (251-254 + byte)
|
|
21
|
+
# - Larger integers: Three bytes (28 + 2 bytes) or five bytes (29 + 4 bytes)
|
|
22
|
+
# - Real numbers: Nibble-encoded (30 + nibbles + 0xF terminator)
|
|
23
|
+
#
|
|
24
|
+
# Operators:
|
|
25
|
+
# - Single-byte: 0-21
|
|
26
|
+
# - Two-byte: 12 followed by second byte
|
|
27
|
+
#
|
|
28
|
+
# Reference: CFF specification section 4 "DICT Data"
|
|
29
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
30
|
+
#
|
|
31
|
+
# @example Building a DICT
|
|
32
|
+
# dict_hash = { version: 391, notice: 392, charset: 0 }
|
|
33
|
+
# dict_data = Fontisan::Tables::Cff::DictBuilder.build(dict_hash)
|
|
34
|
+
class DictBuilder
|
|
35
|
+
# Operator mapping (name => byte(s))
|
|
36
|
+
OPERATORS = {
|
|
37
|
+
version: 0,
|
|
38
|
+
notice: 1,
|
|
39
|
+
full_name: 2,
|
|
40
|
+
family_name: 3,
|
|
41
|
+
weight: 4,
|
|
42
|
+
charset: 15,
|
|
43
|
+
encoding: 16,
|
|
44
|
+
charstrings: 17,
|
|
45
|
+
private: 18,
|
|
46
|
+
copyright: [12, 0],
|
|
47
|
+
is_fixed_pitch: [12, 1],
|
|
48
|
+
italic_angle: [12, 2],
|
|
49
|
+
underline_position: [12, 3],
|
|
50
|
+
underline_thickness: [12, 4],
|
|
51
|
+
paint_type: [12, 5],
|
|
52
|
+
charstring_type: [12, 6],
|
|
53
|
+
font_matrix: [12, 7],
|
|
54
|
+
stroke_width: [12, 8],
|
|
55
|
+
synthetic_base: [12, 20],
|
|
56
|
+
postscript: [12, 21],
|
|
57
|
+
base_font_name: [12, 22],
|
|
58
|
+
base_font_blend: [12, 23],
|
|
59
|
+
# Private DICT operators
|
|
60
|
+
subrs: 19,
|
|
61
|
+
default_width_x: 20,
|
|
62
|
+
nominal_width_x: 21,
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# Build DICT structure from hash
|
|
66
|
+
#
|
|
67
|
+
# @param dict_hash [Hash] Hash of operator => value pairs
|
|
68
|
+
# @return [String] Binary DICT data
|
|
69
|
+
# @raise [ArgumentError] If dict_hash is invalid
|
|
70
|
+
def self.build(dict_hash)
|
|
71
|
+
validate_dict!(dict_hash)
|
|
72
|
+
|
|
73
|
+
return "".b if dict_hash.empty?
|
|
74
|
+
|
|
75
|
+
output = StringIO.new("".b)
|
|
76
|
+
|
|
77
|
+
# Encode each operator with its operands
|
|
78
|
+
dict_hash.each do |operator_name, value|
|
|
79
|
+
# Get operator bytes
|
|
80
|
+
operator_bytes = operator_for_name(operator_name)
|
|
81
|
+
raise ArgumentError, "Unknown operator: #{operator_name}" unless operator_bytes
|
|
82
|
+
|
|
83
|
+
# Write operands (value can be single value or array)
|
|
84
|
+
if value.is_a?(Array)
|
|
85
|
+
value.each { |v| write_operand(output, v) }
|
|
86
|
+
else
|
|
87
|
+
write_operand(output, value)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Write operator
|
|
91
|
+
write_operator(output, operator_bytes)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
output.string
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validate dict parameter
|
|
98
|
+
#
|
|
99
|
+
# @param dict_hash [Object] Dictionary to validate
|
|
100
|
+
# @raise [ArgumentError] If dict_hash is invalid
|
|
101
|
+
def self.validate_dict!(dict_hash)
|
|
102
|
+
unless dict_hash.is_a?(Hash)
|
|
103
|
+
raise ArgumentError,
|
|
104
|
+
"dict_hash must be Hash, got: #{dict_hash.class}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
private_class_method :validate_dict!
|
|
108
|
+
|
|
109
|
+
# Get operator bytes for operator name
|
|
110
|
+
#
|
|
111
|
+
# @param operator_name [Symbol] Operator name
|
|
112
|
+
# @return [Integer, Array<Integer>, nil] Operator byte(s) or nil
|
|
113
|
+
def self.operator_for_name(operator_name)
|
|
114
|
+
OPERATORS[operator_name]
|
|
115
|
+
end
|
|
116
|
+
private_class_method :operator_for_name
|
|
117
|
+
|
|
118
|
+
# Write an operand value to output
|
|
119
|
+
#
|
|
120
|
+
# @param io [StringIO] Output stream
|
|
121
|
+
# @param value [Integer, Float] Operand value
|
|
122
|
+
def self.write_operand(io, value)
|
|
123
|
+
if value.is_a?(Float)
|
|
124
|
+
write_real(io, value)
|
|
125
|
+
else
|
|
126
|
+
write_integer(io, value)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
private_class_method :write_operand
|
|
130
|
+
|
|
131
|
+
# Write an integer operand
|
|
132
|
+
#
|
|
133
|
+
# @param io [StringIO] Output stream
|
|
134
|
+
# @param value [Integer] Integer value
|
|
135
|
+
def self.write_integer(io, value)
|
|
136
|
+
if value >= -107 && value <= 107
|
|
137
|
+
# Single byte: 32-246 represents -107 to +107
|
|
138
|
+
io.putc(value + 139)
|
|
139
|
+
elsif value >= 108 && value <= 1131
|
|
140
|
+
# Positive two-byte: 247-250
|
|
141
|
+
adjusted = value - 108
|
|
142
|
+
b0 = 247 + (adjusted / 256)
|
|
143
|
+
b1 = adjusted % 256
|
|
144
|
+
io.putc(b0)
|
|
145
|
+
io.putc(b1)
|
|
146
|
+
elsif value >= -1131 && value <= -108
|
|
147
|
+
# Negative two-byte: 251-254
|
|
148
|
+
adjusted = -value - 108
|
|
149
|
+
b0 = 251 + (adjusted / 256)
|
|
150
|
+
b1 = adjusted % 256
|
|
151
|
+
io.putc(b0)
|
|
152
|
+
io.putc(b1)
|
|
153
|
+
elsif value >= -32768 && value <= 32767
|
|
154
|
+
# Three-byte signed 16-bit
|
|
155
|
+
io.putc(28)
|
|
156
|
+
io.write([value].pack("s>")) # Signed 16-bit big-endian
|
|
157
|
+
else
|
|
158
|
+
# Five-byte signed 32-bit
|
|
159
|
+
io.putc(29)
|
|
160
|
+
io.write([value].pack("l>")) # Signed 32-bit big-endian
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
private_class_method :write_integer
|
|
164
|
+
|
|
165
|
+
# Write a real number operand
|
|
166
|
+
#
|
|
167
|
+
# Real numbers are encoded using nibbles (4-bit values).
|
|
168
|
+
# Each nibble represents a digit or special character.
|
|
169
|
+
#
|
|
170
|
+
# Nibble values:
|
|
171
|
+
# - 0-9: Decimal digits
|
|
172
|
+
# - a (10): Decimal point
|
|
173
|
+
# - b (11): Positive exponent (E)
|
|
174
|
+
# - c (12): Negative exponent (E-)
|
|
175
|
+
# - e (14): Minus sign
|
|
176
|
+
# - f (15): End of number
|
|
177
|
+
#
|
|
178
|
+
# @param io [StringIO] Output stream
|
|
179
|
+
# @param value [Float] Real number value
|
|
180
|
+
def self.write_real(io, value)
|
|
181
|
+
io.putc(30) # Real number marker
|
|
182
|
+
|
|
183
|
+
# Convert to string representation
|
|
184
|
+
str = value.to_s
|
|
185
|
+
|
|
186
|
+
# Handle special cases
|
|
187
|
+
str = "0" if str == "0.0"
|
|
188
|
+
|
|
189
|
+
# Convert string to nibbles
|
|
190
|
+
nibbles = []
|
|
191
|
+
|
|
192
|
+
str.each_char do |char|
|
|
193
|
+
case char
|
|
194
|
+
when "0".."9"
|
|
195
|
+
nibbles << char.to_i
|
|
196
|
+
when "."
|
|
197
|
+
nibbles << 0xa
|
|
198
|
+
when "-"
|
|
199
|
+
nibbles << 0xe
|
|
200
|
+
when "e", "E"
|
|
201
|
+
# Check if next char is minus
|
|
202
|
+
nibbles << 0xb # Default to positive exponent
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Handle negative exponent
|
|
207
|
+
if str.include?("e-") || str.include?("E-")
|
|
208
|
+
# Replace last 0xb with 0xc
|
|
209
|
+
exp_index = nibbles.rindex(0xb)
|
|
210
|
+
nibbles[exp_index] = 0xc if exp_index
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Add terminator
|
|
214
|
+
nibbles << 0xf
|
|
215
|
+
|
|
216
|
+
# Pack nibbles into bytes
|
|
217
|
+
nibbles.each_slice(2) do |high, low|
|
|
218
|
+
low ||= 0xf # Pad with terminator if odd number
|
|
219
|
+
byte = (high << 4) | low
|
|
220
|
+
io.putc(byte)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
private_class_method :write_real
|
|
224
|
+
|
|
225
|
+
# Write an operator to output
|
|
226
|
+
#
|
|
227
|
+
# @param io [StringIO] Output stream
|
|
228
|
+
# @param operator_bytes [Integer, Array<Integer>] Operator byte(s)
|
|
229
|
+
def self.write_operator(io, operator_bytes)
|
|
230
|
+
if operator_bytes.is_a?(Array)
|
|
231
|
+
# Two-byte operator
|
|
232
|
+
operator_bytes.each { |byte| io.putc(byte) }
|
|
233
|
+
else
|
|
234
|
+
# Single-byte operator
|
|
235
|
+
io.putc(operator_bytes)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
private_class_method :write_operator
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|