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,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../models/hint"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Hints
|
|
8
|
+
# Converts hints between TrueType and PostScript formats
|
|
9
|
+
#
|
|
10
|
+
# This converter handles bidirectional conversion of rendering hints,
|
|
11
|
+
# translating between TrueType instruction-based hinting and PostScript
|
|
12
|
+
# operator-based hinting while preserving intent where possible.
|
|
13
|
+
#
|
|
14
|
+
# **Conversion Strategy:**
|
|
15
|
+
#
|
|
16
|
+
# - TrueType → PostScript: Extract semantic meaning from instructions
|
|
17
|
+
# and convert to corresponding PostScript operators
|
|
18
|
+
# - PostScript → TrueType: Analyze hint operators and generate
|
|
19
|
+
# equivalent TrueType instructions
|
|
20
|
+
#
|
|
21
|
+
# @example Convert TrueType hints to PostScript
|
|
22
|
+
# converter = HintConverter.new
|
|
23
|
+
# ps_hints = converter.to_postscript(tt_hints)
|
|
24
|
+
#
|
|
25
|
+
# @example Convert PostScript hints to TrueType
|
|
26
|
+
# converter = HintConverter.new
|
|
27
|
+
# tt_hints = converter.to_truetype(ps_hints)
|
|
28
|
+
class HintConverter
|
|
29
|
+
# Convert hints to PostScript format
|
|
30
|
+
#
|
|
31
|
+
# @param hints [Array<Hint>] Source hints (any format)
|
|
32
|
+
# @return [Array<Hint>] Hints in PostScript format
|
|
33
|
+
def to_postscript(hints)
|
|
34
|
+
return [] if hints.nil? || hints.empty?
|
|
35
|
+
|
|
36
|
+
hints.map do |hint|
|
|
37
|
+
next hint if hint.source_format == :postscript
|
|
38
|
+
|
|
39
|
+
convert_hint_to_postscript(hint)
|
|
40
|
+
end.compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Convert hints to TrueType format
|
|
44
|
+
#
|
|
45
|
+
# @param hints [Array<Hint>] Source hints (any format)
|
|
46
|
+
# @return [Array<Hint>] Hints in TrueType format
|
|
47
|
+
def to_truetype(hints)
|
|
48
|
+
return [] if hints.nil? || hints.empty?
|
|
49
|
+
|
|
50
|
+
hints.map do |hint|
|
|
51
|
+
next hint if hint.source_format == :truetype
|
|
52
|
+
|
|
53
|
+
convert_hint_to_truetype(hint)
|
|
54
|
+
end.compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Optimize hint set by removing redundant hints
|
|
58
|
+
#
|
|
59
|
+
# @param hints [Array<Hint>] Hints to optimize
|
|
60
|
+
# @return [Array<Hint>] Optimized hints
|
|
61
|
+
def optimize(hints)
|
|
62
|
+
return [] if hints.nil? || hints.empty?
|
|
63
|
+
|
|
64
|
+
# Remove duplicate hints
|
|
65
|
+
unique_hints = hints.uniq { |h| [h.type, h.data] }
|
|
66
|
+
|
|
67
|
+
# Remove conflicting hints (keep first)
|
|
68
|
+
remove_conflicts(unique_hints)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert entire HintSet between formats
|
|
72
|
+
#
|
|
73
|
+
# @param hint_set [Models::HintSet] Source hint set
|
|
74
|
+
# @param target_format [Symbol] Target format (:truetype or :postscript)
|
|
75
|
+
# @return [Models::HintSet] Converted hint set
|
|
76
|
+
def convert_hint_set(hint_set, target_format)
|
|
77
|
+
return hint_set if hint_set.format == target_format.to_s
|
|
78
|
+
|
|
79
|
+
result = Models::HintSet.new(format: target_format.to_s)
|
|
80
|
+
|
|
81
|
+
case target_format
|
|
82
|
+
when :postscript
|
|
83
|
+
# Convert font-level TT → PS
|
|
84
|
+
if hint_set.font_program || hint_set.control_value_program ||
|
|
85
|
+
hint_set.control_values&.any?
|
|
86
|
+
ps_dict = convert_tt_programs_to_ps_dict(
|
|
87
|
+
hint_set.font_program,
|
|
88
|
+
hint_set.control_value_program,
|
|
89
|
+
hint_set.control_values
|
|
90
|
+
)
|
|
91
|
+
result.private_dict_hints = ps_dict.to_json
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Convert per-glyph hints
|
|
95
|
+
hint_set.hinted_glyph_ids.each do |glyph_id|
|
|
96
|
+
glyph_hints = hint_set.get_glyph_hints(glyph_id)
|
|
97
|
+
ps_hints = to_postscript(glyph_hints)
|
|
98
|
+
result.add_glyph_hints(glyph_id, ps_hints) unless ps_hints.empty?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
when :truetype
|
|
102
|
+
# Convert font-level PS → TT
|
|
103
|
+
if hint_set.private_dict_hints && hint_set.private_dict_hints != "{}"
|
|
104
|
+
tt_programs = convert_ps_dict_to_tt_programs(
|
|
105
|
+
JSON.parse(hint_set.private_dict_hints)
|
|
106
|
+
)
|
|
107
|
+
result.font_program = tt_programs[:fpgm]
|
|
108
|
+
result.control_value_program = tt_programs[:prep]
|
|
109
|
+
result.control_values = tt_programs[:cvt]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Convert per-glyph hints
|
|
113
|
+
hint_set.hinted_glyph_ids.each do |glyph_id|
|
|
114
|
+
glyph_hints = hint_set.get_glyph_hints(glyph_id)
|
|
115
|
+
tt_hints = to_truetype(glyph_hints)
|
|
116
|
+
result.add_glyph_hints(glyph_id, tt_hints) unless tt_hints.empty?
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
result.has_hints = !result.empty?
|
|
121
|
+
result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Convert a single hint to PostScript format
|
|
127
|
+
#
|
|
128
|
+
# @param hint [Hint] Source hint
|
|
129
|
+
# @return [Hint, nil] Converted hint or nil if incompatible
|
|
130
|
+
def convert_hint_to_postscript(hint)
|
|
131
|
+
return nil unless hint.compatible_with?(:postscript)
|
|
132
|
+
|
|
133
|
+
# Get PostScript representation from hint
|
|
134
|
+
ps_data = hint.to_postscript
|
|
135
|
+
|
|
136
|
+
# Create new hint with PostScript format
|
|
137
|
+
Models::Hint.new(
|
|
138
|
+
type: hint.type,
|
|
139
|
+
data: ps_data,
|
|
140
|
+
source_format: :postscript
|
|
141
|
+
)
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
warn "Failed to convert hint to PostScript: #{e.message}"
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Convert a single hint to TrueType format
|
|
148
|
+
#
|
|
149
|
+
# @param hint [Hint] Source hint
|
|
150
|
+
# @return [Hint, nil] Converted hint or nil if incompatible
|
|
151
|
+
def convert_hint_to_truetype(hint)
|
|
152
|
+
return nil unless hint.compatible_with?(:truetype)
|
|
153
|
+
|
|
154
|
+
# Get TrueType representation from hint
|
|
155
|
+
tt_instructions = hint.to_truetype
|
|
156
|
+
|
|
157
|
+
# Create new hint with TrueType format
|
|
158
|
+
Models::Hint.new(
|
|
159
|
+
type: hint.type,
|
|
160
|
+
data: { instructions: tt_instructions },
|
|
161
|
+
source_format: :truetype
|
|
162
|
+
)
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
warn "Failed to convert hint to TrueType: #{e.message}"
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Remove conflicting hints from set
|
|
169
|
+
#
|
|
170
|
+
# @param hints [Array<Hint>] Hints to check
|
|
171
|
+
# @return [Array<Hint>] Non-conflicting hints
|
|
172
|
+
def remove_conflicts(hints)
|
|
173
|
+
non_conflicting = []
|
|
174
|
+
|
|
175
|
+
hints.each do |hint|
|
|
176
|
+
# Check if this hint conflicts with any already selected
|
|
177
|
+
conflicts = non_conflicting.any? do |existing|
|
|
178
|
+
hints_conflict?(hint, existing)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
non_conflicting << hint unless conflicts
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
non_conflicting
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Check if two hints conflict
|
|
188
|
+
#
|
|
189
|
+
# @param hint1 [Hint] First hint
|
|
190
|
+
# @param hint2 [Hint] Second hint
|
|
191
|
+
# @return [Boolean] True if hints conflict
|
|
192
|
+
def hints_conflict?(hint1, hint2)
|
|
193
|
+
# Hints of different types don't conflict
|
|
194
|
+
return false if hint1.type != hint2.type
|
|
195
|
+
|
|
196
|
+
case hint1.type
|
|
197
|
+
when :stem
|
|
198
|
+
# Stem hints conflict if they overlap
|
|
199
|
+
stems_overlap?(hint1.data, hint2.data)
|
|
200
|
+
when :interpolate
|
|
201
|
+
# Multiple interpolation hints on same axis conflict
|
|
202
|
+
hint1.data[:axis] == hint2.data[:axis]
|
|
203
|
+
else
|
|
204
|
+
# Other hint types don't conflict
|
|
205
|
+
false
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Check if two stem hints overlap
|
|
210
|
+
#
|
|
211
|
+
# @param stem1 [Hash] First stem data
|
|
212
|
+
# @param stem2 [Hash] Second stem data
|
|
213
|
+
# @return [Boolean] True if stems overlap
|
|
214
|
+
def stems_overlap?(stem1, stem2)
|
|
215
|
+
# Must be same orientation to conflict
|
|
216
|
+
return false if stem1[:orientation] != stem2[:orientation]
|
|
217
|
+
|
|
218
|
+
pos1 = stem1[:position] || 0
|
|
219
|
+
width1 = stem1[:width] || 0
|
|
220
|
+
pos2 = stem2[:position] || 0
|
|
221
|
+
width2 = stem2[:width] || 0
|
|
222
|
+
|
|
223
|
+
# Check if ranges overlap
|
|
224
|
+
end1 = pos1 + width1
|
|
225
|
+
end2 = pos2 + width2
|
|
226
|
+
|
|
227
|
+
pos1 < end2 && pos2 < end1
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Convert TrueType font programs to PostScript Private dict
|
|
231
|
+
#
|
|
232
|
+
# Analyzes TrueType fpgm, prep, and cvt to extract semantic intent
|
|
233
|
+
# and generate corresponding PostScript hint parameters.
|
|
234
|
+
#
|
|
235
|
+
# @param fpgm [String] Font program bytecode
|
|
236
|
+
# @param prep [String] Control value program bytecode
|
|
237
|
+
# @param cvt [Array<Integer>] Control values
|
|
238
|
+
# @return [Hash] PostScript Private dict hint parameters
|
|
239
|
+
def convert_tt_programs_to_ps_dict(fpgm, prep, cvt)
|
|
240
|
+
hints = {}
|
|
241
|
+
|
|
242
|
+
# Extract stem widths from cvt if present
|
|
243
|
+
# CVT values typically contain standard widths
|
|
244
|
+
if cvt && !cvt.empty?
|
|
245
|
+
# First CVT value often represents standard horizontal stem
|
|
246
|
+
hints[:std_hw] = cvt[0].abs if cvt.length > 0
|
|
247
|
+
# Second CVT value often represents standard vertical stem
|
|
248
|
+
hints[:std_vw] = cvt[1].abs if cvt.length > 1
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Analyze control value program for alignment zones
|
|
252
|
+
# TrueType doesn't have exact Blue zones, so we use defaults
|
|
253
|
+
# These are standard values that work for most Latin fonts
|
|
254
|
+
hints[:blue_values] = [-20, 0, 706, 726]
|
|
255
|
+
|
|
256
|
+
# Optional: Add other_blues for descenders if we detect them
|
|
257
|
+
# This would require analyzing prep program, which is complex
|
|
258
|
+
# For now, use conservative defaults
|
|
259
|
+
|
|
260
|
+
hints
|
|
261
|
+
rescue StandardError => e
|
|
262
|
+
warn "Error converting TT programs to PS dict: #{e.message}"
|
|
263
|
+
{}
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Convert PostScript Private dict to TrueType font programs
|
|
267
|
+
#
|
|
268
|
+
# Generates TrueType control values and programs from PostScript
|
|
269
|
+
# hint parameters.
|
|
270
|
+
#
|
|
271
|
+
# @param ps_dict [Hash] PostScript Private dict parameters
|
|
272
|
+
# @return [Hash] TrueType programs ({ fpgm:, prep:, cvt: })
|
|
273
|
+
def convert_ps_dict_to_tt_programs(ps_dict)
|
|
274
|
+
# Handle both string and symbol keys from JSON
|
|
275
|
+
ps_dict = ps_dict.transform_keys(&:to_sym) if ps_dict.keys.first.is_a?(String)
|
|
276
|
+
|
|
277
|
+
# Generate control values from PS parameters
|
|
278
|
+
cvt = []
|
|
279
|
+
|
|
280
|
+
# Add standard stem widths as CVT values
|
|
281
|
+
cvt << ps_dict[:std_hw] if ps_dict[:std_hw]
|
|
282
|
+
cvt << ps_dict[:std_vw] if ps_dict[:std_vw]
|
|
283
|
+
|
|
284
|
+
# Add stem snap values if present
|
|
285
|
+
if ps_dict[:stem_snap_h]&.is_a?(Array)
|
|
286
|
+
cvt.concat(ps_dict[:stem_snap_h])
|
|
287
|
+
end
|
|
288
|
+
if ps_dict[:stem_snap_v]&.is_a?(Array)
|
|
289
|
+
cvt.concat(ps_dict[:stem_snap_v])
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Remove duplicates and sort
|
|
293
|
+
cvt = cvt.uniq.sort
|
|
294
|
+
|
|
295
|
+
# Generate basic prep program (empty for converted fonts)
|
|
296
|
+
# A real implementation would generate instructions to set up CVT
|
|
297
|
+
prep = ""
|
|
298
|
+
|
|
299
|
+
# fpgm typically empty for converted fonts
|
|
300
|
+
# Functions would need to be synthesized from scratch
|
|
301
|
+
fpgm = ""
|
|
302
|
+
|
|
303
|
+
{ fpgm: fpgm, prep: prep, cvt: cvt }
|
|
304
|
+
rescue StandardError => e
|
|
305
|
+
warn "Error converting PS dict to TT programs: #{e.message}"
|
|
306
|
+
{ fpgm: "", prep: "", cvt: [] }
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../models/hint"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Hints
|
|
8
|
+
# Applies rendering hints to PostScript/CFF font tables
|
|
9
|
+
#
|
|
10
|
+
# This applier validates and applies PostScript hint data to CFF fonts by
|
|
11
|
+
# rebuilding the entire CFF table structure with updated Private DICT parameters.
|
|
12
|
+
#
|
|
13
|
+
# **Status**: Fully Operational (Phase 10A Complete)
|
|
14
|
+
#
|
|
15
|
+
# **PostScript Hint Parameters (Private DICT)**:
|
|
16
|
+
#
|
|
17
|
+
# - blue_values: Alignment zones for overshoot suppression
|
|
18
|
+
# - other_blues: Additional alignment zones
|
|
19
|
+
# - std_hw: Standard horizontal stem width
|
|
20
|
+
# - std_vw: Standard vertical stem width
|
|
21
|
+
# - stem_snap_h: Horizontal stem snap widths
|
|
22
|
+
# - stem_snap_v: Vertical stem snap widths
|
|
23
|
+
# - blue_scale, blue_shift, blue_fuzz: Overshoot parameters
|
|
24
|
+
# - force_bold: Force bold flag
|
|
25
|
+
# - language_group: Language group (0=Latin, 1=CJK)
|
|
26
|
+
#
|
|
27
|
+
# @example Apply PostScript hints
|
|
28
|
+
# applier = PostScriptHintApplier.new
|
|
29
|
+
# tables = { "CFF " => cff_table }
|
|
30
|
+
# hint_set = HintSet.new(format: "postscript", private_dict_hints: hints_json)
|
|
31
|
+
# result = applier.apply(hint_set, tables)
|
|
32
|
+
class PostScriptHintApplier
|
|
33
|
+
# Apply PostScript hints to font tables
|
|
34
|
+
#
|
|
35
|
+
# Validates hint data and rebuilds CFF table with updated Private DICT.
|
|
36
|
+
# Supports arbitrary Private DICT size changes through full table reconstruction.
|
|
37
|
+
# Also supports per-glyph hints injected directly into CharStrings.
|
|
38
|
+
#
|
|
39
|
+
# @param hint_set [HintSet] Hint data to apply
|
|
40
|
+
# @param tables [Hash] Font tables (must include "CFF " or "CFF2 ")
|
|
41
|
+
# @return [Hash] Updated font tables with hints applied
|
|
42
|
+
def apply(hint_set, tables)
|
|
43
|
+
return tables if hint_set.nil? || hint_set.empty?
|
|
44
|
+
return tables unless hint_set.format == "postscript"
|
|
45
|
+
|
|
46
|
+
if cff2_table?(tables)
|
|
47
|
+
apply_cff2_hints(hint_set, tables)
|
|
48
|
+
elsif cff_table?(tables)
|
|
49
|
+
apply_cff_hints(hint_set, tables)
|
|
50
|
+
else
|
|
51
|
+
tables
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Check if tables contain CFF2 table
|
|
58
|
+
#
|
|
59
|
+
# @param tables [Hash] Font tables
|
|
60
|
+
# @return [Boolean] True if CFF2 table present
|
|
61
|
+
def cff2_table?(tables)
|
|
62
|
+
tables.key?("CFF2") || tables.key?("CFF2 ")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if tables contain CFF table
|
|
66
|
+
#
|
|
67
|
+
# @param tables [Hash] Font tables
|
|
68
|
+
# @return [Boolean] True if CFF table present
|
|
69
|
+
def cff_table?(tables)
|
|
70
|
+
tables.key?("CFF ")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Apply hints to CFF2 variable font
|
|
74
|
+
#
|
|
75
|
+
# @param hint_set [HintSet] Hint set with font-level and per-glyph hints
|
|
76
|
+
# @param tables [Hash] Font tables
|
|
77
|
+
# @return [Hash] Updated tables
|
|
78
|
+
def apply_cff2_hints(hint_set, tables)
|
|
79
|
+
# Load CFF2 table
|
|
80
|
+
cff2_data = tables["CFF2"] || tables["CFF2 "]
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
require_relative "../tables/cff2/table_reader"
|
|
84
|
+
require_relative "../tables/cff2/table_builder"
|
|
85
|
+
|
|
86
|
+
reader = Tables::Cff2::TableReader.new(cff2_data)
|
|
87
|
+
|
|
88
|
+
# Validate CFF2 version
|
|
89
|
+
reader.read_header
|
|
90
|
+
unless reader.header[:major_version] == 2
|
|
91
|
+
warn "Invalid CFF2 table version: #{reader.header[:major_version]}"
|
|
92
|
+
return tables
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Build with hints
|
|
96
|
+
builder = Tables::Cff2::TableBuilder.new(reader, hint_set)
|
|
97
|
+
modified_table = builder.build
|
|
98
|
+
|
|
99
|
+
# Update tables
|
|
100
|
+
table_key = tables.key?("CFF2") ? "CFF2" : "CFF2 "
|
|
101
|
+
tables[table_key] = modified_table
|
|
102
|
+
|
|
103
|
+
tables
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
warn "Error applying CFF2 hints: #{e.message}"
|
|
106
|
+
tables
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Apply hints to CFF font
|
|
111
|
+
#
|
|
112
|
+
# @param hint_set [HintSet] Hint set with font-level and per-glyph hints
|
|
113
|
+
# @param tables [Hash] Font tables
|
|
114
|
+
# @return [Hash] Updated tables
|
|
115
|
+
def apply_cff_hints(hint_set, tables)
|
|
116
|
+
return tables unless tables["CFF "]
|
|
117
|
+
|
|
118
|
+
# Validate hint parameters (Private DICT)
|
|
119
|
+
hint_params = parse_hint_parameters(hint_set)
|
|
120
|
+
|
|
121
|
+
# Check if we have per-glyph hints
|
|
122
|
+
has_per_glyph_hints = hint_set.hinted_glyph_count.positive?
|
|
123
|
+
|
|
124
|
+
# If neither font-level nor per-glyph hints, return unchanged
|
|
125
|
+
return tables unless hint_params || has_per_glyph_hints
|
|
126
|
+
|
|
127
|
+
# Validate font-level parameters if present
|
|
128
|
+
if hint_params && !valid_hint_parameters?(hint_params)
|
|
129
|
+
return tables
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Apply hints (both font-level and per-glyph)
|
|
133
|
+
begin
|
|
134
|
+
require_relative "../tables/cff/table_builder"
|
|
135
|
+
require_relative "../tables/cff/charstring_rebuilder"
|
|
136
|
+
require_relative "../tables/cff/hint_operation_injector"
|
|
137
|
+
require_relative "../tables/cff"
|
|
138
|
+
|
|
139
|
+
# Parse CFF binary data into Cff object if needed
|
|
140
|
+
cff_data = tables["CFF "]
|
|
141
|
+
cff_table = if cff_data.is_a?(Tables::Cff)
|
|
142
|
+
cff_data
|
|
143
|
+
else
|
|
144
|
+
Tables::Cff.read(cff_data)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Prepare per-glyph hint data if present
|
|
148
|
+
per_glyph_hints = if has_per_glyph_hints
|
|
149
|
+
extract_per_glyph_hints(hint_set)
|
|
150
|
+
else
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
new_cff_data = Tables::Cff::TableBuilder.rebuild(
|
|
155
|
+
cff_table,
|
|
156
|
+
private_dict_hints: hint_params,
|
|
157
|
+
per_glyph_hints: per_glyph_hints
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
tables["CFF "] = new_cff_data
|
|
161
|
+
tables
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
warn "Failed to apply PostScript hints: #{e.message}"
|
|
164
|
+
tables
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Parse hint parameters from HintSet
|
|
169
|
+
#
|
|
170
|
+
# @param hint_set [HintSet] Hint set with Private dict hints
|
|
171
|
+
# @return [Hash, nil] Parsed hint parameters, or nil if invalid
|
|
172
|
+
def parse_hint_parameters(hint_set)
|
|
173
|
+
return nil unless hint_set.private_dict_hints
|
|
174
|
+
return nil if hint_set.private_dict_hints == "{}"
|
|
175
|
+
|
|
176
|
+
JSON.parse(hint_set.private_dict_hints)
|
|
177
|
+
rescue JSON::ParserError => e
|
|
178
|
+
warn "Failed to parse Private dict hints: #{e.message}"
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Validate hint parameters against CFF specification limits
|
|
183
|
+
#
|
|
184
|
+
# @param params [Hash] Hint parameters
|
|
185
|
+
# @return [Boolean] True if all parameters are valid
|
|
186
|
+
def valid_hint_parameters?(params)
|
|
187
|
+
# Validate blue values (must be pairs, max 7 pairs = 14 values)
|
|
188
|
+
if params["blue_values"] || params[:blue_values]
|
|
189
|
+
values = params["blue_values"] || params[:blue_values]
|
|
190
|
+
return false unless values.is_a?(Array)
|
|
191
|
+
return false if values.length > 14 # Max 7 pairs
|
|
192
|
+
return false if values.length.odd? # Must be pairs
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Validate other_blues (max 5 pairs = 10 values)
|
|
196
|
+
if params["other_blues"] || params[:other_blues]
|
|
197
|
+
values = params["other_blues"] || params[:other_blues]
|
|
198
|
+
return false unless values.is_a?(Array)
|
|
199
|
+
return false if values.length > 10
|
|
200
|
+
return false if values.length.odd?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Validate stem widths (single values)
|
|
204
|
+
if params["std_hw"] || params[:std_hw]
|
|
205
|
+
value = params["std_hw"] || params[:std_hw]
|
|
206
|
+
return false unless value.is_a?(Numeric)
|
|
207
|
+
return false if value.negative?
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if params["std_vw"] || params[:std_vw]
|
|
211
|
+
value = params["std_vw"] || params[:std_vw]
|
|
212
|
+
return false unless value.is_a?(Numeric)
|
|
213
|
+
return false if value.negative?
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Validate stem snaps (arrays, max 12 values each)
|
|
217
|
+
%w[stem_snap_h stem_snap_v].each do |key|
|
|
218
|
+
next unless params[key] || params[key.to_sym]
|
|
219
|
+
|
|
220
|
+
values = params[key] || params[key.to_sym]
|
|
221
|
+
return false unless values.is_a?(Array)
|
|
222
|
+
return false if values.length > 12
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Validate blue_scale (should be positive)
|
|
226
|
+
if params["blue_scale"] || params[:blue_scale]
|
|
227
|
+
value = params["blue_scale"] || params[:blue_scale]
|
|
228
|
+
return false unless value.is_a?(Numeric)
|
|
229
|
+
return false if value <= 0
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Validate language_group (0 or 1 only)
|
|
233
|
+
if params["language_group"] || params[:language_group]
|
|
234
|
+
value = params["language_group"] || params[:language_group]
|
|
235
|
+
return false unless [0, 1].include?(value)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Extract specific hint parameter with symbol/string key support
|
|
242
|
+
#
|
|
243
|
+
# @param params [Hash] Hint parameters
|
|
244
|
+
# @param key [String] Parameter name
|
|
245
|
+
# @return [Object, nil] Parameter value
|
|
246
|
+
def extract_param(params, key)
|
|
247
|
+
params[key] || params[key.to_sym]
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Extract per-glyph hint data from HintSet
|
|
251
|
+
#
|
|
252
|
+
# @param hint_set [HintSet] Hint set with per-glyph hints
|
|
253
|
+
# @return [Hash] Hash mapping glyph_id => Array<Hint>
|
|
254
|
+
def extract_per_glyph_hints(hint_set)
|
|
255
|
+
per_glyph = {}
|
|
256
|
+
|
|
257
|
+
hint_set.hinted_glyph_ids.each do |glyph_id|
|
|
258
|
+
hints = hint_set.get_glyph_hints(glyph_id)
|
|
259
|
+
per_glyph[glyph_id.to_i] = hints unless hints.empty?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
per_glyph
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|