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,354 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/hint"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Hints
|
|
7
|
+
# Extracts rendering hints from PostScript/CFF CharString data
|
|
8
|
+
#
|
|
9
|
+
# PostScript Type 1 and CFF fonts embed hints directly in the
|
|
10
|
+
# CharString data as operators. This extractor parses CharString
|
|
11
|
+
# sequences to identify and extract hint operators.
|
|
12
|
+
#
|
|
13
|
+
# **Supported PostScript Hint Operators:**
|
|
14
|
+
#
|
|
15
|
+
# - hstem/vstem - Horizontal/vertical stem hints
|
|
16
|
+
# - hstem3/vstem3 - Multiple stem hints
|
|
17
|
+
# - hintmask - Hint replacement masks
|
|
18
|
+
# - cntrmask - Counter control masks
|
|
19
|
+
#
|
|
20
|
+
# @example Extract hints from a CharString
|
|
21
|
+
# extractor = PostScriptHintExtractor.new
|
|
22
|
+
# hints = extractor.extract(charstring)
|
|
23
|
+
class PostScriptHintExtractor
|
|
24
|
+
# CFF CharString operators
|
|
25
|
+
HSTEM = 1
|
|
26
|
+
VSTEM = 3
|
|
27
|
+
HINTMASK = 19
|
|
28
|
+
CNTRMASK = 20
|
|
29
|
+
HSTEM3 = 12 << 8 | 2
|
|
30
|
+
VSTEM3 = 12 << 8 | 1
|
|
31
|
+
|
|
32
|
+
# Extract complete hint data from OpenType/CFF font
|
|
33
|
+
#
|
|
34
|
+
# This extracts both font-level hints (CFF Private dict) and
|
|
35
|
+
# per-glyph hints from CharStrings.
|
|
36
|
+
#
|
|
37
|
+
# @param font [OpenTypeFont] OpenType font with CFF table
|
|
38
|
+
# @return [Models::HintSet] Complete hint set
|
|
39
|
+
def extract_from_font(font)
|
|
40
|
+
hint_set = Models::HintSet.new(format: "postscript")
|
|
41
|
+
|
|
42
|
+
# Extract font-level Private dict hints
|
|
43
|
+
hint_set.private_dict_hints = extract_private_dict_hints(font).to_json
|
|
44
|
+
|
|
45
|
+
# Extract per-glyph CharString hints
|
|
46
|
+
extract_charstring_hints(font, hint_set)
|
|
47
|
+
|
|
48
|
+
# Update metadata
|
|
49
|
+
hint_set.has_hints = !hint_set.empty?
|
|
50
|
+
|
|
51
|
+
hint_set
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extract hints from CFF CharString
|
|
55
|
+
#
|
|
56
|
+
# @param charstring [CharString, String] CFF CharString object or bytes
|
|
57
|
+
# @return [Array<Hint>] Extracted hints
|
|
58
|
+
def extract(charstring)
|
|
59
|
+
return [] if charstring.nil?
|
|
60
|
+
|
|
61
|
+
# Get CharString bytes
|
|
62
|
+
bytes = if charstring.respond_to?(:data)
|
|
63
|
+
charstring.data
|
|
64
|
+
elsif charstring.respond_to?(:bytes)
|
|
65
|
+
charstring.bytes
|
|
66
|
+
elsif charstring.is_a?(String)
|
|
67
|
+
charstring.bytes
|
|
68
|
+
else
|
|
69
|
+
return []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return [] if bytes.empty?
|
|
73
|
+
|
|
74
|
+
parse_charstring(bytes)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Parse CharString bytes to extract hints
|
|
80
|
+
#
|
|
81
|
+
# @param bytes [Array<Integer>] CharString bytes
|
|
82
|
+
# @return [Array<Hint>] Extracted hints
|
|
83
|
+
def parse_charstring(bytes)
|
|
84
|
+
hints = []
|
|
85
|
+
stack = []
|
|
86
|
+
i = 0
|
|
87
|
+
|
|
88
|
+
while i < bytes.length
|
|
89
|
+
byte = bytes[i]
|
|
90
|
+
|
|
91
|
+
if operator?(byte)
|
|
92
|
+
# Process operator
|
|
93
|
+
operator = if byte == 12
|
|
94
|
+
# Two-byte operator
|
|
95
|
+
i += 1
|
|
96
|
+
(12 << 8) | bytes[i]
|
|
97
|
+
else
|
|
98
|
+
byte
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
hint = process_operator(operator, stack)
|
|
102
|
+
hints << hint if hint
|
|
103
|
+
|
|
104
|
+
# Clear stack after operator
|
|
105
|
+
stack.clear
|
|
106
|
+
i += 1
|
|
107
|
+
else
|
|
108
|
+
# Number - push to stack
|
|
109
|
+
num, consumed = decode_number(bytes, i)
|
|
110
|
+
stack << num if num
|
|
111
|
+
i += consumed
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
hints
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check if byte is an operator
|
|
119
|
+
#
|
|
120
|
+
# @param byte [Integer] Byte value
|
|
121
|
+
# @return [Boolean] True if operator
|
|
122
|
+
def operator?(byte)
|
|
123
|
+
byte <= 31 || byte == 255
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Decode a number from CharString
|
|
127
|
+
#
|
|
128
|
+
# @param bytes [Array<Integer>] CharString bytes
|
|
129
|
+
# @param index [Integer] Starting position
|
|
130
|
+
# @return [Array<Integer, Integer>] [number, bytes_consumed]
|
|
131
|
+
def decode_number(bytes, index)
|
|
132
|
+
byte = bytes[index]
|
|
133
|
+
return [nil, 1] if byte.nil?
|
|
134
|
+
|
|
135
|
+
case byte
|
|
136
|
+
when 28
|
|
137
|
+
# 3-byte signed integer
|
|
138
|
+
if index + 2 < bytes.length
|
|
139
|
+
num = (bytes[index + 1] << 8) | bytes[index + 2]
|
|
140
|
+
num = num - 65536 if num > 32767
|
|
141
|
+
[num, 3]
|
|
142
|
+
else
|
|
143
|
+
[nil, 1]
|
|
144
|
+
end
|
|
145
|
+
when 32..246
|
|
146
|
+
# Single byte integer
|
|
147
|
+
[byte - 139, 1]
|
|
148
|
+
when 247..250
|
|
149
|
+
# Positive 2-byte integer
|
|
150
|
+
if index + 1 < bytes.length
|
|
151
|
+
num = (byte - 247) * 256 + bytes[index + 1] + 108
|
|
152
|
+
[num, 2]
|
|
153
|
+
else
|
|
154
|
+
[nil, 1]
|
|
155
|
+
end
|
|
156
|
+
when 251..254
|
|
157
|
+
# Negative 2-byte integer
|
|
158
|
+
if index + 1 < bytes.length
|
|
159
|
+
num = -(byte - 251) * 256 - bytes[index + 1] - 108
|
|
160
|
+
[num, 2]
|
|
161
|
+
else
|
|
162
|
+
[nil, 1]
|
|
163
|
+
end
|
|
164
|
+
when 255
|
|
165
|
+
# 5-byte signed integer
|
|
166
|
+
if index + 4 < bytes.length
|
|
167
|
+
num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
|
|
168
|
+
(bytes[index + 3] << 8) | bytes[index + 4]
|
|
169
|
+
num = num - 4294967296 if num > 2147483647
|
|
170
|
+
[num, 5]
|
|
171
|
+
else
|
|
172
|
+
[nil, 1]
|
|
173
|
+
end
|
|
174
|
+
else
|
|
175
|
+
[nil, 1]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Process hint operator and create Hint object
|
|
180
|
+
#
|
|
181
|
+
# @param operator [Integer] Operator code
|
|
182
|
+
# @param stack [Array<Integer>] Current operand stack
|
|
183
|
+
# @return [Hint, nil] Hint object if operator is a hint
|
|
184
|
+
def process_operator(operator, stack)
|
|
185
|
+
case operator
|
|
186
|
+
when HSTEM
|
|
187
|
+
# Horizontal stem hint
|
|
188
|
+
extract_stem_hint(stack, :horizontal)
|
|
189
|
+
|
|
190
|
+
when VSTEM
|
|
191
|
+
# Vertical stem hint
|
|
192
|
+
extract_stem_hint(stack, :vertical)
|
|
193
|
+
|
|
194
|
+
when HSTEM3
|
|
195
|
+
# Multiple horizontal stems
|
|
196
|
+
extract_stem3_hint(stack, :horizontal)
|
|
197
|
+
|
|
198
|
+
when VSTEM3
|
|
199
|
+
# Multiple vertical stems
|
|
200
|
+
extract_stem3_hint(stack, :vertical)
|
|
201
|
+
|
|
202
|
+
when HINTMASK
|
|
203
|
+
# Hint replacement mask
|
|
204
|
+
Models::Hint.new(
|
|
205
|
+
type: :hint_replacement,
|
|
206
|
+
data: { mask: stack.dup },
|
|
207
|
+
source_format: :postscript
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
when CNTRMASK
|
|
211
|
+
# Counter control mask
|
|
212
|
+
Models::Hint.new(
|
|
213
|
+
type: :counter,
|
|
214
|
+
data: { zones: stack.dup },
|
|
215
|
+
source_format: :postscript
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
else
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Extract stem hint from stack
|
|
224
|
+
#
|
|
225
|
+
# @param stack [Array<Integer>] Operand stack
|
|
226
|
+
# @param orientation [Symbol] :horizontal or :vertical
|
|
227
|
+
# @return [Hint] Stem hint
|
|
228
|
+
def extract_stem_hint(stack, orientation)
|
|
229
|
+
# Stack should have pairs of [position, width]
|
|
230
|
+
return nil if stack.empty? || stack.length < 2
|
|
231
|
+
|
|
232
|
+
# Take first pair
|
|
233
|
+
position = stack[0]
|
|
234
|
+
width = stack[1]
|
|
235
|
+
|
|
236
|
+
Models::Hint.new(
|
|
237
|
+
type: :stem,
|
|
238
|
+
data: {
|
|
239
|
+
position: position,
|
|
240
|
+
width: width,
|
|
241
|
+
orientation: orientation
|
|
242
|
+
},
|
|
243
|
+
source_format: :postscript
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Extract stem3 hint from stack
|
|
248
|
+
#
|
|
249
|
+
# @param stack [Array<Integer>] Operand stack
|
|
250
|
+
# @param orientation [Symbol] :horizontal or :vertical
|
|
251
|
+
# @return [Hint] Stem3 hint
|
|
252
|
+
def extract_stem3_hint(stack, orientation)
|
|
253
|
+
# Stack should have 6 values: 3 pairs of [position, width]
|
|
254
|
+
return nil if stack.length < 6
|
|
255
|
+
|
|
256
|
+
stems = []
|
|
257
|
+
(0..2).each do |i|
|
|
258
|
+
pos_idx = i * 2
|
|
259
|
+
stems << {
|
|
260
|
+
position: stack[pos_idx],
|
|
261
|
+
width: stack[pos_idx + 1]
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
Models::Hint.new(
|
|
266
|
+
type: :stem3,
|
|
267
|
+
data: {
|
|
268
|
+
stems: stems,
|
|
269
|
+
orientation: orientation
|
|
270
|
+
},
|
|
271
|
+
source_format: :postscript
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Extract Private dict hints from CFF table
|
|
276
|
+
#
|
|
277
|
+
# Private dict contains font-level hint parameters like BlueValues,
|
|
278
|
+
# StdHW, StdVW, etc.
|
|
279
|
+
#
|
|
280
|
+
# @param font [OpenTypeFont] OpenType font
|
|
281
|
+
# @return [Hash] Private dict hint parameters
|
|
282
|
+
def extract_private_dict_hints(font)
|
|
283
|
+
hints = {}
|
|
284
|
+
|
|
285
|
+
return hints unless font.has_table?("CFF ")
|
|
286
|
+
|
|
287
|
+
cff_table = font.table("CFF ")
|
|
288
|
+
return hints unless cff_table
|
|
289
|
+
|
|
290
|
+
# Get Private DICT for first font (index 0)
|
|
291
|
+
private_dict = cff_table.private_dict(0)
|
|
292
|
+
return hints unless private_dict
|
|
293
|
+
|
|
294
|
+
# Extract hint-related parameters from Private DICT
|
|
295
|
+
# These are the key hinting parameters in CFF
|
|
296
|
+
hints[:blue_values] = private_dict.blue_values if private_dict.respond_to?(:blue_values)
|
|
297
|
+
hints[:other_blues] = private_dict.other_blues if private_dict.respond_to?(:other_blues)
|
|
298
|
+
hints[:family_blues] = private_dict.family_blues if private_dict.respond_to?(:family_blues)
|
|
299
|
+
hints[:family_other_blues] = private_dict.family_other_blues if private_dict.respond_to?(:family_other_blues)
|
|
300
|
+
hints[:blue_scale] = private_dict.blue_scale if private_dict.respond_to?(:blue_scale)
|
|
301
|
+
hints[:blue_shift] = private_dict.blue_shift if private_dict.respond_to?(:blue_shift)
|
|
302
|
+
hints[:blue_fuzz] = private_dict.blue_fuzz if private_dict.respond_to?(:blue_fuzz)
|
|
303
|
+
hints[:std_hw] = private_dict.std_hw if private_dict.respond_to?(:std_hw)
|
|
304
|
+
hints[:std_vw] = private_dict.std_vw if private_dict.respond_to?(:std_vw)
|
|
305
|
+
hints[:stem_snap_h] = private_dict.stem_snap_h if private_dict.respond_to?(:stem_snap_h)
|
|
306
|
+
hints[:stem_snap_v] = private_dict.stem_snap_v if private_dict.respond_to?(:stem_snap_v)
|
|
307
|
+
hints[:force_bold] = private_dict.force_bold if private_dict.respond_to?(:force_bold)
|
|
308
|
+
hints[:language_group] = private_dict.language_group if private_dict.respond_to?(:language_group)
|
|
309
|
+
|
|
310
|
+
hints.compact
|
|
311
|
+
rescue StandardError => e
|
|
312
|
+
warn "Failed to extract Private dict hints: #{e.message}"
|
|
313
|
+
{}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Extract per-glyph CharString hints from CFF table
|
|
317
|
+
#
|
|
318
|
+
# @param font [OpenTypeFont] OpenType font
|
|
319
|
+
# @param hint_set [Models::HintSet] Hint set to populate
|
|
320
|
+
# @return [void]
|
|
321
|
+
def extract_charstring_hints(font, hint_set)
|
|
322
|
+
return unless font.has_table?("CFF ")
|
|
323
|
+
|
|
324
|
+
cff_table = font.table("CFF ")
|
|
325
|
+
return unless cff_table
|
|
326
|
+
|
|
327
|
+
# Get CharStrings INDEX
|
|
328
|
+
charstrings_index = cff_table.charstrings_index(0)
|
|
329
|
+
return unless charstrings_index
|
|
330
|
+
|
|
331
|
+
# Iterate through all glyphs
|
|
332
|
+
glyph_count = cff_table.glyph_count(0)
|
|
333
|
+
(0...glyph_count).each do |glyph_id|
|
|
334
|
+
begin
|
|
335
|
+
# Get CharString for this glyph
|
|
336
|
+
charstring = cff_table.charstring_for_glyph(glyph_id, 0)
|
|
337
|
+
next unless charstring
|
|
338
|
+
|
|
339
|
+
# Extract hints from CharString
|
|
340
|
+
hints = extract(charstring)
|
|
341
|
+
next if hints.empty?
|
|
342
|
+
|
|
343
|
+
# Store glyph hints
|
|
344
|
+
hint_set.add_glyph_hints(glyph_id, hints)
|
|
345
|
+
rescue StandardError => e
|
|
346
|
+
warn "Failed to extract hints for glyph #{glyph_id}: #{e.message}"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
rescue StandardError => e
|
|
350
|
+
warn "Failed to extract CharString hints: #{e.message}"
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Hints
|
|
5
|
+
# Applies rendering hints to TrueType font tables
|
|
6
|
+
#
|
|
7
|
+
# This applier writes TrueType hint data into font-level tables:
|
|
8
|
+
# - fpgm (Font Program) - bytecode executed once at font initialization
|
|
9
|
+
# - prep (Control Value Program) - bytecode for glyph preparation
|
|
10
|
+
# - cvt (Control Values) - array of 16-bit values for hinting metrics
|
|
11
|
+
#
|
|
12
|
+
# The applier ensures proper table structure with correct checksums
|
|
13
|
+
# and does not corrupt the font if hint application fails.
|
|
14
|
+
#
|
|
15
|
+
# @example Apply hints from a HintSet
|
|
16
|
+
# applier = TrueTypeHintApplier.new
|
|
17
|
+
# tables = {}
|
|
18
|
+
# updated_tables = applier.apply(hint_set, tables)
|
|
19
|
+
class TrueTypeHintApplier
|
|
20
|
+
# Apply TrueType hints to font tables
|
|
21
|
+
#
|
|
22
|
+
# @param hint_set [HintSet] Hint data to apply
|
|
23
|
+
# @param tables [Hash] Font tables to update
|
|
24
|
+
# @return [Hash] Updated font tables
|
|
25
|
+
def apply(hint_set, tables)
|
|
26
|
+
return tables if hint_set.nil? || hint_set.empty?
|
|
27
|
+
return tables unless hint_set.format == "truetype"
|
|
28
|
+
|
|
29
|
+
# Write fpgm table if present
|
|
30
|
+
if hint_set.font_program && !hint_set.font_program.empty?
|
|
31
|
+
tables["fpgm"] = build_fpgm_table(hint_set.font_program)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Write prep table if present
|
|
35
|
+
if hint_set.control_value_program && !hint_set.control_value_program.empty?
|
|
36
|
+
tables["prep"] = build_prep_table(hint_set.control_value_program)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Write cvt table if present
|
|
40
|
+
if hint_set.control_values && !hint_set.control_values.empty?
|
|
41
|
+
tables["cvt "] = build_cvt_table(hint_set.control_values)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Future enhancement: Apply per-glyph hints to glyf table
|
|
45
|
+
# For now, font-level tables only
|
|
46
|
+
|
|
47
|
+
tables
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Build fpgm (Font Program) table
|
|
53
|
+
#
|
|
54
|
+
# @param program_data [String] Raw bytecode
|
|
55
|
+
# @return [Hash] Table structure with tag, data, and checksum
|
|
56
|
+
def build_fpgm_table(program_data)
|
|
57
|
+
{
|
|
58
|
+
tag: "fpgm",
|
|
59
|
+
data: program_data,
|
|
60
|
+
checksum: calculate_checksum(program_data),
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build prep (Control Value Program) table
|
|
65
|
+
#
|
|
66
|
+
# @param program_data [String] Raw bytecode
|
|
67
|
+
# @return [Hash] Table structure with tag, data, and checksum
|
|
68
|
+
def build_prep_table(program_data)
|
|
69
|
+
{
|
|
70
|
+
tag: "prep",
|
|
71
|
+
data: program_data,
|
|
72
|
+
checksum: calculate_checksum(program_data),
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Build cvt (Control Values) table
|
|
77
|
+
#
|
|
78
|
+
# CVT values are 16-bit signed integers (FWORD) in big-endian format.
|
|
79
|
+
# Each value represents a design-space coordinate used for hinting.
|
|
80
|
+
#
|
|
81
|
+
# @param control_values [Array<Integer>] Array of 16-bit signed values
|
|
82
|
+
# @return [Hash] Table structure with tag, data, and checksum
|
|
83
|
+
def build_cvt_table(control_values)
|
|
84
|
+
# Pack as 16-bit big-endian signed integers (s> = signed big-endian)
|
|
85
|
+
data = control_values.pack("s>*")
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
tag: "cvt ",
|
|
89
|
+
data: data,
|
|
90
|
+
checksum: calculate_checksum(data),
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Calculate OpenType table checksum
|
|
95
|
+
#
|
|
96
|
+
# OpenType spec requires tables to be checksummed as 32-bit unsigned
|
|
97
|
+
# integers in big-endian format. The table is padded to a multiple of
|
|
98
|
+
# 4 bytes with zeros before checksum calculation.
|
|
99
|
+
#
|
|
100
|
+
# @param data [String] Table data
|
|
101
|
+
# @return [Integer] 32-bit checksum
|
|
102
|
+
def calculate_checksum(data)
|
|
103
|
+
# Pad to 4-byte boundary with zeros
|
|
104
|
+
padding_needed = (4 - data.length % 4) % 4
|
|
105
|
+
padded = data + ("\x00" * padding_needed)
|
|
106
|
+
|
|
107
|
+
# Sum as 32-bit unsigned integers in big-endian
|
|
108
|
+
checksum = 0
|
|
109
|
+
(0...padded.length).step(4) do |i|
|
|
110
|
+
checksum = (checksum + padded[i, 4].unpack1("N")) & 0xFFFFFFFF
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
checksum
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|