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,77 @@
|
|
|
1
|
+
# WOFF2 Encoding Configuration
|
|
2
|
+
#
|
|
3
|
+
# This file defines settings for WOFF2 font encoding, including
|
|
4
|
+
# Brotli compression parameters and table transformation options.
|
|
5
|
+
#
|
|
6
|
+
# Reference: https://www.w3.org/TR/WOFF2/
|
|
7
|
+
|
|
8
|
+
# Brotli compression settings
|
|
9
|
+
brotli:
|
|
10
|
+
# Compression quality level (0-11)
|
|
11
|
+
# Higher values give better compression but slower encoding
|
|
12
|
+
# Quality 11 is recommended for production web fonts
|
|
13
|
+
quality: 11
|
|
14
|
+
|
|
15
|
+
# Compression mode
|
|
16
|
+
# :font - Optimized for font data (recommended)
|
|
17
|
+
# :text - Optimized for text data
|
|
18
|
+
# :generic - General purpose compression
|
|
19
|
+
mode: font
|
|
20
|
+
|
|
21
|
+
# Table transformation settings
|
|
22
|
+
transformations:
|
|
23
|
+
# Enable table transformations for better compression
|
|
24
|
+
# For Phase 2 Milestone 2.1: disabled (architecture in place)
|
|
25
|
+
# Future milestones will enable these for optimization
|
|
26
|
+
enabled: false
|
|
27
|
+
|
|
28
|
+
# Transform glyf and loca tables
|
|
29
|
+
# Combines glyf and loca into single stream with delta encoding
|
|
30
|
+
# Typical compression improvement: 20-30%
|
|
31
|
+
glyf_loca: false
|
|
32
|
+
|
|
33
|
+
# Transform hmtx table
|
|
34
|
+
# Compact representation of horizontal metrics
|
|
35
|
+
# Typical compression improvement: 10-20%
|
|
36
|
+
hmtx: false
|
|
37
|
+
|
|
38
|
+
# Metadata settings
|
|
39
|
+
metadata:
|
|
40
|
+
# Include font metadata in WOFF2 file
|
|
41
|
+
include: false
|
|
42
|
+
|
|
43
|
+
# Metadata compression
|
|
44
|
+
compress: true
|
|
45
|
+
|
|
46
|
+
# Private data settings
|
|
47
|
+
private_data:
|
|
48
|
+
# Include private data block
|
|
49
|
+
include: false
|
|
50
|
+
|
|
51
|
+
# Validation settings
|
|
52
|
+
validation:
|
|
53
|
+
# Verify checksums after encoding
|
|
54
|
+
verify_checksums: true
|
|
55
|
+
|
|
56
|
+
# Validate WOFF2 structure
|
|
57
|
+
validate_structure: true
|
|
58
|
+
|
|
59
|
+
# Optimization settings
|
|
60
|
+
optimization:
|
|
61
|
+
# Reorder tables for optimal compression
|
|
62
|
+
# Tables are sorted by tag for consistency
|
|
63
|
+
reorder_tables: true
|
|
64
|
+
|
|
65
|
+
# Remove unnecessary padding
|
|
66
|
+
minimize_padding: true
|
|
67
|
+
|
|
68
|
+
# Logging settings
|
|
69
|
+
logging:
|
|
70
|
+
# Log compression ratios
|
|
71
|
+
log_compression: true
|
|
72
|
+
|
|
73
|
+
# Log table sizes
|
|
74
|
+
log_table_sizes: false
|
|
75
|
+
|
|
76
|
+
# Verbose output
|
|
77
|
+
verbose: false
|
data/lib/fontisan/constants.rb
CHANGED
|
@@ -30,6 +30,15 @@ module Fontisan
|
|
|
30
30
|
# the checksum adjustment field.
|
|
31
31
|
HEAD_TAG = "head"
|
|
32
32
|
|
|
33
|
+
# Hhea table tag identifier (Horizontal Header)
|
|
34
|
+
HHEA_TAG = "hhea"
|
|
35
|
+
|
|
36
|
+
# Hmtx table tag identifier (Horizontal Metrics)
|
|
37
|
+
HMTX_TAG = "hmtx"
|
|
38
|
+
|
|
39
|
+
# Maxp table tag identifier (Maximum Profile)
|
|
40
|
+
MAXP_TAG = "maxp"
|
|
41
|
+
|
|
33
42
|
# Name table tag identifier
|
|
34
43
|
NAME_TAG = "name"
|
|
35
44
|
|
|
@@ -60,6 +69,24 @@ module Fontisan
|
|
|
60
69
|
# Fvar table tag identifier (Font Variations)
|
|
61
70
|
FVAR_TAG = "fvar"
|
|
62
71
|
|
|
72
|
+
# Gvar table tag identifier (Glyph Variations for TrueType)
|
|
73
|
+
GVAR_TAG = "gvar"
|
|
74
|
+
|
|
75
|
+
# HVAR table tag identifier (Horizontal Metrics Variations)
|
|
76
|
+
HVAR_TAG = "HVAR"
|
|
77
|
+
|
|
78
|
+
# MVAR table tag identifier (Metrics Variations)
|
|
79
|
+
MVAR_TAG = "MVAR"
|
|
80
|
+
|
|
81
|
+
# VVAR table tag identifier (Vertical Metrics Variations)
|
|
82
|
+
VVAR_TAG = "VVAR"
|
|
83
|
+
|
|
84
|
+
# Cvar table tag identifier (CVT Variations)
|
|
85
|
+
CVAR_TAG = "cvar"
|
|
86
|
+
|
|
87
|
+
# CFF2 table tag identifier (CFF version 2 with variations)
|
|
88
|
+
CFF2_TAG = "CFF2"
|
|
89
|
+
|
|
63
90
|
# Magic number used for font file checksum adjustment calculation.
|
|
64
91
|
# This constant is used in conjunction with the file checksum to compute
|
|
65
92
|
# the checksumAdjustment value stored in the 'head' table.
|
|
@@ -74,5 +101,47 @@ module Fontisan
|
|
|
74
101
|
# All table data in TTF files must be aligned to 4-byte boundaries,
|
|
75
102
|
# with padding added as necessary.
|
|
76
103
|
TABLE_ALIGNMENT = 4
|
|
104
|
+
|
|
105
|
+
# Common font subfamily names for string interning
|
|
106
|
+
#
|
|
107
|
+
# These strings are frozen and reused to reduce memory allocations
|
|
108
|
+
# when parsing fonts with common subfamily names.
|
|
109
|
+
STRING_POOL = {
|
|
110
|
+
"Regular" => "Regular".freeze,
|
|
111
|
+
"Bold" => "Bold".freeze,
|
|
112
|
+
"Italic" => "Italic".freeze,
|
|
113
|
+
"Bold Italic" => "Bold Italic".freeze,
|
|
114
|
+
"BoldItalic" => "BoldItalic".freeze,
|
|
115
|
+
"Light" => "Light".freeze,
|
|
116
|
+
"Medium" => "Medium".freeze,
|
|
117
|
+
"Semibold" => "Semibold".freeze,
|
|
118
|
+
"SemiBold" => "SemiBold".freeze,
|
|
119
|
+
"Black" => "Black".freeze,
|
|
120
|
+
"Thin" => "Thin".freeze,
|
|
121
|
+
"ExtraLight" => "ExtraLight".freeze,
|
|
122
|
+
"Extra Light" => "Extra Light".freeze,
|
|
123
|
+
"ExtraBold" => "ExtraBold".freeze,
|
|
124
|
+
"Extra Bold" => "Extra Bold".freeze,
|
|
125
|
+
"Heavy" => "Heavy".freeze,
|
|
126
|
+
"Book" => "Book".freeze,
|
|
127
|
+
"Roman" => "Roman".freeze,
|
|
128
|
+
"Normal" => "Normal".freeze,
|
|
129
|
+
"Oblique" => "Oblique".freeze,
|
|
130
|
+
"Light Italic" => "Light Italic".freeze,
|
|
131
|
+
"Medium Italic" => "Medium Italic".freeze,
|
|
132
|
+
"Semibold Italic" => "Semibold Italic".freeze,
|
|
133
|
+
"Bold Oblique" => "Bold Oblique".freeze,
|
|
134
|
+
}.freeze
|
|
135
|
+
|
|
136
|
+
# Intern a string using the string pool
|
|
137
|
+
#
|
|
138
|
+
# If the string is in the pool, returns the pooled instance.
|
|
139
|
+
# Otherwise, freezes and returns the original string.
|
|
140
|
+
#
|
|
141
|
+
# @param str [String] The string to intern
|
|
142
|
+
# @return [String] The interned string
|
|
143
|
+
def self.intern_string(str)
|
|
144
|
+
STRING_POOL[str] || str.freeze
|
|
145
|
+
end
|
|
77
146
|
end
|
|
78
147
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Converters
|
|
5
|
+
# Interface module for font format conversion strategies
|
|
6
|
+
#
|
|
7
|
+
# [`ConversionStrategy`](lib/fontisan/converters/conversion_strategy.rb)
|
|
8
|
+
# defines the contract that all conversion strategy classes must implement.
|
|
9
|
+
# This follows the Strategy pattern to enable polymorphic handling of
|
|
10
|
+
# different conversion types (TTF→OTF, OTF→TTF, same-format copying).
|
|
11
|
+
#
|
|
12
|
+
# Each strategy must implement:
|
|
13
|
+
# - convert(font, options) - Perform the actual conversion
|
|
14
|
+
# - supported_conversions - Return array of [source, target] format pairs
|
|
15
|
+
# - validate(font, target_format) - Validate conversion is possible
|
|
16
|
+
#
|
|
17
|
+
# Strategies are selected by [`FormatConverter`](lib/fontisan/converters/format_converter.rb)
|
|
18
|
+
# based on source and target formats.
|
|
19
|
+
#
|
|
20
|
+
# @example Implementing a strategy
|
|
21
|
+
# class MyStrategy
|
|
22
|
+
# include Fontisan::Converters::ConversionStrategy
|
|
23
|
+
#
|
|
24
|
+
# def convert(font, options = {})
|
|
25
|
+
# # Perform conversion
|
|
26
|
+
# tables = {...}
|
|
27
|
+
# tables
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# def supported_conversions
|
|
31
|
+
# [[:ttf, :otf], [:otf, :ttf]]
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# def validate(font, target_format)
|
|
35
|
+
# # Validate font can be converted
|
|
36
|
+
# raise Error unless valid
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
module ConversionStrategy
|
|
40
|
+
# Convert font to target format
|
|
41
|
+
#
|
|
42
|
+
# This method must return a hash of table tags to binary data,
|
|
43
|
+
# which will be assembled into a complete font by FontWriter.
|
|
44
|
+
#
|
|
45
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
46
|
+
# @param options [Hash] Conversion options
|
|
47
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
48
|
+
# @raise [NotImplementedError] If not implemented by strategy
|
|
49
|
+
def convert(font, options = {})
|
|
50
|
+
raise NotImplementedError,
|
|
51
|
+
"#{self.class.name} must implement convert(font, options)"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get list of supported conversions
|
|
55
|
+
#
|
|
56
|
+
# Returns an array of [source_format, target_format] pairs that
|
|
57
|
+
# this strategy can handle.
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Array<Symbol>>] Supported conversion pairs
|
|
60
|
+
# @raise [NotImplementedError] If not implemented by strategy
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# strategy.supported_conversions
|
|
64
|
+
# # => [[:ttf, :otf], [:otf, :ttf]]
|
|
65
|
+
def supported_conversions
|
|
66
|
+
raise NotImplementedError,
|
|
67
|
+
"#{self.class.name} must implement supported_conversions"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Validate that conversion is possible
|
|
71
|
+
#
|
|
72
|
+
# Checks if the given font can be converted to the target format.
|
|
73
|
+
# Should raise an error with a clear message if conversion is not
|
|
74
|
+
# possible.
|
|
75
|
+
#
|
|
76
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to validate
|
|
77
|
+
# @param target_format [Symbol] Target format (:ttf, :otf, etc.)
|
|
78
|
+
# @return [Boolean] True if valid
|
|
79
|
+
# @raise [Error] If conversion is not possible
|
|
80
|
+
# @raise [NotImplementedError] If not implemented by strategy
|
|
81
|
+
def validate(font, target_format)
|
|
82
|
+
raise NotImplementedError,
|
|
83
|
+
"#{self.class.name} must implement validate(font, target_format)"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if strategy supports a conversion
|
|
87
|
+
#
|
|
88
|
+
# @param source_format [Symbol] Source format
|
|
89
|
+
# @param target_format [Symbol] Target format
|
|
90
|
+
# @return [Boolean] True if supported
|
|
91
|
+
def supports?(source_format, target_format)
|
|
92
|
+
supported_conversions.include?([source_format, target_format])
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "conversion_strategy"
|
|
4
|
+
require_relative "table_copier"
|
|
5
|
+
require_relative "outline_converter"
|
|
6
|
+
require_relative "woff_writer"
|
|
7
|
+
require_relative "woff2_encoder"
|
|
8
|
+
require_relative "svg_generator"
|
|
9
|
+
require "yaml"
|
|
10
|
+
|
|
11
|
+
module Fontisan
|
|
12
|
+
module Converters
|
|
13
|
+
# Main orchestrator for font format conversions
|
|
14
|
+
#
|
|
15
|
+
# [`FormatConverter`](lib/fontisan/converters/format_converter.rb) is the
|
|
16
|
+
# primary entry point for all format conversion operations. It:
|
|
17
|
+
# - Selects appropriate conversion strategy based on source/target formats
|
|
18
|
+
# - Validates conversions against the conversion matrix
|
|
19
|
+
# - Delegates actual conversion to strategy implementations
|
|
20
|
+
# - Provides clean error messages for unsupported conversions
|
|
21
|
+
#
|
|
22
|
+
# The converter uses a strategy pattern with pluggable strategies for
|
|
23
|
+
# different conversion types:
|
|
24
|
+
# - OutlineConverter: TTF ↔ OTF conversions
|
|
25
|
+
# - TableCopier: Same-format operations
|
|
26
|
+
# - Woff2Encoder: TTF/OTF → WOFF2 compression
|
|
27
|
+
# - SvgGenerator: TTF/OTF → SVG font generation
|
|
28
|
+
#
|
|
29
|
+
# Supported conversions are defined in the conversion matrix configuration
|
|
30
|
+
# file, making it easy to extend without modifying code.
|
|
31
|
+
#
|
|
32
|
+
# @example Converting TTF to OTF
|
|
33
|
+
# converter = Fontisan::Converters::FormatConverter.new
|
|
34
|
+
# tables = converter.convert(font, :otf)
|
|
35
|
+
# FontWriter.write_to_file(tables, 'output.otf',
|
|
36
|
+
# sfnt_version: 0x4F54544F)
|
|
37
|
+
#
|
|
38
|
+
# @example Same-format copy
|
|
39
|
+
# converter = Fontisan::Converters::FormatConverter.new
|
|
40
|
+
# tables = converter.convert(font, :ttf) # TTF to TTF
|
|
41
|
+
# FontWriter.write_to_file(tables, 'copy.ttf')
|
|
42
|
+
class FormatConverter
|
|
43
|
+
# @return [Hash] Conversion matrix loaded from config
|
|
44
|
+
attr_reader :conversion_matrix
|
|
45
|
+
|
|
46
|
+
# @return [Array] Available conversion strategies
|
|
47
|
+
attr_reader :strategies
|
|
48
|
+
|
|
49
|
+
# Initialize converter with strategies
|
|
50
|
+
#
|
|
51
|
+
# @param conversion_matrix_path [String, nil] Path to conversion matrix
|
|
52
|
+
# config. If nil, uses default.
|
|
53
|
+
def initialize(conversion_matrix_path: nil)
|
|
54
|
+
@strategies = [
|
|
55
|
+
TableCopier.new,
|
|
56
|
+
OutlineConverter.new,
|
|
57
|
+
WoffWriter.new,
|
|
58
|
+
Woff2Encoder.new,
|
|
59
|
+
SvgGenerator.new,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
load_conversion_matrix(conversion_matrix_path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Convert font to target format
|
|
66
|
+
#
|
|
67
|
+
# This is the main entry point for format conversion. It:
|
|
68
|
+
# 1. Detects source format from font
|
|
69
|
+
# 2. Validates conversion is supported
|
|
70
|
+
# 3. Selects appropriate strategy
|
|
71
|
+
# 4. Delegates conversion to strategy
|
|
72
|
+
#
|
|
73
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
74
|
+
# @param target_format [Symbol] Target format (:ttf, :otf, :woff2, :svg)
|
|
75
|
+
# @param options [Hash] Additional conversion options
|
|
76
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
77
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
78
|
+
# @raise [Error] If conversion is not supported
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# tables = converter.convert(font, :otf)
|
|
82
|
+
def convert(font, target_format, options = {})
|
|
83
|
+
validate_parameters!(font, target_format)
|
|
84
|
+
|
|
85
|
+
source_format = detect_format(font)
|
|
86
|
+
validate_conversion_supported!(source_format, target_format)
|
|
87
|
+
|
|
88
|
+
strategy = select_strategy(source_format, target_format)
|
|
89
|
+
strategy.convert(font, options.merge(target_format: target_format))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check if a conversion is supported
|
|
93
|
+
#
|
|
94
|
+
# @param source_format [Symbol] Source format
|
|
95
|
+
# @param target_format [Symbol] Target format
|
|
96
|
+
# @return [Boolean] True if conversion is supported
|
|
97
|
+
def supported?(source_format, target_format)
|
|
98
|
+
return false unless conversion_matrix
|
|
99
|
+
|
|
100
|
+
conversions = conversion_matrix["conversions"]
|
|
101
|
+
return false unless conversions
|
|
102
|
+
|
|
103
|
+
conversions.any? do |conv|
|
|
104
|
+
conv["from"] == source_format.to_s &&
|
|
105
|
+
conv["to"] == target_format.to_s
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get list of supported target formats for a source format
|
|
110
|
+
#
|
|
111
|
+
# @param source_format [Symbol] Source format
|
|
112
|
+
# @return [Array<Symbol>] Supported target formats
|
|
113
|
+
def supported_targets(source_format)
|
|
114
|
+
return [] unless conversion_matrix
|
|
115
|
+
|
|
116
|
+
conversions = conversion_matrix["conversions"]
|
|
117
|
+
return [] unless conversions
|
|
118
|
+
|
|
119
|
+
conversions
|
|
120
|
+
.select { |conv| conv["from"] == source_format.to_s }
|
|
121
|
+
.map { |conv| conv["to"].to_sym }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Get all supported conversions
|
|
125
|
+
#
|
|
126
|
+
# @return [Array<Hash>] Array of conversion hashes with :from and :to
|
|
127
|
+
def all_conversions
|
|
128
|
+
return [] unless conversion_matrix
|
|
129
|
+
|
|
130
|
+
conversions = conversion_matrix["conversions"]
|
|
131
|
+
return [] unless conversions
|
|
132
|
+
|
|
133
|
+
conversions.map do |conv|
|
|
134
|
+
{ from: conv["from"].to_sym, to: conv["to"].to_sym }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Load conversion matrix from YAML config
|
|
141
|
+
#
|
|
142
|
+
# @param path [String, nil] Path to config file
|
|
143
|
+
def load_conversion_matrix(path)
|
|
144
|
+
config_path = path || default_conversion_matrix_path
|
|
145
|
+
|
|
146
|
+
@conversion_matrix = if File.exist?(config_path)
|
|
147
|
+
YAML.load_file(config_path)
|
|
148
|
+
else
|
|
149
|
+
# Use default inline matrix if file doesn't exist
|
|
150
|
+
default_conversion_matrix
|
|
151
|
+
end
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
warn "Failed to load conversion matrix: #{e.message}"
|
|
154
|
+
@conversion_matrix = default_conversion_matrix
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Get default conversion matrix path
|
|
158
|
+
#
|
|
159
|
+
# @return [String] Path to conversion matrix config
|
|
160
|
+
def default_conversion_matrix_path
|
|
161
|
+
File.join(
|
|
162
|
+
__dir__,
|
|
163
|
+
"..",
|
|
164
|
+
"config",
|
|
165
|
+
"conversion_matrix.yml",
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Get default conversion matrix (fallback)
|
|
170
|
+
#
|
|
171
|
+
# @return [Hash] Default conversion matrix
|
|
172
|
+
def default_conversion_matrix
|
|
173
|
+
{
|
|
174
|
+
"conversions" => [
|
|
175
|
+
{ "from" => "ttf", "to" => "ttf" },
|
|
176
|
+
{ "from" => "otf", "to" => "otf" },
|
|
177
|
+
{ "from" => "ttf", "to" => "otf" },
|
|
178
|
+
{ "from" => "otf", "to" => "ttf" },
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Validate conversion parameters
|
|
184
|
+
#
|
|
185
|
+
# @param font [Object] Font object
|
|
186
|
+
# @param target_format [Symbol] Target format
|
|
187
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
188
|
+
def validate_parameters!(font, target_format)
|
|
189
|
+
raise ArgumentError, "Font cannot be nil" if font.nil?
|
|
190
|
+
|
|
191
|
+
unless font.respond_to?(:table)
|
|
192
|
+
raise ArgumentError, "Font must respond to :table method"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
unless target_format.is_a?(Symbol)
|
|
196
|
+
raise ArgumentError,
|
|
197
|
+
"target_format must be a Symbol, got: #{target_format.class}"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Validate conversion is supported
|
|
202
|
+
#
|
|
203
|
+
# @param source_format [Symbol] Source format
|
|
204
|
+
# @param target_format [Symbol] Target format
|
|
205
|
+
# @raise [Error] If conversion is not supported
|
|
206
|
+
def validate_conversion_supported!(source_format, target_format)
|
|
207
|
+
unless supported?(source_format, target_format)
|
|
208
|
+
available = supported_targets(source_format)
|
|
209
|
+
message = "Conversion from #{source_format} to #{target_format} " \
|
|
210
|
+
"is not supported."
|
|
211
|
+
message += if available.any?
|
|
212
|
+
" Available targets for #{source_format}: " \
|
|
213
|
+
"#{available.join(', ')}"
|
|
214
|
+
else
|
|
215
|
+
" No conversions available from #{source_format}."
|
|
216
|
+
end
|
|
217
|
+
raise Fontisan::Error, message
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Select conversion strategy
|
|
222
|
+
#
|
|
223
|
+
# @param source_format [Symbol] Source format
|
|
224
|
+
# @param target_format [Symbol] Target format
|
|
225
|
+
# @return [ConversionStrategy] Selected strategy
|
|
226
|
+
# @raise [Error] If no strategy supports the conversion
|
|
227
|
+
def select_strategy(source_format, target_format)
|
|
228
|
+
strategy = strategies.find do |s|
|
|
229
|
+
s.supports?(source_format, target_format)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
unless strategy
|
|
233
|
+
raise Fontisan::Error,
|
|
234
|
+
"No strategy available for #{source_format} → #{target_format}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
strategy
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Detect font format from tables
|
|
241
|
+
#
|
|
242
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to detect
|
|
243
|
+
# @return [Symbol] Format (:ttf or :otf)
|
|
244
|
+
# @raise [Error] If format cannot be detected
|
|
245
|
+
def detect_format(font)
|
|
246
|
+
# Check for CFF/CFF2 tables (OpenType/CFF)
|
|
247
|
+
if font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
248
|
+
:otf
|
|
249
|
+
# Check for glyf table (TrueType)
|
|
250
|
+
elsif font.has_table?("glyf")
|
|
251
|
+
:ttf
|
|
252
|
+
else
|
|
253
|
+
raise Fontisan::Error,
|
|
254
|
+
"Cannot detect font format: missing both CFF and glyf tables"
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|