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,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "charstring_parser"
|
|
4
|
+
require_relative "charstring_builder"
|
|
5
|
+
require_relative "index_builder"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Tables
|
|
9
|
+
class Cff
|
|
10
|
+
# Rebuilds CharStrings INDEX with modified CharStrings
|
|
11
|
+
#
|
|
12
|
+
# CharStringRebuilder provides high-level interface for modifying
|
|
13
|
+
# CharStrings in a CFF font. It extracts all CharStrings from the source
|
|
14
|
+
# INDEX, allows modifications through a callback, and rebuilds the INDEX
|
|
15
|
+
# with updated CharString data.
|
|
16
|
+
#
|
|
17
|
+
# Use Cases:
|
|
18
|
+
# - Per-glyph hint injection
|
|
19
|
+
# - CharString optimization
|
|
20
|
+
# - Subroutine insertion
|
|
21
|
+
# - Any operation requiring CharString modification
|
|
22
|
+
#
|
|
23
|
+
# @example Inject hints into specific glyphs
|
|
24
|
+
# rebuilder = CharStringRebuilder.new(charstrings_index)
|
|
25
|
+
# rebuilder.modify_charstring(42) do |operations|
|
|
26
|
+
# # Insert hint operations at beginning
|
|
27
|
+
# hint_ops = [
|
|
28
|
+
# { type: :operator, name: :hstem, operands: [10, 20] }
|
|
29
|
+
# ]
|
|
30
|
+
# hint_ops + operations
|
|
31
|
+
# end
|
|
32
|
+
# new_index_data = rebuilder.rebuild
|
|
33
|
+
class CharStringRebuilder
|
|
34
|
+
# @return [CharstringsIndex] Source CharStrings INDEX
|
|
35
|
+
attr_reader :source_index
|
|
36
|
+
|
|
37
|
+
# @return [Hash] Modified CharString data by glyph index
|
|
38
|
+
attr_reader :modifications
|
|
39
|
+
|
|
40
|
+
# Initialize rebuilder with source CharStrings INDEX
|
|
41
|
+
#
|
|
42
|
+
# @param source_index [CharstringsIndex] Source CharStrings INDEX
|
|
43
|
+
# @param stem_count [Integer] Number of stem hints (for parsing hintmask)
|
|
44
|
+
def initialize(source_index, stem_count: 0)
|
|
45
|
+
@source_index = source_index
|
|
46
|
+
@stem_count = stem_count
|
|
47
|
+
@modifications = {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Modify a CharString by glyph index
|
|
51
|
+
#
|
|
52
|
+
# The block receives the parsed operations for the glyph and should
|
|
53
|
+
# return modified operations.
|
|
54
|
+
#
|
|
55
|
+
# @param glyph_index [Integer] Glyph index (0 = .notdef)
|
|
56
|
+
# @yield [operations] Block to modify operations
|
|
57
|
+
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
58
|
+
# @yieldreturn [Array<Hash>] Modified operations
|
|
59
|
+
def modify_charstring(glyph_index, &block)
|
|
60
|
+
# Get original CharString data
|
|
61
|
+
original_data = @source_index[glyph_index]
|
|
62
|
+
return unless original_data
|
|
63
|
+
|
|
64
|
+
# Parse to operations
|
|
65
|
+
parser = CharStringParser.new(original_data, stem_count: @stem_count)
|
|
66
|
+
operations = parser.parse
|
|
67
|
+
|
|
68
|
+
# Apply modification
|
|
69
|
+
modified_operations = block.call(operations)
|
|
70
|
+
|
|
71
|
+
# Build new CharString
|
|
72
|
+
new_data = CharStringBuilder.build_from_operations(modified_operations)
|
|
73
|
+
|
|
74
|
+
# Store modification
|
|
75
|
+
@modifications[glyph_index] = new_data
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Rebuild CharStrings INDEX with modifications
|
|
79
|
+
#
|
|
80
|
+
# Creates new INDEX with modified CharStrings, keeping unmodified
|
|
81
|
+
# CharStrings unchanged.
|
|
82
|
+
#
|
|
83
|
+
# @return [String] Binary CharStrings INDEX data
|
|
84
|
+
def rebuild
|
|
85
|
+
# Collect all CharString data (modified and unmodified)
|
|
86
|
+
charstrings = []
|
|
87
|
+
|
|
88
|
+
(0...@source_index.count).each do |i|
|
|
89
|
+
if @modifications.key?(i)
|
|
90
|
+
# Use modified CharString
|
|
91
|
+
charstrings << @modifications[i]
|
|
92
|
+
else
|
|
93
|
+
# Use original CharString
|
|
94
|
+
charstrings << @source_index[i]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Build INDEX
|
|
99
|
+
IndexBuilder.build(charstrings)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Batch modify multiple CharStrings
|
|
103
|
+
#
|
|
104
|
+
# More efficient than calling modify_charstring multiple times.
|
|
105
|
+
#
|
|
106
|
+
# @param glyph_indices [Array<Integer>] Glyph indices to modify
|
|
107
|
+
# @yield [glyph_index, operations] Block to modify each glyph
|
|
108
|
+
# @yieldparam glyph_index [Integer] Current glyph index
|
|
109
|
+
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
110
|
+
# @yieldreturn [Array<Hash>] Modified operations
|
|
111
|
+
def batch_modify(glyph_indices, &block)
|
|
112
|
+
glyph_indices.each do |glyph_index|
|
|
113
|
+
modify_charstring(glyph_index) do |operations|
|
|
114
|
+
block.call(glyph_index, operations)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Modify all CharStrings
|
|
120
|
+
#
|
|
121
|
+
# Applies the same modification to every glyph.
|
|
122
|
+
#
|
|
123
|
+
# @yield [glyph_index, operations] Block to modify each glyph
|
|
124
|
+
# @yieldparam glyph_index [Integer] Current glyph index
|
|
125
|
+
# @yieldparam operations [Array<Hash>] Parsed operations
|
|
126
|
+
# @yieldreturn [Array<Hash>] Modified operations
|
|
127
|
+
def modify_all(&block)
|
|
128
|
+
(0...@source_index.count).each do |i|
|
|
129
|
+
modify_charstring(i) do |operations|
|
|
130
|
+
block.call(i, operations)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get CharString data (modified or original)
|
|
136
|
+
#
|
|
137
|
+
# @param glyph_index [Integer] Glyph index
|
|
138
|
+
# @return [String] CharString binary data
|
|
139
|
+
def charstring_data(glyph_index)
|
|
140
|
+
@modifications[glyph_index] || @source_index[glyph_index]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Check if glyph has been modified
|
|
144
|
+
#
|
|
145
|
+
# @param glyph_index [Integer] Glyph index
|
|
146
|
+
# @return [Boolean] True if modified
|
|
147
|
+
def modified?(glyph_index)
|
|
148
|
+
@modifications.key?(glyph_index)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Get count of modified glyphs
|
|
152
|
+
#
|
|
153
|
+
# @return [Integer] Number of modified glyphs
|
|
154
|
+
def modification_count
|
|
155
|
+
@modifications.size
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Clear all modifications
|
|
159
|
+
def clear_modifications
|
|
160
|
+
@modifications.clear
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Update stem count (needed for hintmask parsing)
|
|
164
|
+
#
|
|
165
|
+
# @param count [Integer] Number of stem hints
|
|
166
|
+
def stem_count=(count)
|
|
167
|
+
@stem_count = count
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "index"
|
|
4
|
+
require_relative "charstring"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
class Cff
|
|
9
|
+
# CharStrings INDEX wrapper
|
|
10
|
+
#
|
|
11
|
+
# This class wraps the CharStrings INDEX to provide convenient access
|
|
12
|
+
# to individual CharString objects. The CharStrings INDEX contains the
|
|
13
|
+
# glyph outline programs (Type 2 CharStrings) for each glyph in the font.
|
|
14
|
+
#
|
|
15
|
+
# CharStrings Format:
|
|
16
|
+
# - INDEX structure containing binary CharString data
|
|
17
|
+
# - Each entry is a Type 2 CharString program
|
|
18
|
+
# - Number of entries typically matches the number of glyphs
|
|
19
|
+
# - Index 0 is typically .notdef glyph
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# 1. Create from raw CharStrings INDEX data
|
|
23
|
+
# 2. Provide Private DICT and subroutine INDEXes for interpretation
|
|
24
|
+
# 3. Access individual CharStrings by glyph index
|
|
25
|
+
#
|
|
26
|
+
# Reference: CFF specification section 16 "Local/Global Subrs INDEXes"
|
|
27
|
+
# https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
|
|
28
|
+
#
|
|
29
|
+
# @example Using CharStringsIndex
|
|
30
|
+
# # Get CharStrings INDEX from CFF table
|
|
31
|
+
# charstrings_offset = top_dict.charstrings
|
|
32
|
+
# io = StringIO.new(cff.raw_data)
|
|
33
|
+
# io.seek(charstrings_offset)
|
|
34
|
+
# charstrings_index = CharstringsIndex.new(io, start_offset:
|
|
35
|
+
# charstrings_offset)
|
|
36
|
+
#
|
|
37
|
+
# # Get a specific CharString
|
|
38
|
+
# charstring = charstrings_index.charstring_at(
|
|
39
|
+
# glyph_index,
|
|
40
|
+
# private_dict,
|
|
41
|
+
# global_subrs,
|
|
42
|
+
# local_subrs
|
|
43
|
+
# )
|
|
44
|
+
#
|
|
45
|
+
# # Access CharString properties
|
|
46
|
+
# puts charstring.width
|
|
47
|
+
# puts charstring.bounding_box
|
|
48
|
+
# charstring.to_commands.each { |cmd| puts cmd.inspect }
|
|
49
|
+
class CharstringsIndex < Index
|
|
50
|
+
# Get a CharString object at the specified glyph index
|
|
51
|
+
#
|
|
52
|
+
# This method retrieves the binary CharString data at the given index
|
|
53
|
+
# and interprets it as a Type 2 CharString program.
|
|
54
|
+
#
|
|
55
|
+
# @param index [Integer] Glyph index (0-based, 0 is typically .notdef)
|
|
56
|
+
# @param private_dict [PrivateDict] Private DICT for width defaults
|
|
57
|
+
# @param global_subrs [Index] Global subroutines INDEX
|
|
58
|
+
# @param local_subrs [Index, nil] Local subroutines INDEX (optional)
|
|
59
|
+
# @return [CharString, nil] Interpreted CharString object, or nil if
|
|
60
|
+
# index is out of bounds
|
|
61
|
+
#
|
|
62
|
+
# @example Getting a CharString
|
|
63
|
+
# charstring = charstrings_index.charstring_at(
|
|
64
|
+
# 42,
|
|
65
|
+
# private_dict,
|
|
66
|
+
# global_subrs,
|
|
67
|
+
# local_subrs
|
|
68
|
+
# )
|
|
69
|
+
# puts "Width: #{charstring.width}"
|
|
70
|
+
# puts "Bounding box: #{charstring.bounding_box.inspect}"
|
|
71
|
+
def charstring_at(index, private_dict, global_subrs, local_subrs = nil)
|
|
72
|
+
data = self[index]
|
|
73
|
+
return nil unless data
|
|
74
|
+
|
|
75
|
+
CharString.new(data, private_dict, global_subrs, local_subrs)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get all CharStrings as an array of CharString objects
|
|
79
|
+
#
|
|
80
|
+
# This method interprets all CharStrings in the INDEX. Use with
|
|
81
|
+
# caution for fonts with many glyphs as this can be memory-intensive.
|
|
82
|
+
#
|
|
83
|
+
# @param private_dict [PrivateDict] Private DICT for width defaults
|
|
84
|
+
# @param global_subrs [Index] Global subroutines INDEX
|
|
85
|
+
# @param local_subrs [Index, nil] Local subroutines INDEX (optional)
|
|
86
|
+
# @return [Array<CharString>] Array of interpreted CharString objects
|
|
87
|
+
#
|
|
88
|
+
# @example Getting all CharStrings
|
|
89
|
+
# charstrings = charstrings_index.all_charstrings(
|
|
90
|
+
# private_dict,
|
|
91
|
+
# global_subrs,
|
|
92
|
+
# local_subrs
|
|
93
|
+
# )
|
|
94
|
+
# charstrings.each_with_index do |cs, i|
|
|
95
|
+
# puts "Glyph #{i}: width=#{cs.width}, bbox=#{cs.bounding_box}"
|
|
96
|
+
# end
|
|
97
|
+
def all_charstrings(private_dict, global_subrs, local_subrs = nil)
|
|
98
|
+
Array.new(count) do |i|
|
|
99
|
+
charstring_at(i, private_dict, global_subrs, local_subrs)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Iterate over each CharString in the INDEX
|
|
104
|
+
#
|
|
105
|
+
# This method yields each CharString as it is interpreted, which is
|
|
106
|
+
# more memory-efficient than loading all at once.
|
|
107
|
+
#
|
|
108
|
+
# @param private_dict [PrivateDict] Private DICT for width defaults
|
|
109
|
+
# @param global_subrs [Index] Global subroutines INDEX
|
|
110
|
+
# @param local_subrs [Index, nil] Local subroutines INDEX (optional)
|
|
111
|
+
# @yield [CharString, Integer] Interpreted CharString and its index
|
|
112
|
+
# @return [Enumerator] If no block given
|
|
113
|
+
#
|
|
114
|
+
# @example Iterating over CharStrings
|
|
115
|
+
# charstrings_index.each_charstring(private_dict, global_subrs,
|
|
116
|
+
# local_subrs) do |cs, index|
|
|
117
|
+
# puts "Glyph #{index}: #{cs.bounding_box}"
|
|
118
|
+
# end
|
|
119
|
+
def each_charstring(private_dict, global_subrs, local_subrs = nil)
|
|
120
|
+
unless block_given?
|
|
121
|
+
return enum_for(:each_charstring, private_dict, global_subrs,
|
|
122
|
+
local_subrs)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
count.times do |i|
|
|
126
|
+
charstring = charstring_at(i, private_dict, global_subrs,
|
|
127
|
+
local_subrs)
|
|
128
|
+
yield charstring, i if charstring
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get the number of glyphs (CharStrings) in this INDEX
|
|
133
|
+
#
|
|
134
|
+
# This is typically the same as the number of glyphs in the font.
|
|
135
|
+
#
|
|
136
|
+
# @return [Integer] Number of glyphs
|
|
137
|
+
def glyph_count
|
|
138
|
+
count
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Check if a glyph index is valid
|
|
142
|
+
#
|
|
143
|
+
# @param index [Integer] Glyph index to check
|
|
144
|
+
# @return [Boolean] True if index is valid
|
|
145
|
+
def valid_glyph_index?(index)
|
|
146
|
+
index >= 0 && index < count
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get the size of a CharString in bytes
|
|
150
|
+
#
|
|
151
|
+
# This returns the size of the binary CharString data without
|
|
152
|
+
# interpreting it.
|
|
153
|
+
#
|
|
154
|
+
# @param index [Integer] Glyph index
|
|
155
|
+
# @return [Integer, nil] Size in bytes, or nil if index is invalid
|
|
156
|
+
def charstring_size(index)
|
|
157
|
+
item_size(index)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -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
|