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,411 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "format_detector"
|
|
4
|
+
require_relative "variation_resolver"
|
|
5
|
+
require_relative "../converters/format_converter"
|
|
6
|
+
require_relative "../font_loader"
|
|
7
|
+
require_relative "../font_writer"
|
|
8
|
+
require_relative "output_writer"
|
|
9
|
+
|
|
10
|
+
module Fontisan
|
|
11
|
+
module Pipeline
|
|
12
|
+
# Orchestrates universal font transformation pipeline
|
|
13
|
+
#
|
|
14
|
+
# This is the main entry point for font conversion operations. It coordinates:
|
|
15
|
+
# 1. Format detection (via FormatDetector)
|
|
16
|
+
# 2. Font loading (via FontLoader)
|
|
17
|
+
# 3. Variation resolution (via VariationResolver)
|
|
18
|
+
# 4. Format conversion (via FormatConverter)
|
|
19
|
+
# 5. Output writing (via OutputWriter)
|
|
20
|
+
# 6. Validation (optional, via Validation::Validator)
|
|
21
|
+
#
|
|
22
|
+
# The pipeline follows a clear MECE architecture where each phase has a
|
|
23
|
+
# single responsibility and produces well-defined outputs.
|
|
24
|
+
#
|
|
25
|
+
# @example Basic TTF to OTF conversion
|
|
26
|
+
# pipeline = TransformationPipeline.new("input.ttf", "output.otf")
|
|
27
|
+
# result = pipeline.transform
|
|
28
|
+
# puts result[:success] # => true
|
|
29
|
+
#
|
|
30
|
+
# @example Variable font instance generation
|
|
31
|
+
# pipeline = TransformationPipeline.new(
|
|
32
|
+
# "variable.ttf",
|
|
33
|
+
# "bold.ttf",
|
|
34
|
+
# coordinates: { "wght" => 700.0 }
|
|
35
|
+
# )
|
|
36
|
+
# result = pipeline.transform
|
|
37
|
+
class TransformationPipeline
|
|
38
|
+
# @return [String] Input file path
|
|
39
|
+
attr_reader :input_path
|
|
40
|
+
|
|
41
|
+
# @return [String] Output file path
|
|
42
|
+
attr_reader :output_path
|
|
43
|
+
|
|
44
|
+
# @return [Hash] Transformation options
|
|
45
|
+
attr_reader :options
|
|
46
|
+
|
|
47
|
+
# Initialize transformation pipeline
|
|
48
|
+
#
|
|
49
|
+
# @param input_path [String] Path to input font
|
|
50
|
+
# @param output_path [String] Path to output font
|
|
51
|
+
# @param options [Hash] Transformation options
|
|
52
|
+
# @option options [Symbol] :target_format Target format (:ttf, :otf, :woff, :woff2)
|
|
53
|
+
# @option options [Hash] :coordinates Instance coordinates (for variable fonts)
|
|
54
|
+
# @option options [Integer] :instance_index Named instance index
|
|
55
|
+
# @option options [Boolean] :preserve_variation Preserve variation data (default: auto)
|
|
56
|
+
# @option options [Boolean] :validate Validate output (default: true)
|
|
57
|
+
# @option options [Boolean] :verbose Verbose output (default: false)
|
|
58
|
+
def initialize(input_path, output_path, options = {})
|
|
59
|
+
@input_path = input_path
|
|
60
|
+
@output_path = output_path
|
|
61
|
+
@options = default_options.merge(options)
|
|
62
|
+
@variation_strategy = nil
|
|
63
|
+
|
|
64
|
+
validate_paths!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Execute transformation pipeline
|
|
68
|
+
#
|
|
69
|
+
# This is the main entry point. It orchestrates:
|
|
70
|
+
# 1. Format detection
|
|
71
|
+
# 2. Font loading
|
|
72
|
+
# 3. Variation resolution
|
|
73
|
+
# 4. Format conversion
|
|
74
|
+
# 5. Output writing
|
|
75
|
+
# 6. Validation (optional)
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash] Transformation result with :success, :output_path, :details
|
|
78
|
+
# @raise [Error] If transformation fails
|
|
79
|
+
def transform
|
|
80
|
+
log "Starting transformation: #{@input_path} → #{@output_path}"
|
|
81
|
+
|
|
82
|
+
# Phase 1: Detect input format
|
|
83
|
+
detection = detect_input_format
|
|
84
|
+
log "Detected: #{detection[:format]} (#{detection[:variation_type]})"
|
|
85
|
+
|
|
86
|
+
# Phase 2: Load font
|
|
87
|
+
font = load_font(detection)
|
|
88
|
+
log "Loaded: #{font.class.name}"
|
|
89
|
+
|
|
90
|
+
# Phase 3: Resolve variation
|
|
91
|
+
tables = resolve_variation(font, detection)
|
|
92
|
+
log "Resolved variation using #{@variation_strategy} strategy"
|
|
93
|
+
|
|
94
|
+
# Phase 4: Convert format
|
|
95
|
+
tables = convert_format(tables, detection)
|
|
96
|
+
log "Converted to #{target_format}"
|
|
97
|
+
|
|
98
|
+
# Phase 5: Write output
|
|
99
|
+
write_output(tables, detection)
|
|
100
|
+
log "Written to #{@output_path}"
|
|
101
|
+
|
|
102
|
+
# Phase 6: Validate (optional)
|
|
103
|
+
validate_output if @options[:validate] && !same_format_conversion? && !export_only_format?
|
|
104
|
+
log "Validation passed" if @options[:validate] && !export_only_format?
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
success: true,
|
|
108
|
+
output_path: @output_path,
|
|
109
|
+
details: build_details(detection),
|
|
110
|
+
}
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
handle_error(e)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# Detect input format and capabilities
|
|
118
|
+
#
|
|
119
|
+
# @return [Hash] Detection results from FormatDetector
|
|
120
|
+
def detect_input_format
|
|
121
|
+
detector = FormatDetector.new(@input_path)
|
|
122
|
+
detector.detect
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Load font with appropriate mode
|
|
126
|
+
#
|
|
127
|
+
# @param detection [Hash] Detection results
|
|
128
|
+
# @return [Font] Loaded font object
|
|
129
|
+
def load_font(_detection)
|
|
130
|
+
FontLoader.load(@input_path, mode: :full)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Resolve variation data
|
|
134
|
+
#
|
|
135
|
+
# @param font [Font] Loaded font
|
|
136
|
+
# @param detection [Hash] Detection results
|
|
137
|
+
# @return [Hash] Processed font tables
|
|
138
|
+
def resolve_variation(font, detection)
|
|
139
|
+
# Static fonts - use preserve strategy (just copy tables)
|
|
140
|
+
return resolve_static_font(font) if detection[:variation_type] == :static
|
|
141
|
+
|
|
142
|
+
# Variable fonts - determine strategy
|
|
143
|
+
strategy = determine_variation_strategy(detection)
|
|
144
|
+
@variation_strategy = strategy
|
|
145
|
+
|
|
146
|
+
resolver = VariationResolver.new(
|
|
147
|
+
font,
|
|
148
|
+
strategy: strategy,
|
|
149
|
+
**variation_options,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
resolver.resolve
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Resolve static font (just copy tables)
|
|
156
|
+
#
|
|
157
|
+
# @param font [Font] Static font
|
|
158
|
+
# @return [Hash] Font tables
|
|
159
|
+
def resolve_static_font(font)
|
|
160
|
+
@variation_strategy = :preserve
|
|
161
|
+
|
|
162
|
+
# Get all tables from font - use table_data directly
|
|
163
|
+
font.table_data.dup
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Determine variation strategy based on options and compatibility
|
|
167
|
+
#
|
|
168
|
+
# @param detection [Hash] Detection results
|
|
169
|
+
# @return [Symbol] Strategy type (:preserve, :instance, :named)
|
|
170
|
+
def determine_variation_strategy(detection)
|
|
171
|
+
# User explicitly requested instance generation
|
|
172
|
+
if @options[:coordinates] || @options[:instance_index]
|
|
173
|
+
return @options[:instance_index] ? :named : :instance
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check if preservation is possible
|
|
177
|
+
if can_preserve_variation?(detection)
|
|
178
|
+
@options.fetch(:preserve_variation, true) ? :preserve : :instance
|
|
179
|
+
else
|
|
180
|
+
# Cannot preserve - must generate instance
|
|
181
|
+
:instance
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Check if variation can be preserved for target format
|
|
186
|
+
#
|
|
187
|
+
# @param detection [Hash] Detection results
|
|
188
|
+
# @return [Boolean] True if variation preservable
|
|
189
|
+
def can_preserve_variation?(detection)
|
|
190
|
+
source_format = detection[:format]
|
|
191
|
+
target = target_format
|
|
192
|
+
|
|
193
|
+
# Same format
|
|
194
|
+
return true if source_format == target
|
|
195
|
+
|
|
196
|
+
# Same outline family (packaging change only)
|
|
197
|
+
same_outline_family?(source_format, target)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Check if formats are in same outline family
|
|
201
|
+
#
|
|
202
|
+
# @param source [Symbol] Source format
|
|
203
|
+
# @param target [Symbol] Target format
|
|
204
|
+
# @return [Boolean] True if same family
|
|
205
|
+
def same_outline_family?(source, target)
|
|
206
|
+
truetype_formats = %i[ttf ttc woff woff2]
|
|
207
|
+
opentype_formats = %i[otf otc woff woff2]
|
|
208
|
+
|
|
209
|
+
(truetype_formats.include?(source) && truetype_formats.include?(target)) ||
|
|
210
|
+
(opentype_formats.include?(source) && opentype_formats.include?(target))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Convert format if needed
|
|
214
|
+
#
|
|
215
|
+
# @param tables [Hash] Font tables
|
|
216
|
+
# @param detection [Hash] Detection results
|
|
217
|
+
# @return [Hash] Converted tables
|
|
218
|
+
def convert_format(tables, detection)
|
|
219
|
+
source_format = detection[:format]
|
|
220
|
+
target = target_format
|
|
221
|
+
|
|
222
|
+
# No conversion needed for same format
|
|
223
|
+
return tables if source_format == target
|
|
224
|
+
|
|
225
|
+
# Use FormatConverter for outline conversion
|
|
226
|
+
if needs_outline_conversion?(source_format, target) || target == :svg
|
|
227
|
+
converter = Converters::FormatConverter.new
|
|
228
|
+
# Create temporary font object from tables
|
|
229
|
+
font = build_font_from_tables(tables, source_format)
|
|
230
|
+
converter.convert(font, target, @options)
|
|
231
|
+
else
|
|
232
|
+
# Just packaging change - tables can be used as-is
|
|
233
|
+
tables
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check if outline conversion is needed
|
|
238
|
+
#
|
|
239
|
+
# @param source [Symbol] Source format
|
|
240
|
+
# @param target [Symbol] Target format
|
|
241
|
+
# @return [Boolean] True if outline conversion needed
|
|
242
|
+
def needs_outline_conversion?(source, target)
|
|
243
|
+
# TTF ↔ OTF requires outline conversion
|
|
244
|
+
ttf_formats = %i[ttf ttc woff woff2]
|
|
245
|
+
otf_formats = %i[otf otc]
|
|
246
|
+
|
|
247
|
+
(ttf_formats.include?(source) && otf_formats.include?(target)) ||
|
|
248
|
+
(otf_formats.include?(source) && ttf_formats.include?(target))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Write output font file
|
|
252
|
+
#
|
|
253
|
+
# @param tables [Hash] Font tables
|
|
254
|
+
# @param detection [Hash] Detection results
|
|
255
|
+
def write_output(tables, _detection)
|
|
256
|
+
writer = OutputWriter.new(@output_path, target_format, @options)
|
|
257
|
+
writer.write(tables)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Validate output file
|
|
261
|
+
#
|
|
262
|
+
# @raise [ValidationError] If validation fails
|
|
263
|
+
def validate_output
|
|
264
|
+
return unless File.exist?(@output_path)
|
|
265
|
+
|
|
266
|
+
require_relative "../validation/validator"
|
|
267
|
+
|
|
268
|
+
# Load font for validation
|
|
269
|
+
font = FontLoader.load(@output_path, mode: :full)
|
|
270
|
+
validator = Validation::Validator.new
|
|
271
|
+
result = validator.validate(font, @output_path)
|
|
272
|
+
|
|
273
|
+
return if result.valid
|
|
274
|
+
|
|
275
|
+
error_messages = result.errors.map(&:message).join(", ")
|
|
276
|
+
raise Error, "Output validation failed: #{error_messages}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Get target format
|
|
280
|
+
#
|
|
281
|
+
# @return [Symbol] Target format
|
|
282
|
+
def target_format
|
|
283
|
+
@options[:target_format] || detect_target_from_extension
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Detect target format from output path extension
|
|
287
|
+
#
|
|
288
|
+
# @return [Symbol] Detected format
|
|
289
|
+
def detect_target_from_extension
|
|
290
|
+
ext = File.extname(@output_path).downcase
|
|
291
|
+
case ext
|
|
292
|
+
when ".ttf" then :ttf
|
|
293
|
+
when ".otf" then :otf
|
|
294
|
+
when ".woff" then :woff
|
|
295
|
+
when ".woff2" then :woff2
|
|
296
|
+
else
|
|
297
|
+
raise ArgumentError, "Cannot determine target format from extension: #{ext}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Get variation options for VariationResolver
|
|
302
|
+
#
|
|
303
|
+
# @return [Hash] Variation options
|
|
304
|
+
def variation_options
|
|
305
|
+
opts = {}
|
|
306
|
+
opts[:coordinates] = @options[:coordinates] if @options[:coordinates]
|
|
307
|
+
opts[:instance_index] = @options[:instance_index] if @options[:instance_index]
|
|
308
|
+
opts
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Validate input and output paths
|
|
312
|
+
#
|
|
313
|
+
# @raise [ArgumentError] If paths invalid
|
|
314
|
+
def validate_paths!
|
|
315
|
+
unless File.exist?(@input_path)
|
|
316
|
+
raise ArgumentError, "Input file not found: #{@input_path}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
output_dir = File.dirname(@output_path)
|
|
320
|
+
unless File.directory?(output_dir)
|
|
321
|
+
raise ArgumentError, "Output directory not found: #{output_dir}"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Build font object from tables
|
|
326
|
+
#
|
|
327
|
+
# @param tables [Hash] Font tables
|
|
328
|
+
# @param format [Symbol] Font format
|
|
329
|
+
# @return [Font] Font object
|
|
330
|
+
def build_font_from_tables(tables, format)
|
|
331
|
+
# Detect outline type from tables
|
|
332
|
+
has_cff = tables.key?("CFF ") || tables.key?("CFF2")
|
|
333
|
+
has_glyf = tables.key?("glyf")
|
|
334
|
+
|
|
335
|
+
if has_cff
|
|
336
|
+
OpenTypeFont.from_tables(tables)
|
|
337
|
+
elsif has_glyf
|
|
338
|
+
TrueTypeFont.from_tables(tables)
|
|
339
|
+
else
|
|
340
|
+
# Default based on format
|
|
341
|
+
case format
|
|
342
|
+
when :ttf, :woff, :woff2
|
|
343
|
+
TrueTypeFont.from_tables(tables)
|
|
344
|
+
when :otf
|
|
345
|
+
OpenTypeFont.from_tables(tables)
|
|
346
|
+
else
|
|
347
|
+
raise ArgumentError, "Cannot determine font type: format=#{format}, has_cff=#{has_cff}, has_glyf=#{has_glyf}"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Build transformation details
|
|
353
|
+
#
|
|
354
|
+
# @param detection [Hash] Detection results
|
|
355
|
+
# @return [Hash] Transformation details
|
|
356
|
+
def build_details(detection)
|
|
357
|
+
{
|
|
358
|
+
source_format: detection[:format],
|
|
359
|
+
source_variation: detection[:variation_type],
|
|
360
|
+
target_format: target_format,
|
|
361
|
+
variation_strategy: @variation_strategy,
|
|
362
|
+
variation_preserved: @variation_strategy == :preserve,
|
|
363
|
+
}
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Handle transformation error
|
|
367
|
+
#
|
|
368
|
+
# @param error [StandardError] Error that occurred
|
|
369
|
+
# @raise [Error] Re-raises with context
|
|
370
|
+
def handle_error(error)
|
|
371
|
+
log "ERROR: #{error.message}"
|
|
372
|
+
log error.backtrace.first(5).join("\n") if @options[:verbose]
|
|
373
|
+
|
|
374
|
+
raise Error, "Transformation failed: #{error.message}"
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Log message if verbose
|
|
378
|
+
#
|
|
379
|
+
# @param message [String] Message to log
|
|
380
|
+
def log(message)
|
|
381
|
+
puts "[TransformationPipeline] #{message}" if @options[:verbose]
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Default options
|
|
385
|
+
#
|
|
386
|
+
# @return [Hash] Default options
|
|
387
|
+
def default_options
|
|
388
|
+
{
|
|
389
|
+
validate: true,
|
|
390
|
+
verbose: false,
|
|
391
|
+
preserve_variation: nil, # Auto-determine
|
|
392
|
+
}
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Check if this is a same-format conversion
|
|
396
|
+
#
|
|
397
|
+
# @return [Boolean] True if source and target formats are the same
|
|
398
|
+
def same_format_conversion?
|
|
399
|
+
detection = detect_input_format
|
|
400
|
+
detection[:format] == target_format
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Check if target format is export-only (cannot be validated)
|
|
404
|
+
#
|
|
405
|
+
# @return [Boolean] True if format is export-only
|
|
406
|
+
def export_only_format?
|
|
407
|
+
%i[svg woff woff2].include?(target_format)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "strategies/base_strategy"
|
|
4
|
+
require_relative "strategies/preserve_strategy"
|
|
5
|
+
require_relative "strategies/instance_strategy"
|
|
6
|
+
require_relative "strategies/named_strategy"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Pipeline
|
|
10
|
+
# Resolves variation data using strategy pattern
|
|
11
|
+
#
|
|
12
|
+
# This class orchestrates variation resolution during font conversion by
|
|
13
|
+
# selecting and executing the appropriate strategy based on user intent.
|
|
14
|
+
# It follows the Strategy pattern to allow different approaches to handling
|
|
15
|
+
# variable font data.
|
|
16
|
+
#
|
|
17
|
+
# Three strategies are available:
|
|
18
|
+
# - PreserveStrategy: Keep variation data intact (for compatible formats)
|
|
19
|
+
# - InstanceStrategy: Generate static instance at coordinates
|
|
20
|
+
# - NamedStrategy: Use named instance from fvar table
|
|
21
|
+
#
|
|
22
|
+
# Strategy selection is explicit through the :strategy option. Each strategy
|
|
23
|
+
# has its own required and optional parameters.
|
|
24
|
+
#
|
|
25
|
+
# @example Preserve variation data
|
|
26
|
+
# resolver = VariationResolver.new(font, strategy: :preserve)
|
|
27
|
+
# tables = resolver.resolve
|
|
28
|
+
#
|
|
29
|
+
# @example Generate instance at coordinates
|
|
30
|
+
# resolver = VariationResolver.new(
|
|
31
|
+
# font,
|
|
32
|
+
# strategy: :instance,
|
|
33
|
+
# coordinates: { "wght" => 700.0 }
|
|
34
|
+
# )
|
|
35
|
+
# tables = resolver.resolve
|
|
36
|
+
#
|
|
37
|
+
# @example Use named instance
|
|
38
|
+
# resolver = VariationResolver.new(
|
|
39
|
+
# font,
|
|
40
|
+
# strategy: :named,
|
|
41
|
+
# instance_index: 0
|
|
42
|
+
# )
|
|
43
|
+
# tables = resolver.resolve
|
|
44
|
+
class VariationResolver
|
|
45
|
+
# @return [TrueTypeFont, OpenTypeFont] Font to process
|
|
46
|
+
attr_reader :font
|
|
47
|
+
|
|
48
|
+
# @return [Strategies::BaseStrategy] Selected strategy
|
|
49
|
+
attr_reader :strategy
|
|
50
|
+
|
|
51
|
+
# Initialize resolver with font and strategy
|
|
52
|
+
#
|
|
53
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to process
|
|
54
|
+
# @param options [Hash] Resolution options
|
|
55
|
+
# @option options [Symbol] :strategy Strategy to use (:preserve, :instance, :named)
|
|
56
|
+
# @option options [Hash] :coordinates Design space coordinates (for :instance)
|
|
57
|
+
# @option options [Integer] :instance_index Named instance index (for :named)
|
|
58
|
+
# @raise [ArgumentError] If strategy is missing or invalid
|
|
59
|
+
def initialize(font, options = {})
|
|
60
|
+
@font = font
|
|
61
|
+
|
|
62
|
+
strategy_type = options[:strategy]
|
|
63
|
+
raise ArgumentError, "strategy is required" unless strategy_type
|
|
64
|
+
|
|
65
|
+
@strategy = build_strategy(strategy_type, options)
|
|
66
|
+
|
|
67
|
+
# Validate strategy-specific requirements
|
|
68
|
+
validate_strategy_requirements(strategy_type, options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Resolve variation data
|
|
72
|
+
#
|
|
73
|
+
# Delegates to the selected strategy to process the font and return
|
|
74
|
+
# the appropriate tables.
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash<String, String>] Font tables after resolution
|
|
77
|
+
def resolve
|
|
78
|
+
@strategy.resolve(@font)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if resolution preserves variation data
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean] True if variation is preserved
|
|
84
|
+
def preserves_variation?
|
|
85
|
+
@strategy.preserves_variation?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get strategy name
|
|
89
|
+
#
|
|
90
|
+
# @return [Symbol] Strategy identifier
|
|
91
|
+
def strategy_name
|
|
92
|
+
@strategy.strategy_name
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Build strategy instance based on type
|
|
98
|
+
#
|
|
99
|
+
# @param type [Symbol] Strategy type (:preserve, :instance, :named)
|
|
100
|
+
# @param options [Hash] Strategy options
|
|
101
|
+
# @return [Strategies::BaseStrategy] Strategy instance
|
|
102
|
+
# @raise [ArgumentError] If strategy type is unknown
|
|
103
|
+
def build_strategy(type, options)
|
|
104
|
+
case type
|
|
105
|
+
when :preserve
|
|
106
|
+
Strategies::PreserveStrategy.new(options)
|
|
107
|
+
when :instance
|
|
108
|
+
Strategies::InstanceStrategy.new(options)
|
|
109
|
+
when :named
|
|
110
|
+
Strategies::NamedStrategy.new(options)
|
|
111
|
+
else
|
|
112
|
+
raise ArgumentError,
|
|
113
|
+
"Unknown strategy: #{type}. " \
|
|
114
|
+
"Valid strategies: :preserve, :instance, :named"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Validate strategy-specific requirements
|
|
119
|
+
#
|
|
120
|
+
# @param type [Symbol] Strategy type
|
|
121
|
+
# @param options [Hash] Strategy options
|
|
122
|
+
# @raise [ArgumentError, InvalidCoordinatesError] If validation fails
|
|
123
|
+
def validate_strategy_requirements(type, options)
|
|
124
|
+
case type
|
|
125
|
+
when :instance
|
|
126
|
+
validate_instance_coordinates(options[:coordinates]) if options[:coordinates]
|
|
127
|
+
when :named
|
|
128
|
+
validate_named_instance_index(options[:instance_index]) if options[:instance_index]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Validate coordinates for instance strategy
|
|
133
|
+
#
|
|
134
|
+
# @param coordinates [Hash] Coordinates to validate
|
|
135
|
+
# @raise [InvalidCoordinatesError] If coordinates invalid
|
|
136
|
+
def validate_instance_coordinates(coordinates)
|
|
137
|
+
return if coordinates.empty?
|
|
138
|
+
|
|
139
|
+
require_relative "../variation/variation_context"
|
|
140
|
+
context = Variation::VariationContext.new(@font)
|
|
141
|
+
context.validate_coordinates(coordinates)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Validate instance index for named strategy
|
|
145
|
+
#
|
|
146
|
+
# @param instance_index [Integer] Instance index to validate
|
|
147
|
+
# @raise [ArgumentError] If index invalid
|
|
148
|
+
def validate_named_instance_index(instance_index)
|
|
149
|
+
require_relative "../variation/variation_context"
|
|
150
|
+
context = Variation::VariationContext.new(@font)
|
|
151
|
+
|
|
152
|
+
unless context.fvar
|
|
153
|
+
raise ArgumentError, "Font is not a variable font (no fvar table)"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
instances = context.fvar.instances
|
|
157
|
+
if instance_index.negative? || instance_index >= instances.length
|
|
158
|
+
raise ArgumentError,
|
|
159
|
+
"Invalid instance index #{instance_index}. " \
|
|
160
|
+
"Font has #{instances.length} named instances."
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|