fontisan 0.2.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 +270 -131
- data/README.adoc +158 -4
- data/Rakefile +44 -47
- data/lib/fontisan/cli.rb +84 -33
- data/lib/fontisan/collection/builder.rb +81 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +16 -0
- data/lib/fontisan/commands/convert_command.rb +97 -170
- data/lib/fontisan/commands/instance_command.rb +71 -80
- data/lib/fontisan/commands/validate_command.rb +25 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +10 -0
- data/lib/fontisan/converters/format_converter.rb +150 -1
- data/lib/fontisan/converters/outline_converter.rb +80 -18
- data/lib/fontisan/converters/woff_writer.rb +1 -1
- data/lib/fontisan/font_loader.rb +3 -5
- data/lib/fontisan/font_writer.rb +7 -6
- data/lib/fontisan/hints/hint_converter.rb +133 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +221 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +100 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +127 -0
- data/lib/fontisan/loading_modes.rb +2 -0
- data/lib/fontisan/models/font_export.rb +2 -2
- data/lib/fontisan/models/hint.rb +173 -1
- data/lib/fontisan/models/validation_report.rb +1 -1
- data/lib/fontisan/open_type_font.rb +25 -9
- data/lib/fontisan/open_type_font_extensions.rb +54 -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/tables/cff/charstring.rb +33 -4
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -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/dict_builder.rb +15 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -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.rb +2 -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 +9 -4
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/true_type_font.rb +24 -9
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/converter.rb +120 -13
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -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/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +475 -470
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +12 -0
- metadata +31 -2
|
@@ -15,6 +15,12 @@ module Fontisan
|
|
|
15
15
|
# sharing_map = deduplicator.build_sharing_map
|
|
16
16
|
# canonical_tables = deduplicator.canonical_tables
|
|
17
17
|
class TableDeduplicator
|
|
18
|
+
# Tables that can be shared in variable font collections if identical
|
|
19
|
+
VARIATION_SHAREABLE_TABLES = %w[fvar avar STAT HVAR VVAR MVAR].freeze
|
|
20
|
+
|
|
21
|
+
# Tables that must remain font-specific in variable fonts
|
|
22
|
+
VARIATION_FONT_SPECIFIC_TABLES = %w[gvar CFF2].freeze
|
|
23
|
+
|
|
18
24
|
# Canonical tables (unique table data)
|
|
19
25
|
# @return [Hash<String, Hash>] Map of table tag to canonical versions
|
|
20
26
|
attr_reader :canonical_tables
|
|
@@ -62,6 +68,9 @@ module Fontisan
|
|
|
62
68
|
# First pass: collect all unique tables
|
|
63
69
|
collect_canonical_tables
|
|
64
70
|
|
|
71
|
+
# Handle variable font table deduplication
|
|
72
|
+
deduplicate_variation_tables if has_variable_fonts?
|
|
73
|
+
|
|
65
74
|
# Second pass: build sharing map for each font
|
|
66
75
|
build_font_sharing_references
|
|
67
76
|
|
|
@@ -115,6 +124,73 @@ module Fontisan
|
|
|
115
124
|
|
|
116
125
|
private
|
|
117
126
|
|
|
127
|
+
# Check if any font is a variable font
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] true if any font has fvar table
|
|
130
|
+
def has_variable_fonts?
|
|
131
|
+
@fonts.any? { |font| font.has_table?("fvar") }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Deduplicate variable font tables
|
|
135
|
+
#
|
|
136
|
+
# Handles special logic for variable font tables:
|
|
137
|
+
# - Share tables that are identical across fonts (fvar, avar, etc.)
|
|
138
|
+
# - Keep font-specific tables separate (gvar, CFF2)
|
|
139
|
+
#
|
|
140
|
+
# @return [void]
|
|
141
|
+
def deduplicate_variation_tables
|
|
142
|
+
# Share tables that are identical across fonts
|
|
143
|
+
VARIATION_SHAREABLE_TABLES.each do |tag|
|
|
144
|
+
share_if_identical(tag)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Never share font-specific variation tables
|
|
148
|
+
VARIATION_FONT_SPECIFIC_TABLES.each do |tag|
|
|
149
|
+
keep_separate(tag)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Share table if identical across all fonts that have it
|
|
154
|
+
#
|
|
155
|
+
# @param tag [String] Table tag
|
|
156
|
+
# @return [void]
|
|
157
|
+
def share_if_identical(tag)
|
|
158
|
+
# Get all instances of this table
|
|
159
|
+
tables = @fonts.map { |f| f.table_data[tag] }.compact
|
|
160
|
+
return if tables.empty?
|
|
161
|
+
|
|
162
|
+
# Check if all instances are identical
|
|
163
|
+
nil if tables.uniq.length > 1
|
|
164
|
+
|
|
165
|
+
# All instances are identical, mark as shareable
|
|
166
|
+
# The normal deduplication logic will handle this
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Ensure table stays separate for each font
|
|
170
|
+
#
|
|
171
|
+
# @param tag [String] Table tag
|
|
172
|
+
# @return [void]
|
|
173
|
+
def keep_separate(tag)
|
|
174
|
+
# Mark each font's instance as non-shareable
|
|
175
|
+
@fonts.each_with_index do |font, _font_index|
|
|
176
|
+
next unless font.has_table?(tag)
|
|
177
|
+
|
|
178
|
+
# Find this font's canonical table for this tag
|
|
179
|
+
table_data = font.table_data[tag]
|
|
180
|
+
checksum = calculate_checksum(table_data)
|
|
181
|
+
|
|
182
|
+
# Ensure canonical table exists
|
|
183
|
+
@canonical_tables[tag] ||= {}
|
|
184
|
+
canonical_id = @checksum_to_canonical.dig(tag, checksum)
|
|
185
|
+
|
|
186
|
+
next unless canonical_id && @canonical_tables[tag][canonical_id]
|
|
187
|
+
|
|
188
|
+
# Mark as non-shareable
|
|
189
|
+
@canonical_tables[tag][canonical_id][:shared] = false
|
|
190
|
+
@canonical_tables[tag][canonical_id][:font_specific] = true
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
118
194
|
# Collect all unique (canonical) tables across all fonts
|
|
119
195
|
#
|
|
120
196
|
# Identifies unique table content based on checksum and stores one
|
|
@@ -66,6 +66,22 @@ module Fontisan
|
|
|
66
66
|
# @raise [InvalidFontError] for corrupted or unknown formats
|
|
67
67
|
# @raise [Error] for other loading failures
|
|
68
68
|
def load_font
|
|
69
|
+
# BaseCommand is for inspection - reject compressed formats first
|
|
70
|
+
# Check file signature before attempting to load
|
|
71
|
+
File.open(@font_path, "rb") do |io|
|
|
72
|
+
signature = io.read(4)
|
|
73
|
+
|
|
74
|
+
if signature == "wOFF"
|
|
75
|
+
raise UnsupportedFormatError,
|
|
76
|
+
"Unsupported font format: WOFF files must be decompressed first. " \
|
|
77
|
+
"Use ConvertCommand to convert WOFF to TTF/OTF."
|
|
78
|
+
elsif signature == "wOF2"
|
|
79
|
+
raise UnsupportedFormatError,
|
|
80
|
+
"Unsupported font format: WOFF2 files must be decompressed first. " \
|
|
81
|
+
"Use ConvertCommand to convert WOFF2 to TTF/OTF."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
69
85
|
# ConvertCommand and similar commands need all tables loaded upfront
|
|
70
86
|
# Use mode and lazy from options, or sensible defaults
|
|
71
87
|
FontLoader.load(
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base_command"
|
|
4
|
-
require_relative "../
|
|
5
|
-
require_relative "../font_writer"
|
|
4
|
+
require_relative "../pipeline/transformation_pipeline"
|
|
6
5
|
|
|
7
6
|
module Fontisan
|
|
8
7
|
module Commands
|
|
9
8
|
# Command for converting fonts between formats
|
|
10
9
|
#
|
|
11
10
|
# [`ConvertCommand`](lib/fontisan/commands/convert_command.rb) provides
|
|
12
|
-
# CLI interface for font format conversion operations
|
|
11
|
+
# CLI interface for font format conversion operations using the universal
|
|
12
|
+
# transformation pipeline. It supports:
|
|
13
13
|
# - Same-format operations (copy/optimize)
|
|
14
|
-
# - TTF ↔ OTF outline format conversion
|
|
15
|
-
# -
|
|
14
|
+
# - TTF ↔ OTF outline format conversion
|
|
15
|
+
# - Variable font operations (preserve/instance generation)
|
|
16
|
+
# - WOFF/WOFF2 compression
|
|
16
17
|
#
|
|
17
|
-
# The command uses [`
|
|
18
|
+
# The command uses [`TransformationPipeline`](lib/fontisan/pipeline/transformation_pipeline.rb)
|
|
18
19
|
# to orchestrate conversions with appropriate strategies.
|
|
19
20
|
#
|
|
20
21
|
# @example Convert TTF to OTF
|
|
@@ -25,11 +26,12 @@ module Fontisan
|
|
|
25
26
|
# )
|
|
26
27
|
# command.run
|
|
27
28
|
#
|
|
28
|
-
# @example
|
|
29
|
+
# @example Generate instance at coordinates
|
|
29
30
|
# command = ConvertCommand.new(
|
|
30
|
-
# '
|
|
31
|
+
# 'variable.ttf',
|
|
31
32
|
# to: 'ttf',
|
|
32
|
-
# output: '
|
|
33
|
+
# output: 'bold.ttf',
|
|
34
|
+
# coordinates: 'wght=700,wdth=100'
|
|
33
35
|
# )
|
|
34
36
|
# command.run
|
|
35
37
|
class ConvertCommand < BaseCommand
|
|
@@ -37,24 +39,34 @@ module Fontisan
|
|
|
37
39
|
#
|
|
38
40
|
# @param font_path [String] Path to input font file
|
|
39
41
|
# @param options [Hash] Conversion options
|
|
40
|
-
# @option options [String] :to Target format (ttf, otf,
|
|
42
|
+
# @option options [String] :to Target format (ttf, otf, woff, woff2)
|
|
41
43
|
# @option options [String] :output Output file path (required)
|
|
42
44
|
# @option options [Integer] :font_index Index for TTC/OTC (default: 0)
|
|
43
|
-
# @option options [
|
|
44
|
-
# @option options [
|
|
45
|
-
# @option options [Integer] :
|
|
46
|
-
# @option options [Boolean] :
|
|
45
|
+
# @option options [String] :coordinates Coordinate string (e.g., "wght=700,wdth=100")
|
|
46
|
+
# @option options [Hash] :instance_coordinates Axis coordinates hash (e.g., {"wght" => 700.0})
|
|
47
|
+
# @option options [Integer] :instance_index Named instance index
|
|
48
|
+
# @option options [Boolean] :preserve_variation Preserve variation data (default: auto)
|
|
49
|
+
# @option options [Boolean] :preserve_hints Preserve rendering hints (default: false)
|
|
50
|
+
# @option options [Boolean] :no_validate Skip output validation
|
|
51
|
+
# @option options [Boolean] :verbose Verbose output
|
|
47
52
|
def initialize(font_path, options = {})
|
|
48
53
|
super(font_path, options)
|
|
49
|
-
@target_format = parse_target_format(options[:to])
|
|
50
54
|
@output_path = options[:output]
|
|
51
|
-
@converter = Converters::FormatConverter.new
|
|
52
55
|
|
|
53
|
-
#
|
|
54
|
-
@
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@
|
|
56
|
+
# Parse target format
|
|
57
|
+
@target_format = parse_target_format(options[:to])
|
|
58
|
+
|
|
59
|
+
# Parse coordinates if string provided
|
|
60
|
+
@coordinates = if options[:coordinates]
|
|
61
|
+
parse_coordinates(options[:coordinates])
|
|
62
|
+
elsif options[:instance_coordinates]
|
|
63
|
+
options[:instance_coordinates]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@instance_index = options[:instance_index]
|
|
67
|
+
@preserve_variation = options[:preserve_variation]
|
|
68
|
+
@preserve_hints = options.fetch(:preserve_hints, false)
|
|
69
|
+
@validate = !options[:no_validate]
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
# Execute the conversion
|
|
@@ -65,65 +77,59 @@ module Fontisan
|
|
|
65
77
|
def run
|
|
66
78
|
validate_options!
|
|
67
79
|
|
|
68
|
-
puts "Converting #{File.basename(font_path)} to #{@target_format}..."
|
|
80
|
+
puts "Converting #{File.basename(font_path)} to #{@target_format}..." unless @options[:quiet]
|
|
69
81
|
|
|
70
|
-
# Build
|
|
71
|
-
|
|
82
|
+
# Build pipeline options
|
|
83
|
+
pipeline_options = {
|
|
72
84
|
target_format: @target_format,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
max_subroutines: @max_subroutines,
|
|
76
|
-
optimize_ordering: @optimize_ordering,
|
|
77
|
-
verbose: options[:verbose],
|
|
85
|
+
validate: @validate,
|
|
86
|
+
verbose: @options[:verbose],
|
|
78
87
|
}
|
|
79
88
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
# Add variation options if specified
|
|
90
|
+
pipeline_options[:coordinates] = @coordinates if @coordinates
|
|
91
|
+
pipeline_options[:instance_index] = @instance_index if @instance_index
|
|
92
|
+
pipeline_options[:preserve_variation] = @preserve_variation unless @preserve_variation.nil?
|
|
93
|
+
|
|
94
|
+
# Add hint preservation option
|
|
95
|
+
pipeline_options[:preserve_hints] = @preserve_hints if @preserve_hints
|
|
96
|
+
|
|
97
|
+
# Use TransformationPipeline for universal conversion
|
|
98
|
+
pipeline = Pipeline::TransformationPipeline.new(
|
|
99
|
+
font_path,
|
|
100
|
+
@output_path,
|
|
101
|
+
pipeline_options,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
result = pipeline.transform
|
|
105
|
+
|
|
106
|
+
# Display results
|
|
107
|
+
unless @options[:quiet]
|
|
108
|
+
output_size = File.size(@output_path)
|
|
109
|
+
input_size = File.size(font_path)
|
|
110
|
+
|
|
111
|
+
puts "Conversion complete!"
|
|
112
|
+
puts " Input: #{font_path} (#{format_size(input_size)})"
|
|
113
|
+
puts " Output: #{@output_path} (#{format_size(output_size)})"
|
|
114
|
+
puts " Format: #{result[:details][:source_format]} → #{result[:details][:target_format]}"
|
|
115
|
+
|
|
116
|
+
if result[:details][:variation_preserved]
|
|
117
|
+
puts " Variation: Preserved (#{result[:details][:variation_strategy]})"
|
|
118
|
+
elsif result[:details][:variation_strategy] != :preserve
|
|
119
|
+
puts " Variation: Instance generated (#{result[:details][:variation_strategy]})"
|
|
120
|
+
end
|
|
104
121
|
end
|
|
105
122
|
|
|
106
|
-
output_size = File.size(@output_path)
|
|
107
|
-
input_size = File.size(font_path)
|
|
108
|
-
|
|
109
|
-
puts "Conversion complete!"
|
|
110
|
-
puts " Input: #{font_path} (#{format_size(input_size)})"
|
|
111
|
-
puts " Output: #{@output_path} (#{format_size(output_size)})"
|
|
112
|
-
|
|
113
123
|
{
|
|
114
124
|
success: true,
|
|
115
125
|
input_path: font_path,
|
|
116
126
|
output_path: @output_path,
|
|
117
|
-
source_format:
|
|
118
|
-
target_format:
|
|
119
|
-
input_size:
|
|
120
|
-
output_size:
|
|
127
|
+
source_format: result[:details][:source_format],
|
|
128
|
+
target_format: result[:details][:target_format],
|
|
129
|
+
input_size: File.size(font_path),
|
|
130
|
+
output_size: File.size(@output_path),
|
|
131
|
+
variation_strategy: result[:details][:variation_strategy],
|
|
121
132
|
}
|
|
122
|
-
rescue NotImplementedError
|
|
123
|
-
# Let NotImplementedError propagate for tests that expect it
|
|
124
|
-
raise
|
|
125
|
-
rescue Converters::ConversionStrategy => e
|
|
126
|
-
handle_conversion_error(e)
|
|
127
133
|
rescue ArgumentError
|
|
128
134
|
# Let ArgumentError propagate for validation errors
|
|
129
135
|
raise
|
|
@@ -131,26 +137,27 @@ module Fontisan
|
|
|
131
137
|
raise Error, "Conversion failed: #{e.message}"
|
|
132
138
|
end
|
|
133
139
|
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
# @return [Array<Hash>] List of supported conversions
|
|
137
|
-
def self.supported_conversions
|
|
138
|
-
converter = Converters::FormatConverter.new
|
|
139
|
-
converter.all_conversions
|
|
140
|
-
end
|
|
140
|
+
private
|
|
141
141
|
|
|
142
|
-
#
|
|
142
|
+
# Parse coordinates string to hash
|
|
143
|
+
#
|
|
144
|
+
# Parses strings like "wght=700,wdth=100" into {"wght" => 700.0, "wdth" => 100.0}
|
|
143
145
|
#
|
|
144
|
-
# @param
|
|
145
|
-
# @
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
# @param coord_string [String] Coordinate string
|
|
147
|
+
# @return [Hash] Parsed coordinates
|
|
148
|
+
def parse_coordinates(coord_string)
|
|
149
|
+
coords = {}
|
|
150
|
+
coord_string.split(",").each do |pair|
|
|
151
|
+
key, value = pair.split("=")
|
|
152
|
+
next unless key && value
|
|
153
|
+
|
|
154
|
+
coords[key.strip] = value.to_f
|
|
155
|
+
end
|
|
156
|
+
coords
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
raise ArgumentError, "Invalid coordinates format '#{coord_string}': #{e.message}"
|
|
150
159
|
end
|
|
151
160
|
|
|
152
|
-
private
|
|
153
|
-
|
|
154
161
|
# Validate command options
|
|
155
162
|
#
|
|
156
163
|
# @raise [ArgumentError] If required options are missing
|
|
@@ -164,18 +171,6 @@ module Fontisan
|
|
|
164
171
|
raise ArgumentError,
|
|
165
172
|
"Target format is required. Use --to option."
|
|
166
173
|
end
|
|
167
|
-
|
|
168
|
-
# Check if conversion is supported
|
|
169
|
-
source_format = detect_source_format
|
|
170
|
-
unless @converter.supported?(source_format, @target_format)
|
|
171
|
-
available = @converter.supported_targets(source_format)
|
|
172
|
-
message = "Conversion from #{source_format} to #{@target_format} " \
|
|
173
|
-
"is not supported."
|
|
174
|
-
if available.any?
|
|
175
|
-
message += " Available targets: #{available.join(', ')}"
|
|
176
|
-
end
|
|
177
|
-
raise ArgumentError, message
|
|
178
|
-
end
|
|
179
174
|
end
|
|
180
175
|
|
|
181
176
|
# Parse target format from string/symbol
|
|
@@ -191,46 +186,17 @@ module Fontisan
|
|
|
191
186
|
:ttf
|
|
192
187
|
when "otf", "opentype", "cff"
|
|
193
188
|
:otf
|
|
189
|
+
when "svg"
|
|
190
|
+
:svg
|
|
194
191
|
when "woff"
|
|
195
|
-
|
|
192
|
+
raise ArgumentError,
|
|
193
|
+
"WOFF format conversion is not supported yet. Use woff2 instead."
|
|
196
194
|
when "woff2"
|
|
197
195
|
:woff2
|
|
198
|
-
when "svg"
|
|
199
|
-
:svg
|
|
200
196
|
else
|
|
201
197
|
raise ArgumentError,
|
|
202
198
|
"Unknown target format: #{format}. " \
|
|
203
|
-
"Supported: ttf, otf,
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# Detect source font format
|
|
208
|
-
#
|
|
209
|
-
# @return [Symbol] Source format
|
|
210
|
-
def detect_source_format
|
|
211
|
-
# Check for CFF/CFF2 tables (OpenType/CFF)
|
|
212
|
-
if font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
213
|
-
:otf
|
|
214
|
-
# Check for glyf table (TrueType)
|
|
215
|
-
elsif font.has_table?("glyf")
|
|
216
|
-
:ttf
|
|
217
|
-
else
|
|
218
|
-
:unknown
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
# Determine sfnt version for target format
|
|
223
|
-
#
|
|
224
|
-
# @param format [Symbol] Target format
|
|
225
|
-
# @return [Integer] sfnt version
|
|
226
|
-
def determine_sfnt_version(format)
|
|
227
|
-
case format
|
|
228
|
-
when :otf
|
|
229
|
-
0x4F54544F # 'OTTO' for OpenType/CFF
|
|
230
|
-
when :ttf
|
|
231
|
-
0x00010000 # 1.0 for TrueType
|
|
232
|
-
else
|
|
233
|
-
0x00010000 # Default to TrueType
|
|
199
|
+
"Supported: ttf, otf, svg, woff2"
|
|
234
200
|
end
|
|
235
201
|
end
|
|
236
202
|
|
|
@@ -247,45 +213,6 @@ module Fontisan
|
|
|
247
213
|
"#{(bytes / (1024.0 * 1024)).round(1)} MB"
|
|
248
214
|
end
|
|
249
215
|
end
|
|
250
|
-
|
|
251
|
-
# Handle conversion errors with helpful messages
|
|
252
|
-
#
|
|
253
|
-
# @param error [StandardError] The error that occurred
|
|
254
|
-
# @raise [Error] Wrapped error with helpful message
|
|
255
|
-
def handle_conversion_error(error)
|
|
256
|
-
message = "Conversion failed: #{error.message}"
|
|
257
|
-
|
|
258
|
-
# Add helpful hints based on error type
|
|
259
|
-
if error.is_a?(NotImplementedError)
|
|
260
|
-
message += "\n\nNote: Some conversions are not yet fully " \
|
|
261
|
-
"implemented. Check the conversion matrix configuration " \
|
|
262
|
-
"for implementation status."
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
raise Error, message
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Display optimization results from subroutine generation
|
|
269
|
-
#
|
|
270
|
-
# @param tables [Hash] Table data with optimization metadata
|
|
271
|
-
def display_optimization_results(tables)
|
|
272
|
-
optimization = tables.instance_variable_get(:@subroutine_optimization)
|
|
273
|
-
return unless optimization
|
|
274
|
-
|
|
275
|
-
puts "\n=== Subroutine Optimization Results ==="
|
|
276
|
-
puts " Patterns found: #{optimization[:pattern_count]}"
|
|
277
|
-
puts " Patterns selected: #{optimization[:selected_count]}"
|
|
278
|
-
puts " Subroutines generated: #{optimization[:local_subrs].length}"
|
|
279
|
-
puts " Estimated bytes saved: #{optimization[:savings]}"
|
|
280
|
-
puts " CFF bias: #{optimization[:bias]}"
|
|
281
|
-
|
|
282
|
-
if optimization[:selected_count].zero?
|
|
283
|
-
puts " Note: No beneficial patterns found for optimization"
|
|
284
|
-
elsif optimization[:savings].positive?
|
|
285
|
-
savings_kb = (optimization[:savings] / 1024.0).round(1)
|
|
286
|
-
puts " Estimated space savings: #{savings_kb} KB"
|
|
287
|
-
end
|
|
288
|
-
end
|
|
289
216
|
end
|
|
290
217
|
end
|
|
291
218
|
end
|