fontisan 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/hint"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Hints
|
|
7
|
+
# Applies rendering hints to PostScript/CFF CharString data
|
|
8
|
+
#
|
|
9
|
+
# This applier converts universal Hint objects into PostScript hint
|
|
10
|
+
# operators and integrates them into CharString data. It ensures proper
|
|
11
|
+
# operator placement and maintains CharString validity.
|
|
12
|
+
#
|
|
13
|
+
# **PostScript Hint Placement:**
|
|
14
|
+
#
|
|
15
|
+
# - Stem hints (hstem/vstem) must appear at the beginning
|
|
16
|
+
# - Hintmask operators can appear throughout the CharString
|
|
17
|
+
# - Hints affect all subsequent path operations
|
|
18
|
+
#
|
|
19
|
+
# @example Apply hints to a CharString
|
|
20
|
+
# applier = PostScriptHintApplier.new
|
|
21
|
+
# charstring_with_hints = applier.apply(charstring, hints)
|
|
22
|
+
class PostScriptHintApplier
|
|
23
|
+
# CFF CharString operators
|
|
24
|
+
HSTEM = 1
|
|
25
|
+
VSTEM = 3
|
|
26
|
+
HINTMASK = 19
|
|
27
|
+
CNTRMASK = 20
|
|
28
|
+
HSTEM3 = [12, 2]
|
|
29
|
+
VSTEM3 = [12, 1]
|
|
30
|
+
|
|
31
|
+
# Apply hints to CharString
|
|
32
|
+
#
|
|
33
|
+
# @param charstring [String] Original CharString bytes
|
|
34
|
+
# @param hints [Array<Hint>] Hints to apply
|
|
35
|
+
# @return [String] CharString with applied hints
|
|
36
|
+
def apply(charstring, hints)
|
|
37
|
+
return charstring if hints.nil? || hints.empty?
|
|
38
|
+
return charstring if charstring.nil? || charstring.empty?
|
|
39
|
+
|
|
40
|
+
# Build hint operators
|
|
41
|
+
hint_ops = build_hint_operators(hints)
|
|
42
|
+
|
|
43
|
+
# Insert hints at the beginning of CharString
|
|
44
|
+
# (simplified - real implementation would analyze existing structure)
|
|
45
|
+
hint_ops + charstring
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Build hint operators from hints
|
|
51
|
+
#
|
|
52
|
+
# @param hints [Array<Hint>] Hints to convert
|
|
53
|
+
# @return [String] Hint operator bytes
|
|
54
|
+
def build_hint_operators(hints)
|
|
55
|
+
operators = "".b
|
|
56
|
+
|
|
57
|
+
# Group hints by type for proper ordering
|
|
58
|
+
stem_hints = hints.select { |h| h.type == :stem }
|
|
59
|
+
stem3_hints = hints.select { |h| h.type == :stem3 }
|
|
60
|
+
mask_hints = hints.select { |h| %i[hint_replacement counter].include?(h.type) }
|
|
61
|
+
|
|
62
|
+
# Add stem hints first
|
|
63
|
+
stem_hints.each do |hint|
|
|
64
|
+
operators << encode_stem_hint(hint)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Add stem3 hints
|
|
68
|
+
stem3_hints.each do |hint|
|
|
69
|
+
operators << encode_stem3_hint(hint)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Add mask hints
|
|
73
|
+
mask_hints.each do |hint|
|
|
74
|
+
operators << encode_mask_hint(hint)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
operators
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Encode stem hint as CharString bytes
|
|
81
|
+
#
|
|
82
|
+
# @param hint [Hint] Stem hint
|
|
83
|
+
# @return [String] Encoded bytes
|
|
84
|
+
def encode_stem_hint(hint)
|
|
85
|
+
data = hint.to_postscript
|
|
86
|
+
return "".b if data.empty?
|
|
87
|
+
|
|
88
|
+
args = data[:args] || []
|
|
89
|
+
operator = data[:operator]
|
|
90
|
+
|
|
91
|
+
# Encode arguments as CFF integers
|
|
92
|
+
bytes = args.map { |arg| encode_cff_integer(arg) }.join
|
|
93
|
+
|
|
94
|
+
# Add operator
|
|
95
|
+
bytes << if operator == :vstem
|
|
96
|
+
[VSTEM].pack("C")
|
|
97
|
+
else
|
|
98
|
+
[HSTEM].pack("C")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
bytes
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Encode stem3 hint as CharString bytes
|
|
105
|
+
#
|
|
106
|
+
# @param hint [Hint] Stem3 hint
|
|
107
|
+
# @return [String] Encoded bytes
|
|
108
|
+
def encode_stem3_hint(hint)
|
|
109
|
+
data = hint.to_postscript
|
|
110
|
+
return "".b if data.empty?
|
|
111
|
+
|
|
112
|
+
args = data[:args] || []
|
|
113
|
+
operator = data[:operator]
|
|
114
|
+
|
|
115
|
+
# Encode arguments
|
|
116
|
+
bytes = args.map { |arg| encode_cff_integer(arg) }.join
|
|
117
|
+
|
|
118
|
+
# Add two-byte operator (12 followed by subop)
|
|
119
|
+
bytes << if operator == :vstem3
|
|
120
|
+
VSTEM3.pack("C*")
|
|
121
|
+
else
|
|
122
|
+
HSTEM3.pack("C*")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
bytes
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Encode mask hint as CharString bytes
|
|
129
|
+
#
|
|
130
|
+
# @param hint [Hint] Mask hint
|
|
131
|
+
# @return [String] Encoded bytes
|
|
132
|
+
def encode_mask_hint(hint)
|
|
133
|
+
operator = hint.type == :hint_replacement ? HINTMASK : CNTRMASK
|
|
134
|
+
mask = hint.data[:mask] || []
|
|
135
|
+
|
|
136
|
+
# Encode mask bytes
|
|
137
|
+
bytes = mask.map { |b| [b].pack("C") }.join
|
|
138
|
+
|
|
139
|
+
# Add operator
|
|
140
|
+
bytes + [operator].pack("C")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Encode integer as CFF CharString number
|
|
144
|
+
#
|
|
145
|
+
# @param num [Integer] Number to encode
|
|
146
|
+
# @return [String] Encoded bytes
|
|
147
|
+
def encode_cff_integer(num)
|
|
148
|
+
# Range 1: -107 to 107 (single byte)
|
|
149
|
+
if num >= -107 && num <= 107
|
|
150
|
+
return [32 + num].pack("c")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Range 2: 108 to 1131 (two bytes)
|
|
154
|
+
if num >= 108 && num <= 1131
|
|
155
|
+
b0 = 247 + ((num - 108) >> 8)
|
|
156
|
+
b1 = (num - 108) & 0xff
|
|
157
|
+
return [b0, b1].pack("C*")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Range 3: -1131 to -108 (two bytes)
|
|
161
|
+
if num >= -1131 && num <= -108
|
|
162
|
+
b0 = 251 - ((num + 108) >> 8)
|
|
163
|
+
b1 = -(num + 108) & 0xff
|
|
164
|
+
return [b0, b1].pack("C*")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Range 4: -32768 to 32767 (three bytes)
|
|
168
|
+
if num >= -32_768 && num <= 32_767
|
|
169
|
+
bytes = [28, (num >> 8) & 0xff, num & 0xff]
|
|
170
|
+
return bytes.pack("C*")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Range 5: Larger numbers (five bytes)
|
|
174
|
+
bytes = [
|
|
175
|
+
255,
|
|
176
|
+
(num >> 24) & 0xff,
|
|
177
|
+
(num >> 16) & 0xff,
|
|
178
|
+
(num >> 8) & 0xff,
|
|
179
|
+
num & 0xff
|
|
180
|
+
]
|
|
181
|
+
bytes.pack("C*")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,254 @@
|
|
|
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 hints from CFF CharString
|
|
33
|
+
#
|
|
34
|
+
# @param charstring [CharString, String] CFF CharString object or bytes
|
|
35
|
+
# @return [Array<Hint>] Extracted hints
|
|
36
|
+
def extract(charstring)
|
|
37
|
+
return [] if charstring.nil?
|
|
38
|
+
|
|
39
|
+
# Get CharString bytes
|
|
40
|
+
bytes = if charstring.respond_to?(:data)
|
|
41
|
+
charstring.data
|
|
42
|
+
elsif charstring.respond_to?(:bytes)
|
|
43
|
+
charstring.bytes
|
|
44
|
+
elsif charstring.is_a?(String)
|
|
45
|
+
charstring.bytes
|
|
46
|
+
else
|
|
47
|
+
return []
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return [] if bytes.empty?
|
|
51
|
+
|
|
52
|
+
parse_charstring(bytes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Parse CharString bytes to extract hints
|
|
58
|
+
#
|
|
59
|
+
# @param bytes [Array<Integer>] CharString bytes
|
|
60
|
+
# @return [Array<Hint>] Extracted hints
|
|
61
|
+
def parse_charstring(bytes)
|
|
62
|
+
hints = []
|
|
63
|
+
stack = []
|
|
64
|
+
i = 0
|
|
65
|
+
|
|
66
|
+
while i < bytes.length
|
|
67
|
+
byte = bytes[i]
|
|
68
|
+
|
|
69
|
+
if operator?(byte)
|
|
70
|
+
# Process operator
|
|
71
|
+
operator = if byte == 12
|
|
72
|
+
# Two-byte operator
|
|
73
|
+
i += 1
|
|
74
|
+
(12 << 8) | bytes[i]
|
|
75
|
+
else
|
|
76
|
+
byte
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
hint = process_operator(operator, stack)
|
|
80
|
+
hints << hint if hint
|
|
81
|
+
|
|
82
|
+
# Clear stack after operator
|
|
83
|
+
stack.clear
|
|
84
|
+
i += 1
|
|
85
|
+
else
|
|
86
|
+
# Number - push to stack
|
|
87
|
+
num, consumed = decode_number(bytes, i)
|
|
88
|
+
stack << num if num
|
|
89
|
+
i += consumed
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
hints
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if byte is an operator
|
|
97
|
+
#
|
|
98
|
+
# @param byte [Integer] Byte value
|
|
99
|
+
# @return [Boolean] True if operator
|
|
100
|
+
def operator?(byte)
|
|
101
|
+
byte <= 31 || byte == 255
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Decode a number from CharString
|
|
105
|
+
#
|
|
106
|
+
# @param bytes [Array<Integer>] CharString bytes
|
|
107
|
+
# @param index [Integer] Starting position
|
|
108
|
+
# @return [Array<Integer, Integer>] [number, bytes_consumed]
|
|
109
|
+
def decode_number(bytes, index)
|
|
110
|
+
byte = bytes[index]
|
|
111
|
+
return [nil, 1] if byte.nil?
|
|
112
|
+
|
|
113
|
+
case byte
|
|
114
|
+
when 28
|
|
115
|
+
# 3-byte signed integer
|
|
116
|
+
if index + 2 < bytes.length
|
|
117
|
+
num = (bytes[index + 1] << 8) | bytes[index + 2]
|
|
118
|
+
num = num - 65536 if num > 32767
|
|
119
|
+
[num, 3]
|
|
120
|
+
else
|
|
121
|
+
[nil, 1]
|
|
122
|
+
end
|
|
123
|
+
when 32..246
|
|
124
|
+
# Single byte integer
|
|
125
|
+
[byte - 139, 1]
|
|
126
|
+
when 247..250
|
|
127
|
+
# Positive 2-byte integer
|
|
128
|
+
if index + 1 < bytes.length
|
|
129
|
+
num = (byte - 247) * 256 + bytes[index + 1] + 108
|
|
130
|
+
[num, 2]
|
|
131
|
+
else
|
|
132
|
+
[nil, 1]
|
|
133
|
+
end
|
|
134
|
+
when 251..254
|
|
135
|
+
# Negative 2-byte integer
|
|
136
|
+
if index + 1 < bytes.length
|
|
137
|
+
num = -(byte - 251) * 256 - bytes[index + 1] - 108
|
|
138
|
+
[num, 2]
|
|
139
|
+
else
|
|
140
|
+
[nil, 1]
|
|
141
|
+
end
|
|
142
|
+
when 255
|
|
143
|
+
# 5-byte signed integer
|
|
144
|
+
if index + 4 < bytes.length
|
|
145
|
+
num = (bytes[index + 1] << 24) | (bytes[index + 2] << 16) |
|
|
146
|
+
(bytes[index + 3] << 8) | bytes[index + 4]
|
|
147
|
+
num = num - 4294967296 if num > 2147483647
|
|
148
|
+
[num, 5]
|
|
149
|
+
else
|
|
150
|
+
[nil, 1]
|
|
151
|
+
end
|
|
152
|
+
else
|
|
153
|
+
[nil, 1]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Process hint operator and create Hint object
|
|
158
|
+
#
|
|
159
|
+
# @param operator [Integer] Operator code
|
|
160
|
+
# @param stack [Array<Integer>] Current operand stack
|
|
161
|
+
# @return [Hint, nil] Hint object if operator is a hint
|
|
162
|
+
def process_operator(operator, stack)
|
|
163
|
+
case operator
|
|
164
|
+
when HSTEM
|
|
165
|
+
# Horizontal stem hint
|
|
166
|
+
extract_stem_hint(stack, :horizontal)
|
|
167
|
+
|
|
168
|
+
when VSTEM
|
|
169
|
+
# Vertical stem hint
|
|
170
|
+
extract_stem_hint(stack, :vertical)
|
|
171
|
+
|
|
172
|
+
when HSTEM3
|
|
173
|
+
# Multiple horizontal stems
|
|
174
|
+
extract_stem3_hint(stack, :horizontal)
|
|
175
|
+
|
|
176
|
+
when VSTEM3
|
|
177
|
+
# Multiple vertical stems
|
|
178
|
+
extract_stem3_hint(stack, :vertical)
|
|
179
|
+
|
|
180
|
+
when HINTMASK
|
|
181
|
+
# Hint replacement mask
|
|
182
|
+
Models::Hint.new(
|
|
183
|
+
type: :hint_replacement,
|
|
184
|
+
data: { mask: stack.dup },
|
|
185
|
+
source_format: :postscript
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
when CNTRMASK
|
|
189
|
+
# Counter control mask
|
|
190
|
+
Models::Hint.new(
|
|
191
|
+
type: :counter,
|
|
192
|
+
data: { zones: stack.dup },
|
|
193
|
+
source_format: :postscript
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
else
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Extract stem hint from stack
|
|
202
|
+
#
|
|
203
|
+
# @param stack [Array<Integer>] Operand stack
|
|
204
|
+
# @param orientation [Symbol] :horizontal or :vertical
|
|
205
|
+
# @return [Hint] Stem hint
|
|
206
|
+
def extract_stem_hint(stack, orientation)
|
|
207
|
+
# Stack should have pairs of [position, width]
|
|
208
|
+
return nil if stack.empty? || stack.length < 2
|
|
209
|
+
|
|
210
|
+
# Take first pair
|
|
211
|
+
position = stack[0]
|
|
212
|
+
width = stack[1]
|
|
213
|
+
|
|
214
|
+
Models::Hint.new(
|
|
215
|
+
type: :stem,
|
|
216
|
+
data: {
|
|
217
|
+
position: position,
|
|
218
|
+
width: width,
|
|
219
|
+
orientation: orientation
|
|
220
|
+
},
|
|
221
|
+
source_format: :postscript
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Extract stem3 hint from stack
|
|
226
|
+
#
|
|
227
|
+
# @param stack [Array<Integer>] Operand stack
|
|
228
|
+
# @param orientation [Symbol] :horizontal or :vertical
|
|
229
|
+
# @return [Hint] Stem3 hint
|
|
230
|
+
def extract_stem3_hint(stack, orientation)
|
|
231
|
+
# Stack should have 6 values: 3 pairs of [position, width]
|
|
232
|
+
return nil if stack.length < 6
|
|
233
|
+
|
|
234
|
+
stems = []
|
|
235
|
+
(0..2).each do |i|
|
|
236
|
+
pos_idx = i * 2
|
|
237
|
+
stems << {
|
|
238
|
+
position: stack[pos_idx],
|
|
239
|
+
width: stack[pos_idx + 1]
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
Models::Hint.new(
|
|
244
|
+
type: :stem3,
|
|
245
|
+
data: {
|
|
246
|
+
stems: stems,
|
|
247
|
+
orientation: orientation
|
|
248
|
+
},
|
|
249
|
+
source_format: :postscript
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/hint"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Hints
|
|
7
|
+
# Applies rendering hints to TrueType glyph data
|
|
8
|
+
#
|
|
9
|
+
# This applier converts universal Hint objects into TrueType bytecode
|
|
10
|
+
# instructions and integrates them into glyph data. It ensures proper
|
|
11
|
+
# instruction sequencing and maintains compatibility with TrueType
|
|
12
|
+
# instruction execution model.
|
|
13
|
+
#
|
|
14
|
+
# @example Apply hints to a glyph
|
|
15
|
+
# applier = TrueTypeHintApplier.new
|
|
16
|
+
# glyph_with_hints = applier.apply(glyph, hints)
|
|
17
|
+
class TrueTypeHintApplier
|
|
18
|
+
# Apply hints to TrueType glyph
|
|
19
|
+
#
|
|
20
|
+
# @param glyph [Glyph] Target glyph
|
|
21
|
+
# @param hints [Array<Hint>] Hints to apply
|
|
22
|
+
# @return [Glyph] Glyph with applied hints
|
|
23
|
+
def apply(glyph, hints)
|
|
24
|
+
return glyph if hints.nil? || hints.empty?
|
|
25
|
+
return glyph if glyph.nil?
|
|
26
|
+
|
|
27
|
+
# Convert hints to TrueType instructions
|
|
28
|
+
instructions = build_instructions(hints)
|
|
29
|
+
|
|
30
|
+
# Apply to glyph (this is a simplified version)
|
|
31
|
+
# In a real implementation, we would need to:
|
|
32
|
+
# 1. Analyze existing glyph structure
|
|
33
|
+
# 2. Insert instructions at appropriate points
|
|
34
|
+
# 3. Update glyph instruction data
|
|
35
|
+
|
|
36
|
+
# For now, we just return the glyph as-is since
|
|
37
|
+
# this is a complex operation requiring deep integration
|
|
38
|
+
# with the glyph structure
|
|
39
|
+
glyph
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Build TrueType instruction sequence from hints
|
|
45
|
+
#
|
|
46
|
+
# @param hints [Array<Hint>] Hints to convert
|
|
47
|
+
# @return [Array<Integer>] Instruction bytes
|
|
48
|
+
def build_instructions(hints)
|
|
49
|
+
instructions = []
|
|
50
|
+
|
|
51
|
+
hints.each do |hint|
|
|
52
|
+
hint_instructions = hint.to_truetype
|
|
53
|
+
instructions.concat(hint_instructions) if hint_instructions
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
instructions
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Validate instruction sequence
|
|
60
|
+
#
|
|
61
|
+
# @param instructions [Array<Integer>] Instructions to validate
|
|
62
|
+
# @return [Boolean] True if valid
|
|
63
|
+
def valid_instructions?(instructions)
|
|
64
|
+
return true if instructions.empty?
|
|
65
|
+
|
|
66
|
+
# Basic validation - check for valid opcodes
|
|
67
|
+
instructions.all? { |byte| byte >= 0 && byte <= 255 }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
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 hints from TrueType glyph
|
|
39
|
+
#
|
|
40
|
+
# @param glyph [Glyph] TrueType glyph with instructions
|
|
41
|
+
# @return [Array<Hint>] Extracted hints
|
|
42
|
+
def extract(glyph)
|
|
43
|
+
return [] if glyph.nil? || glyph.empty?
|
|
44
|
+
return [] unless glyph.respond_to?(:instructions)
|
|
45
|
+
|
|
46
|
+
instructions = glyph.instructions || []
|
|
47
|
+
return [] if instructions.empty?
|
|
48
|
+
|
|
49
|
+
parse_instructions(instructions)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Parse TrueType instruction bytes into Hint objects
|
|
55
|
+
#
|
|
56
|
+
# @param instructions [String, Array<Integer>] Instruction bytes
|
|
57
|
+
# @return [Array<Hint>] Parsed hints
|
|
58
|
+
def parse_instructions(instructions)
|
|
59
|
+
hints = []
|
|
60
|
+
bytes = instructions.is_a?(String) ? instructions.bytes : instructions
|
|
61
|
+
i = 0
|
|
62
|
+
|
|
63
|
+
while i < bytes.length
|
|
64
|
+
opcode = bytes[i]
|
|
65
|
+
|
|
66
|
+
case opcode
|
|
67
|
+
when MDAP_RND, MDAP_NORND
|
|
68
|
+
# Stem positioning hint
|
|
69
|
+
hint = extract_stem_hint(bytes, i)
|
|
70
|
+
hints << hint if hint
|
|
71
|
+
i += 1
|
|
72
|
+
|
|
73
|
+
when MDRP_MIN_RND_BLACK
|
|
74
|
+
# Stem width hint (usually follows MDAP)
|
|
75
|
+
# This is typically part of a stem hint pair
|
|
76
|
+
i += 1
|
|
77
|
+
|
|
78
|
+
when IUP_Y, IUP_X
|
|
79
|
+
# Interpolation hint
|
|
80
|
+
hints << Models::Hint.new(
|
|
81
|
+
type: :interpolate,
|
|
82
|
+
data: { axis: opcode == IUP_Y ? :y : :x },
|
|
83
|
+
source_format: :truetype
|
|
84
|
+
)
|
|
85
|
+
i += 1
|
|
86
|
+
|
|
87
|
+
when *SHP
|
|
88
|
+
# Shift point hint
|
|
89
|
+
hints << Models::Hint.new(
|
|
90
|
+
type: :shift,
|
|
91
|
+
data: { instructions: [opcode] },
|
|
92
|
+
source_format: :truetype
|
|
93
|
+
)
|
|
94
|
+
i += 1
|
|
95
|
+
|
|
96
|
+
when ALIGNRP
|
|
97
|
+
# Alignment hint
|
|
98
|
+
hints << Models::Hint.new(
|
|
99
|
+
type: :align,
|
|
100
|
+
data: {},
|
|
101
|
+
source_format: :truetype
|
|
102
|
+
)
|
|
103
|
+
i += 1
|
|
104
|
+
|
|
105
|
+
when DELTAP1, DELTAP2, DELTAP3
|
|
106
|
+
# Delta hint - pixel-level adjustments
|
|
107
|
+
# Next byte is the count
|
|
108
|
+
i += 1
|
|
109
|
+
if i < bytes.length
|
|
110
|
+
count = bytes[i]
|
|
111
|
+
delta_data = bytes[i + 1, count * 2] || []
|
|
112
|
+
hints << Models::Hint.new(
|
|
113
|
+
type: :delta,
|
|
114
|
+
data: {
|
|
115
|
+
instructions: [opcode] + [count] + delta_data,
|
|
116
|
+
count: count
|
|
117
|
+
},
|
|
118
|
+
source_format: :truetype
|
|
119
|
+
)
|
|
120
|
+
i += count * 2 + 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
else
|
|
124
|
+
# Unknown or data bytes - skip
|
|
125
|
+
i += 1
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
hints
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Extract stem hint from MDAP instruction
|
|
133
|
+
#
|
|
134
|
+
# @param bytes [Array<Integer>] Instruction bytes
|
|
135
|
+
# @param index [Integer] Current position
|
|
136
|
+
# @return [Hint, nil] Stem hint if found
|
|
137
|
+
def extract_stem_hint(bytes, index)
|
|
138
|
+
# In TrueType, stem hints are inferred from MDAP + MDRP pairs
|
|
139
|
+
# This is a simplified extraction - real implementation would
|
|
140
|
+
# need to track the graphics state and point references
|
|
141
|
+
|
|
142
|
+
# Check if next instruction is MDRP (stem width)
|
|
143
|
+
has_width = index + 1 < bytes.length &&
|
|
144
|
+
bytes[index + 1] == MDRP_MIN_RND_BLACK
|
|
145
|
+
|
|
146
|
+
if has_width
|
|
147
|
+
Models::Hint.new(
|
|
148
|
+
type: :stem,
|
|
149
|
+
data: {
|
|
150
|
+
position: 0, # Would be extracted from graphics state
|
|
151
|
+
width: 0, # Would be calculated from MDRP
|
|
152
|
+
orientation: :vertical # Inferred from instruction context
|
|
153
|
+
},
|
|
154
|
+
source_format: :truetype
|
|
155
|
+
)
|
|
156
|
+
else
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|