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,257 @@
|
|
|
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
|
+
font_bbox: 5,
|
|
56
|
+
synthetic_base: [12, 20],
|
|
57
|
+
postscript: [12, 21],
|
|
58
|
+
base_font_name: [12, 22],
|
|
59
|
+
base_font_blend: [12, 23],
|
|
60
|
+
# Private DICT operators
|
|
61
|
+
subrs: 19,
|
|
62
|
+
default_width_x: 20,
|
|
63
|
+
nominal_width_x: 21,
|
|
64
|
+
# Hint-related Private DICT operators
|
|
65
|
+
blue_values: 6,
|
|
66
|
+
other_blues: 7,
|
|
67
|
+
family_blues: 8,
|
|
68
|
+
family_other_blues: 9,
|
|
69
|
+
std_hw: 10,
|
|
70
|
+
std_vw: 11,
|
|
71
|
+
stem_snap_h: [12, 12],
|
|
72
|
+
stem_snap_v: [12, 13],
|
|
73
|
+
blue_scale: [12, 9],
|
|
74
|
+
blue_shift: [12, 10],
|
|
75
|
+
blue_fuzz: [12, 11],
|
|
76
|
+
force_bold: [12, 14],
|
|
77
|
+
language_group: [12, 17],
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
# Build DICT structure from hash
|
|
81
|
+
#
|
|
82
|
+
# @param dict_hash [Hash] Hash of operator => value pairs
|
|
83
|
+
# @return [String] Binary DICT data
|
|
84
|
+
# @raise [ArgumentError] If dict_hash is invalid
|
|
85
|
+
def self.build(dict_hash)
|
|
86
|
+
validate_dict!(dict_hash)
|
|
87
|
+
|
|
88
|
+
return "".b if dict_hash.empty?
|
|
89
|
+
|
|
90
|
+
output = StringIO.new("".b)
|
|
91
|
+
|
|
92
|
+
# Encode each operator with its operands
|
|
93
|
+
dict_hash.each do |operator_name, value|
|
|
94
|
+
# Get operator bytes
|
|
95
|
+
operator_bytes = operator_for_name(operator_name)
|
|
96
|
+
raise ArgumentError, "Unknown operator: #{operator_name}" unless operator_bytes
|
|
97
|
+
|
|
98
|
+
# Write operands (value can be single value or array)
|
|
99
|
+
if value.is_a?(Array)
|
|
100
|
+
value.each { |v| write_operand(output, v) }
|
|
101
|
+
else
|
|
102
|
+
write_operand(output, value)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Write operator
|
|
106
|
+
write_operator(output, operator_bytes)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
output.string
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Validate dict parameter
|
|
113
|
+
#
|
|
114
|
+
# @param dict_hash [Object] Dictionary to validate
|
|
115
|
+
# @raise [ArgumentError] If dict_hash is invalid
|
|
116
|
+
def self.validate_dict!(dict_hash)
|
|
117
|
+
unless dict_hash.is_a?(Hash)
|
|
118
|
+
raise ArgumentError,
|
|
119
|
+
"dict_hash must be Hash, got: #{dict_hash.class}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
private_class_method :validate_dict!
|
|
123
|
+
|
|
124
|
+
# Get operator bytes for operator name
|
|
125
|
+
#
|
|
126
|
+
# @param operator_name [Symbol] Operator name
|
|
127
|
+
# @return [Integer, Array<Integer>, nil] Operator byte(s) or nil
|
|
128
|
+
def self.operator_for_name(operator_name)
|
|
129
|
+
OPERATORS[operator_name]
|
|
130
|
+
end
|
|
131
|
+
private_class_method :operator_for_name
|
|
132
|
+
|
|
133
|
+
# Write an operand value to output
|
|
134
|
+
#
|
|
135
|
+
# @param io [StringIO] Output stream
|
|
136
|
+
# @param value [Integer, Float] Operand value
|
|
137
|
+
def self.write_operand(io, value)
|
|
138
|
+
if value.is_a?(Float)
|
|
139
|
+
write_real(io, value)
|
|
140
|
+
else
|
|
141
|
+
write_integer(io, value)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
private_class_method :write_operand
|
|
145
|
+
|
|
146
|
+
# Write an integer operand
|
|
147
|
+
#
|
|
148
|
+
# @param io [StringIO] Output stream
|
|
149
|
+
# @param value [Integer] Integer value
|
|
150
|
+
def self.write_integer(io, value)
|
|
151
|
+
if value >= -107 && value <= 107
|
|
152
|
+
# Single byte: 32-246 represents -107 to +107
|
|
153
|
+
io.putc(value + 139)
|
|
154
|
+
elsif value >= 108 && value <= 1131
|
|
155
|
+
# Positive two-byte: 247-250
|
|
156
|
+
adjusted = value - 108
|
|
157
|
+
b0 = 247 + (adjusted / 256)
|
|
158
|
+
b1 = adjusted % 256
|
|
159
|
+
io.putc(b0)
|
|
160
|
+
io.putc(b1)
|
|
161
|
+
elsif value >= -1131 && value <= -108
|
|
162
|
+
# Negative two-byte: 251-254
|
|
163
|
+
adjusted = -value - 108
|
|
164
|
+
b0 = 251 + (adjusted / 256)
|
|
165
|
+
b1 = adjusted % 256
|
|
166
|
+
io.putc(b0)
|
|
167
|
+
io.putc(b1)
|
|
168
|
+
elsif value >= -32768 && value <= 32767
|
|
169
|
+
# Three-byte signed 16-bit
|
|
170
|
+
io.putc(28)
|
|
171
|
+
io.write([value].pack("s>")) # Signed 16-bit big-endian
|
|
172
|
+
else
|
|
173
|
+
# Five-byte signed 32-bit
|
|
174
|
+
io.putc(29)
|
|
175
|
+
io.write([value].pack("l>")) # Signed 32-bit big-endian
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
private_class_method :write_integer
|
|
179
|
+
|
|
180
|
+
# Write a real number operand
|
|
181
|
+
#
|
|
182
|
+
# Real numbers are encoded using nibbles (4-bit values).
|
|
183
|
+
# Each nibble represents a digit or special character.
|
|
184
|
+
#
|
|
185
|
+
# Nibble values:
|
|
186
|
+
# - 0-9: Decimal digits
|
|
187
|
+
# - a (10): Decimal point
|
|
188
|
+
# - b (11): Positive exponent (E)
|
|
189
|
+
# - c (12): Negative exponent (E-)
|
|
190
|
+
# - e (14): Minus sign
|
|
191
|
+
# - f (15): End of number
|
|
192
|
+
#
|
|
193
|
+
# @param io [StringIO] Output stream
|
|
194
|
+
# @param value [Float] Real number value
|
|
195
|
+
def self.write_real(io, value)
|
|
196
|
+
io.putc(30) # Real number marker
|
|
197
|
+
|
|
198
|
+
# Convert to string representation
|
|
199
|
+
str = value.to_s
|
|
200
|
+
|
|
201
|
+
# Handle special cases
|
|
202
|
+
str = "0" if str == "0.0"
|
|
203
|
+
|
|
204
|
+
# Convert string to nibbles
|
|
205
|
+
nibbles = []
|
|
206
|
+
|
|
207
|
+
str.each_char do |char|
|
|
208
|
+
case char
|
|
209
|
+
when "0".."9"
|
|
210
|
+
nibbles << char.to_i
|
|
211
|
+
when "."
|
|
212
|
+
nibbles << 0xa
|
|
213
|
+
when "-"
|
|
214
|
+
nibbles << 0xe
|
|
215
|
+
when "e", "E"
|
|
216
|
+
# Check if next char is minus
|
|
217
|
+
nibbles << 0xb # Default to positive exponent
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Handle negative exponent
|
|
222
|
+
if str.include?("e-") || str.include?("E-")
|
|
223
|
+
# Replace last 0xb with 0xc
|
|
224
|
+
exp_index = nibbles.rindex(0xb)
|
|
225
|
+
nibbles[exp_index] = 0xc if exp_index
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Add terminator
|
|
229
|
+
nibbles << 0xf
|
|
230
|
+
|
|
231
|
+
# Pack nibbles into bytes
|
|
232
|
+
nibbles.each_slice(2) do |high, low|
|
|
233
|
+
low ||= 0xf # Pad with terminator if odd number
|
|
234
|
+
byte = (high << 4) | low
|
|
235
|
+
io.putc(byte)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
private_class_method :write_real
|
|
239
|
+
|
|
240
|
+
# Write an operator to output
|
|
241
|
+
#
|
|
242
|
+
# @param io [StringIO] Output stream
|
|
243
|
+
# @param operator_bytes [Integer, Array<Integer>] Operator byte(s)
|
|
244
|
+
def self.write_operator(io, operator_bytes)
|
|
245
|
+
if operator_bytes.is_a?(Array)
|
|
246
|
+
# Two-byte operator
|
|
247
|
+
operator_bytes.each { |byte| io.putc(byte) }
|
|
248
|
+
else
|
|
249
|
+
# Single-byte operator
|
|
250
|
+
io.putc(operator_bytes)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
private_class_method :write_operator
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
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 Encoding structure
|
|
10
|
+
#
|
|
11
|
+
# Encoding maps character codes to glyph IDs (GIDs).
|
|
12
|
+
# GID 0 (.notdef) is not encoded.
|
|
13
|
+
#
|
|
14
|
+
# Three formats:
|
|
15
|
+
# - Format 0: Array of codes (one per glyph)
|
|
16
|
+
# - Format 1: Ranges of consecutive codes
|
|
17
|
+
# - Format 0/1 with supplement: Format 0 or 1 with additional mappings
|
|
18
|
+
#
|
|
19
|
+
# Predefined encodings:
|
|
20
|
+
# - 0: Standard encoding (Adobe standard character set)
|
|
21
|
+
# - 1: Expert encoding (Adobe expert character set)
|
|
22
|
+
#
|
|
23
|
+
# Reference: CFF specification section 14 "Encodings"
|
|
24
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
25
|
+
#
|
|
26
|
+
# @example Reading an Encoding
|
|
27
|
+
# encoding = Fontisan::Tables::Cff::Encoding.new(data, num_glyphs)
|
|
28
|
+
# puts encoding.glyph_id(65) # => GID for char code 65 ('A')
|
|
29
|
+
# puts encoding.char_code(5) # => char code for GID 5
|
|
30
|
+
class Encoding
|
|
31
|
+
# Predefined encoding identifiers
|
|
32
|
+
PREDEFINED = {
|
|
33
|
+
0 => :standard,
|
|
34
|
+
1 => :expert,
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Format mask to extract format type
|
|
38
|
+
FORMAT_MASK = 0x7F
|
|
39
|
+
|
|
40
|
+
# @return [Integer] Encoding format (0 or 1)
|
|
41
|
+
attr_reader :format_type
|
|
42
|
+
|
|
43
|
+
# @return [Hash<Integer, Integer>] Map from character code to GID
|
|
44
|
+
attr_reader :code_to_gid
|
|
45
|
+
|
|
46
|
+
# @return [Hash<Integer, Integer>] Map from GID to character code
|
|
47
|
+
attr_reader :gid_to_code
|
|
48
|
+
|
|
49
|
+
# Initialize an Encoding
|
|
50
|
+
#
|
|
51
|
+
# @param data [String, Integer] Binary data or predefined encoding ID
|
|
52
|
+
# @param num_glyphs [Integer] Number of glyphs in the font
|
|
53
|
+
def initialize(data, num_glyphs)
|
|
54
|
+
@num_glyphs = num_glyphs
|
|
55
|
+
@code_to_gid = {}
|
|
56
|
+
@gid_to_code = {}
|
|
57
|
+
|
|
58
|
+
# GID 0 (.notdef) is always at code 0
|
|
59
|
+
@code_to_gid[0] = 0
|
|
60
|
+
@gid_to_code[0] = 0
|
|
61
|
+
|
|
62
|
+
if data.is_a?(Integer) && PREDEFINED.key?(data)
|
|
63
|
+
load_predefined_encoding(data)
|
|
64
|
+
else
|
|
65
|
+
@data = data
|
|
66
|
+
parse!
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get GID for a character code
|
|
71
|
+
#
|
|
72
|
+
# @param code [Integer] Character code (0-255)
|
|
73
|
+
# @return [Integer, nil] Glyph ID or nil if not mapped
|
|
74
|
+
def glyph_id(code)
|
|
75
|
+
@code_to_gid[code]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get character code for a GID
|
|
79
|
+
#
|
|
80
|
+
# @param gid [Integer] Glyph ID
|
|
81
|
+
# @return [Integer, nil] Character code or nil if not mapped
|
|
82
|
+
def char_code(gid)
|
|
83
|
+
@gid_to_code[gid]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get the format symbol
|
|
87
|
+
#
|
|
88
|
+
# @return [Symbol] Format identifier (:array, :range, or :predefined)
|
|
89
|
+
def format
|
|
90
|
+
return :predefined unless @format_type
|
|
91
|
+
|
|
92
|
+
@format_type.zero? ? :array : :range
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if encoding has supplement
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean] True if encoding has supplemental mappings
|
|
98
|
+
def has_supplement?
|
|
99
|
+
@has_supplement || false
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Parse the Encoding from binary data
|
|
105
|
+
def parse!
|
|
106
|
+
io = StringIO.new(@data)
|
|
107
|
+
format_byte = read_uint8(io)
|
|
108
|
+
|
|
109
|
+
# Extract format (lower 7 bits) and supplement flag (bit 7)
|
|
110
|
+
@format_type = format_byte & FORMAT_MASK
|
|
111
|
+
@has_supplement = (format_byte & 0x80) != 0
|
|
112
|
+
|
|
113
|
+
case @format_type
|
|
114
|
+
when 0
|
|
115
|
+
parse_format_0(io)
|
|
116
|
+
when 1
|
|
117
|
+
parse_format_1(io)
|
|
118
|
+
else
|
|
119
|
+
raise CorruptedTableError,
|
|
120
|
+
"Invalid Encoding format: #{@format_type}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Parse supplemental encoding if present
|
|
124
|
+
parse_supplement(io) if @has_supplement
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
raise CorruptedTableError,
|
|
127
|
+
"Failed to parse Encoding: #{e.message}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Parse Format 0: Array of codes
|
|
131
|
+
#
|
|
132
|
+
# Format 0 directly lists character codes for each glyph (except
|
|
133
|
+
# .notdef)
|
|
134
|
+
#
|
|
135
|
+
# @param io [StringIO] Input stream positioned after format byte
|
|
136
|
+
def parse_format_0(io)
|
|
137
|
+
n_codes = read_uint8(io)
|
|
138
|
+
|
|
139
|
+
# Read one code per glyph (GIDs start at 1, skipping .notdef)
|
|
140
|
+
n_codes.times do |i|
|
|
141
|
+
code = read_uint8(io)
|
|
142
|
+
gid = i + 1 # GID 0 is .notdef, so start at 1
|
|
143
|
+
|
|
144
|
+
@code_to_gid[code] = gid
|
|
145
|
+
@gid_to_code[gid] = code
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Parse Format 1: Ranges of codes
|
|
150
|
+
#
|
|
151
|
+
# Format 1 uses ranges: first code, nLeft (number of consecutive codes)
|
|
152
|
+
#
|
|
153
|
+
# @param io [StringIO] Input stream positioned after format byte
|
|
154
|
+
def parse_format_1(io)
|
|
155
|
+
n_ranges = read_uint8(io)
|
|
156
|
+
gid = 1 # Start at GID 1 (skip .notdef at 0)
|
|
157
|
+
|
|
158
|
+
n_ranges.times do
|
|
159
|
+
first_code = read_uint8(io)
|
|
160
|
+
n_left = read_uint8(io)
|
|
161
|
+
|
|
162
|
+
# Map the range of codes
|
|
163
|
+
(n_left + 1).times do |i|
|
|
164
|
+
code = first_code + i
|
|
165
|
+
@code_to_gid[code] = gid
|
|
166
|
+
@gid_to_code[gid] = code
|
|
167
|
+
gid += 1
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Parse supplemental encoding
|
|
173
|
+
#
|
|
174
|
+
# Supplemental encoding provides additional code-to-GID mappings
|
|
175
|
+
#
|
|
176
|
+
# @param io [StringIO] Input stream positioned after main encoding data
|
|
177
|
+
def parse_supplement(io)
|
|
178
|
+
n_sups = read_uint8(io)
|
|
179
|
+
|
|
180
|
+
n_sups.times do
|
|
181
|
+
read_uint8(io)
|
|
182
|
+
read_uint16(io)
|
|
183
|
+
|
|
184
|
+
# Find GID for this SID (requires charset lookup)
|
|
185
|
+
# For now, we'll store the code mapping
|
|
186
|
+
# A full implementation would need charset access to resolve SID to
|
|
187
|
+
# GID
|
|
188
|
+
# This is typically used when the charset has glyphs not in the
|
|
189
|
+
# standard encoding
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Load a predefined encoding
|
|
194
|
+
#
|
|
195
|
+
# @param encoding_id [Integer] Predefined encoding ID (0 or 1)
|
|
196
|
+
def load_predefined_encoding(encoding_id)
|
|
197
|
+
@format_type = nil # Predefined encodings don't have a format
|
|
198
|
+
|
|
199
|
+
case encoding_id
|
|
200
|
+
when 0
|
|
201
|
+
load_standard_encoding
|
|
202
|
+
when 1
|
|
203
|
+
load_expert_encoding
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Load Standard encoding
|
|
208
|
+
#
|
|
209
|
+
# Adobe Standard Encoding is the default encoding for Type 1 fonts
|
|
210
|
+
# It maps common Latin characters to specific codes
|
|
211
|
+
def load_standard_encoding
|
|
212
|
+
# Standard encoding for common characters (codes 0-255)
|
|
213
|
+
# This is a simplified version - a full implementation would include
|
|
214
|
+
# all 256 standard encoding mappings from the CFF specification
|
|
215
|
+
# Appendix B
|
|
216
|
+
|
|
217
|
+
# Common ASCII mappings (basic Latin)
|
|
218
|
+
gid = 1
|
|
219
|
+
(32..126).each do |code|
|
|
220
|
+
@code_to_gid[code] = gid
|
|
221
|
+
@gid_to_code[gid] = code
|
|
222
|
+
gid += 1
|
|
223
|
+
break if gid >= @num_glyphs
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Load Expert encoding
|
|
228
|
+
#
|
|
229
|
+
# Adobe Expert Encoding is used for expert fonts with special
|
|
230
|
+
# characters like small caps, old-style figures, ligatures, etc.
|
|
231
|
+
def load_expert_encoding
|
|
232
|
+
# Expert encoding for special characters
|
|
233
|
+
# This is a simplified version - a full implementation would include
|
|
234
|
+
# all expert encoding mappings from the CFF specification Appendix C
|
|
235
|
+
|
|
236
|
+
# Map some common expert characters
|
|
237
|
+
gid = 1
|
|
238
|
+
expert_codes = [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
|
|
239
|
+
45, 46, 47]
|
|
240
|
+
expert_codes.each do |code|
|
|
241
|
+
@code_to_gid[code] = gid if gid < @num_glyphs
|
|
242
|
+
@gid_to_code[gid] = code if gid < @num_glyphs
|
|
243
|
+
gid += 1
|
|
244
|
+
break if gid >= @num_glyphs
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Read an unsigned 8-bit integer
|
|
249
|
+
#
|
|
250
|
+
# @param io [StringIO] Input stream
|
|
251
|
+
# @return [Integer] The value
|
|
252
|
+
def read_uint8(io)
|
|
253
|
+
byte = io.read(1)
|
|
254
|
+
raise CorruptedTableError, "Unexpected end of Encoding data" if
|
|
255
|
+
byte.nil?
|
|
256
|
+
|
|
257
|
+
byte.unpack1("C")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Read an unsigned 16-bit integer (big-endian)
|
|
261
|
+
#
|
|
262
|
+
# @param io [StringIO] Input stream
|
|
263
|
+
# @return [Integer] The value
|
|
264
|
+
def read_uint16(io)
|
|
265
|
+
bytes = io.read(2)
|
|
266
|
+
raise CorruptedTableError, "Unexpected end of Encoding data" if
|
|
267
|
+
bytes.nil? || bytes.bytesize < 2
|
|
268
|
+
|
|
269
|
+
bytes.unpack1("n") # Big-endian unsigned 16-bit
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../binary/base_record"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
class Cff
|
|
8
|
+
# CFF Header structure
|
|
9
|
+
#
|
|
10
|
+
# The CFF header appears at the beginning of the CFF table and contains
|
|
11
|
+
# basic version and structural information about the CFF data.
|
|
12
|
+
#
|
|
13
|
+
# Structure (4 bytes minimum):
|
|
14
|
+
# - uint8: major version (always 1 for CFF, 2 for CFF2)
|
|
15
|
+
# - uint8: minor version (always 0)
|
|
16
|
+
# - uint8: hdr_size (header size in bytes, typically 4)
|
|
17
|
+
# - uint8: off_size (offset size used throughout CFF, 1-4 bytes)
|
|
18
|
+
#
|
|
19
|
+
# Reference: CFF specification section 4 "Header"
|
|
20
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
21
|
+
#
|
|
22
|
+
# @example Reading a CFF header
|
|
23
|
+
# data = File.binread("font.otf", 4, cff_offset)
|
|
24
|
+
# header = Fontisan::Tables::Cff::Header.read(data)
|
|
25
|
+
# puts header.major # => 1
|
|
26
|
+
# puts header.minor # => 0
|
|
27
|
+
# puts header.off_size # => 4
|
|
28
|
+
class Header < Binary::BaseRecord
|
|
29
|
+
# Major version number (1 for CFF, 2 for CFF2)
|
|
30
|
+
uint8 :major
|
|
31
|
+
|
|
32
|
+
# Minor version number (always 0)
|
|
33
|
+
uint8 :minor
|
|
34
|
+
|
|
35
|
+
# Header size in bytes (typically 4, but can be larger for extensions)
|
|
36
|
+
uint8 :hdr_size
|
|
37
|
+
|
|
38
|
+
# Offset size used throughout the CFF table
|
|
39
|
+
# Valid values are 1, 2, 3, or 4 bytes
|
|
40
|
+
#
|
|
41
|
+
# This determines how offsets are encoded in INDEX structures and
|
|
42
|
+
# other parts of the CFF table.
|
|
43
|
+
uint8 :off_size
|
|
44
|
+
|
|
45
|
+
# Check if this is a valid CFF version 1.0 header
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean] True if major version is 1 and minor is 0
|
|
48
|
+
def cff?
|
|
49
|
+
major == 1 && minor.zero?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Check if this is a CFF2 header (variable CFF fonts)
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean] True if major version is 2
|
|
55
|
+
def cff2?
|
|
56
|
+
major == 2
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the version as a string
|
|
60
|
+
#
|
|
61
|
+
# @return [String] Version in "major.minor" format
|
|
62
|
+
def version
|
|
63
|
+
"#{major}.#{minor}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Validate that the header has correct values
|
|
67
|
+
#
|
|
68
|
+
# @return [Boolean] True if header is valid
|
|
69
|
+
def valid?
|
|
70
|
+
# Major version must be 1 or 2
|
|
71
|
+
return false unless [1, 2].include?(major)
|
|
72
|
+
|
|
73
|
+
# Minor version must be 0
|
|
74
|
+
return false unless minor.zero?
|
|
75
|
+
|
|
76
|
+
# Header size must be at least 4 bytes
|
|
77
|
+
return false unless hdr_size >= 4
|
|
78
|
+
|
|
79
|
+
# Offset size must be between 1 and 4
|
|
80
|
+
return false unless (1..4).cover?(off_size)
|
|
81
|
+
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Validate header and raise error if invalid
|
|
86
|
+
#
|
|
87
|
+
# @raise [Fontisan::CorruptedTableError] If header is invalid
|
|
88
|
+
def validate!
|
|
89
|
+
return if valid?
|
|
90
|
+
|
|
91
|
+
message = "Invalid CFF header: " \
|
|
92
|
+
"version=#{version}, " \
|
|
93
|
+
"hdr_size=#{hdr_size}, " \
|
|
94
|
+
"off_size=#{off_size}"
|
|
95
|
+
error = Fontisan::CorruptedTableError.new(message)
|
|
96
|
+
error.set_backtrace(caller)
|
|
97
|
+
Kernel.raise(error)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|