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,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/hint"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Hints
|
|
7
|
+
# Extracts rendering hints from TrueType glyph data
|
|
8
|
+
#
|
|
9
|
+
# TrueType uses bytecode instructions for hinting. This extractor
|
|
10
|
+
# analyzes glyph instruction sequences and converts them into
|
|
11
|
+
# universal Hint objects for format-agnostic representation.
|
|
12
|
+
#
|
|
13
|
+
# **Supported TrueType Instructions:**
|
|
14
|
+
#
|
|
15
|
+
# - MDAP - Move Direct Absolute Point (stem positioning)
|
|
16
|
+
# - MDRP - Move Direct Relative Point (stem width)
|
|
17
|
+
# - IUP - Interpolate Untouched Points (smooth interpolation)
|
|
18
|
+
# - SHP - Shift Point (point adjustments)
|
|
19
|
+
# - ALIGNRP - Align to Reference Point (alignment)
|
|
20
|
+
# - DELTA - Delta instructions (pixel-level adjustments)
|
|
21
|
+
#
|
|
22
|
+
# @example Extract hints from a glyph
|
|
23
|
+
# extractor = TrueTypeHintExtractor.new
|
|
24
|
+
# hints = extractor.extract(glyph)
|
|
25
|
+
class TrueTypeHintExtractor
|
|
26
|
+
# TrueType instruction opcodes
|
|
27
|
+
MDAP_RND = 0x2E
|
|
28
|
+
MDAP_NORND = 0x2F
|
|
29
|
+
MDRP_MIN_RND_BLACK = 0xC0
|
|
30
|
+
IUP_Y = 0x30
|
|
31
|
+
IUP_X = 0x31
|
|
32
|
+
SHP = [0x32, 0x33]
|
|
33
|
+
ALIGNRP = 0x3C
|
|
34
|
+
DELTAP1 = 0x5D
|
|
35
|
+
DELTAP2 = 0x71
|
|
36
|
+
DELTAP3 = 0x72
|
|
37
|
+
|
|
38
|
+
# Extract complete hint data from TrueType font
|
|
39
|
+
#
|
|
40
|
+
# This extracts both font-level hints (fpgm, prep, cvt tables) and
|
|
41
|
+
# per-glyph hints from glyph instructions.
|
|
42
|
+
#
|
|
43
|
+
# @param font [TrueTypeFont] TrueType font object
|
|
44
|
+
# @return [Models::HintSet] Complete hint set
|
|
45
|
+
def extract_from_font(font)
|
|
46
|
+
hint_set = Models::HintSet.new(format: "truetype")
|
|
47
|
+
|
|
48
|
+
# Extract font-level programs
|
|
49
|
+
hint_set.font_program = extract_font_program(font)
|
|
50
|
+
hint_set.control_value_program = extract_control_value_program(font)
|
|
51
|
+
hint_set.control_values = extract_control_values(font)
|
|
52
|
+
|
|
53
|
+
# Extract per-glyph hints
|
|
54
|
+
extract_glyph_hints(font, hint_set)
|
|
55
|
+
|
|
56
|
+
# Update metadata
|
|
57
|
+
hint_set.has_hints = !hint_set.empty?
|
|
58
|
+
|
|
59
|
+
hint_set
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Extract hints from TrueType glyph
|
|
63
|
+
#
|
|
64
|
+
# @param glyph [Glyph] TrueType glyph with instructions
|
|
65
|
+
# @return [Array<Hint>] Extracted hints
|
|
66
|
+
def extract(glyph)
|
|
67
|
+
return [] if glyph.nil? || glyph.empty?
|
|
68
|
+
return [] unless glyph.respond_to?(:instructions)
|
|
69
|
+
|
|
70
|
+
instructions = glyph.instructions || []
|
|
71
|
+
return [] if instructions.empty?
|
|
72
|
+
|
|
73
|
+
parse_instructions(instructions)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Parse TrueType instruction bytes into Hint objects
|
|
79
|
+
#
|
|
80
|
+
# @param instructions [String, Array<Integer>] Instruction bytes
|
|
81
|
+
# @return [Array<Hint>] Parsed hints
|
|
82
|
+
def parse_instructions(instructions)
|
|
83
|
+
hints = []
|
|
84
|
+
bytes = instructions.is_a?(String) ? instructions.bytes : instructions
|
|
85
|
+
i = 0
|
|
86
|
+
|
|
87
|
+
while i < bytes.length
|
|
88
|
+
opcode = bytes[i]
|
|
89
|
+
|
|
90
|
+
case opcode
|
|
91
|
+
when MDAP_RND, MDAP_NORND
|
|
92
|
+
# Stem positioning hint
|
|
93
|
+
hint = extract_stem_hint(bytes, i)
|
|
94
|
+
hints << hint if hint
|
|
95
|
+
i += 1
|
|
96
|
+
|
|
97
|
+
when MDRP_MIN_RND_BLACK
|
|
98
|
+
# Stem width hint (usually follows MDAP)
|
|
99
|
+
# This is typically part of a stem hint pair
|
|
100
|
+
i += 1
|
|
101
|
+
|
|
102
|
+
when IUP_Y, IUP_X
|
|
103
|
+
# Interpolation hint
|
|
104
|
+
hints << Models::Hint.new(
|
|
105
|
+
type: :interpolate,
|
|
106
|
+
data: { axis: opcode == IUP_Y ? :y : :x },
|
|
107
|
+
source_format: :truetype
|
|
108
|
+
)
|
|
109
|
+
i += 1
|
|
110
|
+
|
|
111
|
+
when *SHP
|
|
112
|
+
# Shift point hint
|
|
113
|
+
hints << Models::Hint.new(
|
|
114
|
+
type: :shift,
|
|
115
|
+
data: { instructions: [opcode] },
|
|
116
|
+
source_format: :truetype
|
|
117
|
+
)
|
|
118
|
+
i += 1
|
|
119
|
+
|
|
120
|
+
when ALIGNRP
|
|
121
|
+
# Alignment hint
|
|
122
|
+
hints << Models::Hint.new(
|
|
123
|
+
type: :align,
|
|
124
|
+
data: {},
|
|
125
|
+
source_format: :truetype
|
|
126
|
+
)
|
|
127
|
+
i += 1
|
|
128
|
+
|
|
129
|
+
when DELTAP1, DELTAP2, DELTAP3
|
|
130
|
+
# Delta hint - pixel-level adjustments
|
|
131
|
+
# Next byte is the count
|
|
132
|
+
i += 1
|
|
133
|
+
if i < bytes.length
|
|
134
|
+
count = bytes[i]
|
|
135
|
+
delta_data = bytes[i + 1, count * 2] || []
|
|
136
|
+
hints << Models::Hint.new(
|
|
137
|
+
type: :delta,
|
|
138
|
+
data: {
|
|
139
|
+
instructions: [opcode] + [count] + delta_data,
|
|
140
|
+
count: count
|
|
141
|
+
},
|
|
142
|
+
source_format: :truetype
|
|
143
|
+
)
|
|
144
|
+
i += count * 2 + 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
else
|
|
148
|
+
# Unknown or data bytes - skip
|
|
149
|
+
i += 1
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
hints
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Extract stem hint from MDAP instruction
|
|
157
|
+
#
|
|
158
|
+
# @param bytes [Array<Integer>] Instruction bytes
|
|
159
|
+
# @param index [Integer] Current position
|
|
160
|
+
# @return [Hint, nil] Stem hint if found
|
|
161
|
+
def extract_stem_hint(bytes, index)
|
|
162
|
+
# In TrueType, stem hints are inferred from MDAP + MDRP pairs
|
|
163
|
+
# This is a simplified extraction - real implementation would
|
|
164
|
+
# need to track the graphics state and point references
|
|
165
|
+
|
|
166
|
+
# Check if next instruction is MDRP (stem width)
|
|
167
|
+
has_width = index + 1 < bytes.length &&
|
|
168
|
+
bytes[index + 1] == MDRP_MIN_RND_BLACK
|
|
169
|
+
|
|
170
|
+
if has_width
|
|
171
|
+
Models::Hint.new(
|
|
172
|
+
type: :stem,
|
|
173
|
+
data: {
|
|
174
|
+
position: 0, # Would be extracted from graphics state
|
|
175
|
+
width: 0, # Would be calculated from MDRP
|
|
176
|
+
orientation: :vertical # Inferred from instruction context
|
|
177
|
+
},
|
|
178
|
+
source_format: :truetype
|
|
179
|
+
)
|
|
180
|
+
else
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Extract font program (fpgm table)
|
|
186
|
+
#
|
|
187
|
+
# @param font [TrueTypeFont] TrueType font
|
|
188
|
+
# @return [String] Font program bytecode (binary string)
|
|
189
|
+
def extract_font_program(font)
|
|
190
|
+
return "" unless font.has_table?("fpgm")
|
|
191
|
+
|
|
192
|
+
font_program_data = font.instance_variable_get(:@table_data)["fpgm"]
|
|
193
|
+
return "" unless font_program_data
|
|
194
|
+
|
|
195
|
+
# Return as binary string
|
|
196
|
+
font_program_data.force_encoding("ASCII-8BIT")
|
|
197
|
+
rescue StandardError => e
|
|
198
|
+
warn "Failed to extract font program: #{e.message}"
|
|
199
|
+
""
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Extract control value program (prep table)
|
|
203
|
+
#
|
|
204
|
+
# @param font [TrueTypeFont] TrueType font
|
|
205
|
+
# @return [String] Control value program bytecode (binary string)
|
|
206
|
+
def extract_control_value_program(font)
|
|
207
|
+
return "" unless font.has_table?("prep")
|
|
208
|
+
|
|
209
|
+
prep_data = font.instance_variable_get(:@table_data)["prep"]
|
|
210
|
+
return "" unless prep_data
|
|
211
|
+
|
|
212
|
+
# Return as binary string
|
|
213
|
+
prep_data.force_encoding("ASCII-8BIT")
|
|
214
|
+
rescue StandardError => e
|
|
215
|
+
warn "Failed to extract control value program: #{e.message}"
|
|
216
|
+
""
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Extract control values (cvt table)
|
|
220
|
+
#
|
|
221
|
+
# @param font [TrueTypeFont] TrueType font
|
|
222
|
+
# @return [Array<Integer>] Control values
|
|
223
|
+
def extract_control_values(font)
|
|
224
|
+
return [] unless font.has_table?("cvt ")
|
|
225
|
+
|
|
226
|
+
cvt_data = font.instance_variable_get(:@table_data)["cvt "]
|
|
227
|
+
return [] unless cvt_data
|
|
228
|
+
|
|
229
|
+
# CVT table is an array of 16-bit signed integers (FWord values)
|
|
230
|
+
values = []
|
|
231
|
+
io = StringIO.new(cvt_data)
|
|
232
|
+
while !io.eof?
|
|
233
|
+
# Read 16-bit big-endian signed integer
|
|
234
|
+
bytes = io.read(2)
|
|
235
|
+
break unless bytes&.length == 2
|
|
236
|
+
|
|
237
|
+
value = bytes.unpack1("n") # Unsigned short
|
|
238
|
+
# Convert to signed
|
|
239
|
+
value = value - 65536 if value > 32767
|
|
240
|
+
values << value
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
values
|
|
244
|
+
rescue StandardError => e
|
|
245
|
+
warn "Failed to extract control values: #{e.message}"
|
|
246
|
+
[]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Extract per-glyph hints from glyf table
|
|
250
|
+
#
|
|
251
|
+
# @param font [TrueTypeFont] TrueType font
|
|
252
|
+
# @param hint_set [Models::HintSet] Hint set to populate
|
|
253
|
+
# @return [void]
|
|
254
|
+
def extract_glyph_hints(font, hint_set)
|
|
255
|
+
return unless font.has_table?("glyf")
|
|
256
|
+
|
|
257
|
+
glyf_table = font.table("glyf")
|
|
258
|
+
return unless glyf_table
|
|
259
|
+
|
|
260
|
+
# Get number of glyphs from maxp table
|
|
261
|
+
maxp_table = font.table("maxp")
|
|
262
|
+
return unless maxp_table
|
|
263
|
+
|
|
264
|
+
num_glyphs = maxp_table.num_glyphs
|
|
265
|
+
|
|
266
|
+
# Iterate through all glyphs
|
|
267
|
+
(0...num_glyphs).each do |glyph_id|
|
|
268
|
+
begin
|
|
269
|
+
glyph = glyf_table.glyph_for(glyph_id)
|
|
270
|
+
next unless glyph
|
|
271
|
+
next if glyph.number_of_contours <= 0 # Skip compound glyphs and empty glyphs
|
|
272
|
+
|
|
273
|
+
# Extract hints from simple glyph instructions
|
|
274
|
+
hints = extract(glyph)
|
|
275
|
+
next if hints.empty?
|
|
276
|
+
|
|
277
|
+
# Store glyph hints
|
|
278
|
+
hint_set.add_glyph_hints(glyph_id, hints)
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
# Skip glyphs that fail to parse
|
|
281
|
+
next
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
rescue StandardError => e
|
|
285
|
+
warn "Failed to extract glyph hints: #{e.message}"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
# Loading modes module that defines which tables are loaded in each mode.
|
|
7
|
+
#
|
|
8
|
+
# This module provides a MECE (Mutually Exclusive, Collectively Exhaustive)
|
|
9
|
+
# architecture for font loading modes. Each mode defines a specific set of
|
|
10
|
+
# tables to load, enabling efficient parsing for different use cases.
|
|
11
|
+
#
|
|
12
|
+
# @example Using metadata mode
|
|
13
|
+
# mode = LoadingModes::METADATA
|
|
14
|
+
# tables = LoadingModes.tables_for(mode) # => ["name", "head", "hhea", "maxp", "OS/2", "post"]
|
|
15
|
+
#
|
|
16
|
+
# @example Checking table availability
|
|
17
|
+
# LoadingModes.table_allowed?(:metadata, "GSUB") # => false
|
|
18
|
+
# LoadingModes.table_allowed?(:full, "GSUB") # => true
|
|
19
|
+
module LoadingModes
|
|
20
|
+
# Metadata mode: loads only tables needed for font identification and metrics
|
|
21
|
+
# Equivalent to otfinfo functionality
|
|
22
|
+
METADATA = :metadata
|
|
23
|
+
|
|
24
|
+
# Full mode: loads all tables in the font
|
|
25
|
+
FULL = :full
|
|
26
|
+
|
|
27
|
+
# Mode definitions with their respective table lists
|
|
28
|
+
MODES = {
|
|
29
|
+
METADATA => {
|
|
30
|
+
tables: %w[name head hhea maxp OS/2 post].freeze,
|
|
31
|
+
description: "Metadata mode - loads only identification and metrics tables (otfinfo-equivalent)"
|
|
32
|
+
}.freeze,
|
|
33
|
+
FULL => {
|
|
34
|
+
tables: :all,
|
|
35
|
+
description: "Full mode - loads all tables in the font"
|
|
36
|
+
}.freeze
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# Pre-computed Set for O(1) lookup of metadata tables
|
|
40
|
+
# This constant avoids recreating the Set on every font load
|
|
41
|
+
METADATA_TABLES_SET = MODES[METADATA][:tables].to_set.freeze
|
|
42
|
+
|
|
43
|
+
# Get the list of tables allowed for a given mode
|
|
44
|
+
#
|
|
45
|
+
# @param mode [Symbol] The loading mode (:metadata or :full)
|
|
46
|
+
# @return [Array<String>, Symbol] Array of table tags or :all for full mode
|
|
47
|
+
# @raise [ArgumentError] if mode is invalid
|
|
48
|
+
def self.tables_for(mode)
|
|
49
|
+
validate_mode!(mode)
|
|
50
|
+
MODES[mode][:tables]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if a table is allowed in a given mode
|
|
54
|
+
#
|
|
55
|
+
# @param mode [Symbol] The loading mode (:metadata or :full)
|
|
56
|
+
# @param tag [String] The table tag to check
|
|
57
|
+
# @return [Boolean] true if table is allowed in the mode
|
|
58
|
+
# @raise [ArgumentError] if mode is invalid
|
|
59
|
+
def self.table_allowed?(mode, tag)
|
|
60
|
+
validate_mode!(mode)
|
|
61
|
+
|
|
62
|
+
tables = MODES[mode][:tables]
|
|
63
|
+
return true if tables == :all
|
|
64
|
+
|
|
65
|
+
tables.include?(tag)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Validate that a mode is valid
|
|
69
|
+
#
|
|
70
|
+
# @param mode [Symbol] The mode to validate
|
|
71
|
+
# @return [Boolean] true if mode is valid
|
|
72
|
+
def self.valid_mode?(mode)
|
|
73
|
+
MODES.key?(mode)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the default lazy loading setting for a mode
|
|
77
|
+
#
|
|
78
|
+
# @param mode [Symbol] The loading mode
|
|
79
|
+
# @return [Boolean] true if lazy loading is recommended for this mode
|
|
80
|
+
# @raise [ArgumentError] if mode is invalid
|
|
81
|
+
def self.default_lazy?(mode)
|
|
82
|
+
validate_mode!(mode)
|
|
83
|
+
true # Lazy loading is recommended for all modes
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get mode description
|
|
87
|
+
#
|
|
88
|
+
# @param mode [Symbol] The loading mode
|
|
89
|
+
# @return [String] Description of the mode
|
|
90
|
+
# @raise [ArgumentError] if mode is invalid
|
|
91
|
+
def self.description(mode)
|
|
92
|
+
validate_mode!(mode)
|
|
93
|
+
MODES[mode][:description]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get all available modes
|
|
97
|
+
#
|
|
98
|
+
# @return [Array<Symbol>] List of all mode symbols
|
|
99
|
+
def self.all_modes
|
|
100
|
+
MODES.keys
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Validate mode and raise error if invalid
|
|
104
|
+
#
|
|
105
|
+
# @param mode [Symbol] The mode to validate
|
|
106
|
+
# @return [void]
|
|
107
|
+
# @raise [ArgumentError] if mode is invalid
|
|
108
|
+
def self.validate_mode!(mode)
|
|
109
|
+
return if valid_mode?(mode)
|
|
110
|
+
|
|
111
|
+
raise ArgumentError,
|
|
112
|
+
"Invalid mode: #{mode.inspect}. Valid modes are: #{all_modes.map(&:inspect).join(', ')}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
# High-level utility class for accessing font metrics
|
|
7
|
+
#
|
|
8
|
+
# MetricsCalculator provides a convenient API for querying font metrics from
|
|
9
|
+
# multiple OpenType tables without needing to work with the low-level table
|
|
10
|
+
# structures directly. It wraps access to hhea, hmtx, head, maxp, and cmap
|
|
11
|
+
# tables.
|
|
12
|
+
#
|
|
13
|
+
# The calculator handles missing tables gracefully and provides both
|
|
14
|
+
# individual glyph metrics and string-level calculations.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# font = FontLoader.from_file("path/to/font.ttf")
|
|
18
|
+
# calc = MetricsCalculator.new(font)
|
|
19
|
+
#
|
|
20
|
+
# puts calc.ascent # => 2048
|
|
21
|
+
# puts calc.descent # => -512
|
|
22
|
+
# puts calc.line_height # => 2650
|
|
23
|
+
# puts calc.units_per_em # => 2048
|
|
24
|
+
#
|
|
25
|
+
# @example Glyph metrics
|
|
26
|
+
# width = calc.glyph_width(42)
|
|
27
|
+
# lsb = calc.glyph_left_side_bearing(42)
|
|
28
|
+
#
|
|
29
|
+
# @example String width calculation
|
|
30
|
+
# width = calc.string_width("Hello")
|
|
31
|
+
#
|
|
32
|
+
# @example Checking for metrics support
|
|
33
|
+
# if calc.has_metrics?
|
|
34
|
+
# puts "Font has complete horizontal metrics"
|
|
35
|
+
# end
|
|
36
|
+
class MetricsCalculator
|
|
37
|
+
# The font object this calculator operates on
|
|
38
|
+
#
|
|
39
|
+
# @return [OpenTypeFont, TrueTypeFont] The font instance
|
|
40
|
+
attr_reader :font
|
|
41
|
+
|
|
42
|
+
# Initialize a new MetricsCalculator
|
|
43
|
+
#
|
|
44
|
+
# @param font [OpenTypeFont, TrueTypeFont] Font instance to calculate metrics for
|
|
45
|
+
# @raise [ArgumentError] if font is nil
|
|
46
|
+
def initialize(font)
|
|
47
|
+
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
48
|
+
|
|
49
|
+
@font = font
|
|
50
|
+
@hhea_table = nil
|
|
51
|
+
@hmtx_table = nil
|
|
52
|
+
@head_table = nil
|
|
53
|
+
@maxp_table = nil
|
|
54
|
+
@cmap_table = nil
|
|
55
|
+
@hmtx_parsed = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get typographic ascent from hhea table
|
|
59
|
+
#
|
|
60
|
+
# The ascent is the distance from the baseline to the highest ascender.
|
|
61
|
+
# It is a positive value in font units (FUnits).
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer, nil] Ascent value in FUnits, or nil if hhea table is missing
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# calc.ascent # => 2048
|
|
67
|
+
def ascent
|
|
68
|
+
hhea&.ascent
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get typographic descent from hhea table
|
|
72
|
+
#
|
|
73
|
+
# The descent is the distance from the baseline to the lowest descender.
|
|
74
|
+
# It is typically a negative value in font units (FUnits).
|
|
75
|
+
#
|
|
76
|
+
# @return [Integer, nil] Descent value in FUnits, or nil if hhea table is missing
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# calc.descent # => -512
|
|
80
|
+
def descent
|
|
81
|
+
hhea&.descent
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get line gap from hhea table
|
|
85
|
+
#
|
|
86
|
+
# The line gap is additional vertical space between lines of text.
|
|
87
|
+
# It is a non-negative value in font units (FUnits).
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer, nil] Line gap value in FUnits, or nil if hhea table is missing
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# calc.line_gap # => 90
|
|
93
|
+
def line_gap
|
|
94
|
+
hhea&.line_gap
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get units per em from head table
|
|
98
|
+
#
|
|
99
|
+
# This value defines the font's coordinate system scale. Common values
|
|
100
|
+
# are 1000 (PostScript fonts) or 2048 (TrueType fonts).
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer, nil] Units per em value, or nil if head table is missing
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# calc.units_per_em # => 2048
|
|
106
|
+
def units_per_em
|
|
107
|
+
head&.units_per_em
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get advance width for a specific glyph
|
|
111
|
+
#
|
|
112
|
+
# The advance width is the horizontal distance to advance the pen position
|
|
113
|
+
# after rendering this glyph. It is in font units (FUnits).
|
|
114
|
+
#
|
|
115
|
+
# @param glyph_id [Integer] The glyph ID (0-based)
|
|
116
|
+
# @return [Integer, nil] Advance width in FUnits, or nil if not available
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
# calc.glyph_width(42) # => 1234
|
|
120
|
+
def glyph_width(glyph_id)
|
|
121
|
+
ensure_hmtx_parsed
|
|
122
|
+
return nil unless hmtx
|
|
123
|
+
|
|
124
|
+
metric = hmtx.metric_for(glyph_id)
|
|
125
|
+
metric&.dig(:advance_width)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Alias for {#glyph_width}
|
|
129
|
+
#
|
|
130
|
+
# @param glyph_id [Integer] The glyph ID (0-based)
|
|
131
|
+
# @return [Integer, nil] Advance width in FUnits, or nil if not available
|
|
132
|
+
alias glyph_advance_width glyph_width
|
|
133
|
+
|
|
134
|
+
# Get left side bearing for a specific glyph
|
|
135
|
+
#
|
|
136
|
+
# The left side bearing (LSB) is the horizontal distance from the pen
|
|
137
|
+
# position to the leftmost point of the glyph. It can be negative if
|
|
138
|
+
# the glyph extends to the left of the pen position.
|
|
139
|
+
#
|
|
140
|
+
# @param glyph_id [Integer] The glyph ID (0-based)
|
|
141
|
+
# @return [Integer, nil] Left side bearing in FUnits, or nil if not available
|
|
142
|
+
#
|
|
143
|
+
# @example
|
|
144
|
+
# calc.glyph_left_side_bearing(42) # => 50
|
|
145
|
+
def glyph_left_side_bearing(glyph_id)
|
|
146
|
+
ensure_hmtx_parsed
|
|
147
|
+
return nil unless hmtx
|
|
148
|
+
|
|
149
|
+
metric = hmtx.metric_for(glyph_id)
|
|
150
|
+
metric&.dig(:lsb)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Calculate total width for a string
|
|
154
|
+
#
|
|
155
|
+
# Calculates the sum of advance widths for all characters in the string.
|
|
156
|
+
# This is a simplified calculation that does not account for kerning,
|
|
157
|
+
# ligatures, or other advanced typography features.
|
|
158
|
+
#
|
|
159
|
+
# Characters not mapped in the font are skipped.
|
|
160
|
+
#
|
|
161
|
+
# @param string [String] The string to measure
|
|
162
|
+
# @return [Integer, nil] Total width in FUnits, or nil if metrics unavailable
|
|
163
|
+
#
|
|
164
|
+
# @example
|
|
165
|
+
# calc.string_width("Hello") # => 5420
|
|
166
|
+
def string_width(string)
|
|
167
|
+
return nil unless has_metrics?
|
|
168
|
+
return 0 if string.nil? || string.empty?
|
|
169
|
+
|
|
170
|
+
total_width = 0
|
|
171
|
+
string.each_codepoint do |codepoint|
|
|
172
|
+
glyph_id = codepoint_to_glyph_id(codepoint)
|
|
173
|
+
next unless glyph_id
|
|
174
|
+
|
|
175
|
+
width = glyph_width(glyph_id)
|
|
176
|
+
total_width += width if width
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
total_width
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Calculate line height
|
|
183
|
+
#
|
|
184
|
+
# Line height is calculated as: ascent - descent + line_gap
|
|
185
|
+
# This represents the recommended spacing between consecutive baselines.
|
|
186
|
+
#
|
|
187
|
+
# @return [Integer, nil] Line height in FUnits, or nil if hhea table is missing
|
|
188
|
+
#
|
|
189
|
+
# @example
|
|
190
|
+
# calc.line_height # => 2650 (when ascent=2048, descent=-512, line_gap=90)
|
|
191
|
+
def line_height
|
|
192
|
+
return nil unless hhea
|
|
193
|
+
|
|
194
|
+
ascent - descent + line_gap
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Alias for {#units_per_em}
|
|
198
|
+
#
|
|
199
|
+
# @return [Integer, nil] Units per em value, or nil if head table is missing
|
|
200
|
+
alias em_height units_per_em
|
|
201
|
+
|
|
202
|
+
# Check if font has complete horizontal metrics
|
|
203
|
+
#
|
|
204
|
+
# Returns true if the font has all required tables for horizontal metrics:
|
|
205
|
+
# hhea, hmtx, head, and maxp tables.
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean] True if all metrics tables are present
|
|
208
|
+
#
|
|
209
|
+
# @example
|
|
210
|
+
# calc.has_metrics? # => true
|
|
211
|
+
def has_metrics?
|
|
212
|
+
!hhea.nil? && !hmtx.nil? && !head.nil? && !maxp.nil?
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
# Get hhea table, caching the result
|
|
218
|
+
#
|
|
219
|
+
# @return [Tables::Hhea, nil] The hhea table or nil
|
|
220
|
+
def hhea
|
|
221
|
+
@hhea ||= font.table(Constants::HHEA_TAG)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Get hmtx table, caching the result
|
|
225
|
+
#
|
|
226
|
+
# @return [Tables::Hmtx, nil] The hmtx table or nil
|
|
227
|
+
def hmtx
|
|
228
|
+
@hmtx ||= font.table(Constants::HMTX_TAG)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Get head table, caching the result
|
|
232
|
+
#
|
|
233
|
+
# @return [Tables::Head, nil] The head table or nil
|
|
234
|
+
def head
|
|
235
|
+
@head ||= font.table(Constants::HEAD_TAG)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get maxp table, caching the result
|
|
239
|
+
#
|
|
240
|
+
# @return [Tables::Maxp, nil] The maxp table or nil
|
|
241
|
+
def maxp
|
|
242
|
+
@maxp ||= font.table(Constants::MAXP_TAG)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Get cmap table, caching the result
|
|
246
|
+
#
|
|
247
|
+
# @return [Tables::Cmap, nil] The cmap table or nil
|
|
248
|
+
def cmap
|
|
249
|
+
@cmap ||= font.table(Constants::CMAP_TAG)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Ensure hmtx table is parsed with context
|
|
253
|
+
#
|
|
254
|
+
# The hmtx table requires numberOfHMetrics from hhea and numGlyphs from maxp
|
|
255
|
+
# to be parsed correctly. This method ensures parsing happens lazily on first use.
|
|
256
|
+
#
|
|
257
|
+
# @return [void]
|
|
258
|
+
def ensure_hmtx_parsed
|
|
259
|
+
return if @hmtx_parsed
|
|
260
|
+
return unless hmtx && hhea && maxp
|
|
261
|
+
|
|
262
|
+
hmtx.parse_with_context(hhea.number_of_h_metrics, maxp.num_glyphs)
|
|
263
|
+
@hmtx_parsed = true
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Map Unicode codepoint to glyph ID using cmap table
|
|
267
|
+
#
|
|
268
|
+
# @param codepoint [Integer] Unicode codepoint
|
|
269
|
+
# @return [Integer, nil] Glyph ID or nil if not mapped
|
|
270
|
+
def codepoint_to_glyph_id(codepoint)
|
|
271
|
+
return nil unless cmap
|
|
272
|
+
|
|
273
|
+
mappings = cmap.unicode_mappings
|
|
274
|
+
mappings[codepoint]
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|