fontisan 0.2.0 → 0.2.2
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 +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -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 +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- 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/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- 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 +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
|
@@ -211,7 +211,8 @@ module Fontisan
|
|
|
211
211
|
batch_entries.each do |entry|
|
|
212
212
|
relative_offset = entry.offset - batch_offset
|
|
213
213
|
tag_key = entry.tag.dup.force_encoding("UTF-8")
|
|
214
|
-
@table_data[tag_key] =
|
|
214
|
+
@table_data[tag_key] =
|
|
215
|
+
batch_data[relative_offset, entry.table_length]
|
|
215
216
|
end
|
|
216
217
|
end
|
|
217
218
|
|
|
@@ -258,6 +259,20 @@ module Fontisan
|
|
|
258
259
|
true
|
|
259
260
|
end
|
|
260
261
|
|
|
262
|
+
# Check if font is TrueType flavored
|
|
263
|
+
#
|
|
264
|
+
# @return [Boolean] false for OpenType fonts
|
|
265
|
+
def truetype?
|
|
266
|
+
false
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Check if font is CFF flavored
|
|
270
|
+
#
|
|
271
|
+
# @return [Boolean] true for OpenType fonts
|
|
272
|
+
def cff?
|
|
273
|
+
true
|
|
274
|
+
end
|
|
275
|
+
|
|
261
276
|
# Check if font has a specific table
|
|
262
277
|
#
|
|
263
278
|
# @param tag [String] The table tag to check for
|
|
@@ -272,6 +287,7 @@ module Fontisan
|
|
|
272
287
|
# @return [Boolean] true if table is available in current mode
|
|
273
288
|
def table_available?(tag)
|
|
274
289
|
return false unless has_table?(tag)
|
|
290
|
+
|
|
275
291
|
LoadingModes.table_allowed?(@loading_mode, tag)
|
|
276
292
|
end
|
|
277
293
|
|
|
@@ -490,6 +506,7 @@ module Fontisan
|
|
|
490
506
|
Constants::OS2_TAG => Tables::Os2,
|
|
491
507
|
Constants::POST_TAG => Tables::Post,
|
|
492
508
|
Constants::CMAP_TAG => Tables::Cmap,
|
|
509
|
+
Constants::CFF_TAG => Tables::Cff,
|
|
493
510
|
Constants::FVAR_TAG => Tables::Fvar,
|
|
494
511
|
Constants::GSUB_TAG => Tables::Gsub,
|
|
495
512
|
Constants::GPOS_TAG => Tables::Gpos,
|
|
@@ -558,18 +575,19 @@ module Fontisan
|
|
|
558
575
|
# @param path [String] Path to the OTF file
|
|
559
576
|
# @return [void]
|
|
560
577
|
def update_checksum_adjustment_in_file(path)
|
|
561
|
-
#
|
|
562
|
-
|
|
578
|
+
# Use tempfile-based checksum calculation for Windows compatibility
|
|
579
|
+
# This keeps the tempfile alive until we're done with the checksum
|
|
580
|
+
File.open(path, "r+b") do |io|
|
|
581
|
+
checksum, _tmpfile = Utilities::ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
|
|
563
582
|
|
|
564
|
-
|
|
565
|
-
|
|
583
|
+
# Calculate adjustment
|
|
584
|
+
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
566
585
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
586
|
+
# Find head table position
|
|
587
|
+
head_entry = head_table
|
|
588
|
+
return unless head_entry
|
|
570
589
|
|
|
571
|
-
|
|
572
|
-
File.open(path, "r+b") do |io|
|
|
590
|
+
# Write adjustment to head table (offset 8 within head table)
|
|
573
591
|
io.seek(head_entry.offset + 8)
|
|
574
592
|
io.write([adjustment].pack("N"))
|
|
575
593
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# Extensions to OpenTypeFont for table-based construction
|
|
5
|
+
class OpenTypeFont
|
|
6
|
+
# Create font from hash of tables
|
|
7
|
+
#
|
|
8
|
+
# This is used during font conversion when we have tables but not a file.
|
|
9
|
+
#
|
|
10
|
+
# @param tables [Hash<String, String>] Map of table tag to binary data
|
|
11
|
+
# @return [OpenTypeFont] New font instance
|
|
12
|
+
def self.from_tables(tables)
|
|
13
|
+
# Create minimal header structure
|
|
14
|
+
font = new
|
|
15
|
+
font.initialize_storage
|
|
16
|
+
font.loading_mode = LoadingModes::FULL
|
|
17
|
+
|
|
18
|
+
# Store table data
|
|
19
|
+
font.table_data = tables
|
|
20
|
+
|
|
21
|
+
# Build header from tables
|
|
22
|
+
num_tables = tables.size
|
|
23
|
+
max_power = 0
|
|
24
|
+
n = num_tables
|
|
25
|
+
while n > 1
|
|
26
|
+
n >>= 1
|
|
27
|
+
max_power += 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
search_range = (1 << max_power) * 16
|
|
31
|
+
entry_selector = max_power
|
|
32
|
+
range_shift = (num_tables * 16) - search_range
|
|
33
|
+
|
|
34
|
+
font.header.sfnt_version = 0x4F54544F # 'OTTO' for OpenType/CFF
|
|
35
|
+
font.header.num_tables = num_tables
|
|
36
|
+
font.header.search_range = search_range
|
|
37
|
+
font.header.entry_selector = entry_selector
|
|
38
|
+
font.header.range_shift = range_shift
|
|
39
|
+
|
|
40
|
+
# Build table directory
|
|
41
|
+
font.tables.clear
|
|
42
|
+
tables.each_key do |tag|
|
|
43
|
+
entry = TableDirectory.new
|
|
44
|
+
entry.tag = tag
|
|
45
|
+
entry.checksum = 0 # Will be calculated on write
|
|
46
|
+
entry.offset = 0 # Will be calculated on write
|
|
47
|
+
entry.table_length = tables[tag].bytesize
|
|
48
|
+
font.tables << entry
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
font
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -222,7 +222,8 @@ module Fontisan
|
|
|
222
222
|
if @stack_aware
|
|
223
223
|
tracker = @stack_trackers[glyph_id]
|
|
224
224
|
next unless tracker
|
|
225
|
-
next unless tracker.stack_neutral?(start_pos,
|
|
225
|
+
next unless tracker.stack_neutral?(start_pos,
|
|
226
|
+
start_pos + length)
|
|
226
227
|
end
|
|
227
228
|
|
|
228
229
|
pattern_bytes = charstring[start_pos, length]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../font_loader"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Pipeline
|
|
7
|
+
# Detects font format and capabilities
|
|
8
|
+
#
|
|
9
|
+
# This class analyzes font files to determine:
|
|
10
|
+
# - Format: TTF, OTF, TTC, OTC, WOFF, WOFF2, SVG
|
|
11
|
+
# - Variation type: static, gvar (TrueType variable), CFF2 (OpenType variable)
|
|
12
|
+
# - Capabilities: outline type, variation support, collection support
|
|
13
|
+
#
|
|
14
|
+
# Used by the universal transformation pipeline to determine conversion
|
|
15
|
+
# strategies and validate compatibility.
|
|
16
|
+
#
|
|
17
|
+
# @example Detecting a font's format
|
|
18
|
+
# detector = FormatDetector.new("font.ttf")
|
|
19
|
+
# info = detector.detect
|
|
20
|
+
# puts info[:format] # => :ttf
|
|
21
|
+
# puts info[:variation_type] # => :gvar
|
|
22
|
+
# puts info[:capabilities][:outline] # => :truetype
|
|
23
|
+
class FormatDetector
|
|
24
|
+
# @return [String] Path to font file
|
|
25
|
+
attr_reader :file_path
|
|
26
|
+
|
|
27
|
+
# @return [TrueTypeFont, OpenTypeFont, TrueTypeCollection, OpenTypeCollection, nil] Loaded font
|
|
28
|
+
attr_reader :font
|
|
29
|
+
|
|
30
|
+
# Initialize detector
|
|
31
|
+
#
|
|
32
|
+
# @param file_path [String] Path to font file
|
|
33
|
+
def initialize(file_path)
|
|
34
|
+
@file_path = file_path
|
|
35
|
+
@font = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Detect format and capabilities
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash] Detection results with :format, :variation_type, :capabilities
|
|
41
|
+
def detect
|
|
42
|
+
load_font
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
format: detect_format,
|
|
46
|
+
variation_type: detect_variation,
|
|
47
|
+
capabilities: detect_capabilities,
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Detect font format
|
|
52
|
+
#
|
|
53
|
+
# @return [Symbol] One of :ttf, :otf, :ttc, :otc, :woff, :woff2, :svg
|
|
54
|
+
def detect_format
|
|
55
|
+
# Check for SVG first (from file extension even if font failed to load)
|
|
56
|
+
return :svg if @file_path.end_with?(".svg")
|
|
57
|
+
|
|
58
|
+
return :unknown unless @font
|
|
59
|
+
|
|
60
|
+
# Use is_a? for proper class checking
|
|
61
|
+
case @font
|
|
62
|
+
when Fontisan::TrueTypeCollection
|
|
63
|
+
:ttc
|
|
64
|
+
when Fontisan::OpenTypeCollection
|
|
65
|
+
:otc
|
|
66
|
+
when Fontisan::TrueTypeFont
|
|
67
|
+
if @file_path.end_with?(".woff")
|
|
68
|
+
:woff
|
|
69
|
+
elsif @file_path.end_with?(".woff2")
|
|
70
|
+
:woff2
|
|
71
|
+
else
|
|
72
|
+
:ttf
|
|
73
|
+
end
|
|
74
|
+
when Fontisan::OpenTypeFont
|
|
75
|
+
if @file_path.end_with?(".woff")
|
|
76
|
+
:woff
|
|
77
|
+
elsif @file_path.end_with?(".woff2")
|
|
78
|
+
:woff2
|
|
79
|
+
else
|
|
80
|
+
:otf
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
:unknown
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Detect variation type
|
|
88
|
+
#
|
|
89
|
+
# @return [Symbol] One of :static, :gvar, :cff2
|
|
90
|
+
def detect_variation
|
|
91
|
+
return :static unless @font
|
|
92
|
+
|
|
93
|
+
# Collections don't have has_table? method
|
|
94
|
+
# Return :static for collections (variation detection would need to load first font)
|
|
95
|
+
return :static if collection?
|
|
96
|
+
|
|
97
|
+
# Check for variable font tables
|
|
98
|
+
if @font.has_table?("fvar")
|
|
99
|
+
# Variable font detected - check variation type
|
|
100
|
+
if @font.has_table?("gvar")
|
|
101
|
+
:gvar # TrueType variable font
|
|
102
|
+
elsif @font.has_table?("CFF2")
|
|
103
|
+
:cff2 # OpenType variable font (CFF2)
|
|
104
|
+
else
|
|
105
|
+
:static # Has fvar but no variation data (shouldn't happen)
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
:static
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Detect font capabilities
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash] Capabilities hash
|
|
115
|
+
def detect_capabilities
|
|
116
|
+
return default_capabilities unless @font
|
|
117
|
+
|
|
118
|
+
# Check if this is a collection
|
|
119
|
+
is_collection = collection?
|
|
120
|
+
|
|
121
|
+
font_to_check = if is_collection
|
|
122
|
+
# Collections don't have fonts method, need to load first font
|
|
123
|
+
nil # Will handle in API usage
|
|
124
|
+
else
|
|
125
|
+
@font
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# For collections, return basic capabilities
|
|
129
|
+
if is_collection
|
|
130
|
+
return {
|
|
131
|
+
outline: :unknown, # Would need to load first font to know
|
|
132
|
+
variation: false, # Would need to load first font to know
|
|
133
|
+
collection: true,
|
|
134
|
+
tables: [],
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return default_capabilities unless font_to_check
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
outline: detect_outline_type(font_to_check),
|
|
142
|
+
variation: detect_variation != :static,
|
|
143
|
+
collection: false,
|
|
144
|
+
tables: available_tables(font_to_check),
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if font is a collection
|
|
149
|
+
#
|
|
150
|
+
# @return [Boolean] True if collection (TTC/OTC)
|
|
151
|
+
def collection?
|
|
152
|
+
@font.is_a?(Fontisan::TrueTypeCollection) ||
|
|
153
|
+
@font.is_a?(Fontisan::OpenTypeCollection)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Check if font is variable
|
|
157
|
+
#
|
|
158
|
+
# @return [Boolean] True if variable font
|
|
159
|
+
def variable?
|
|
160
|
+
detect_variation != :static
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check if format is compatible with target
|
|
164
|
+
#
|
|
165
|
+
# @param target_format [Symbol] Target format (:ttf, :otf, etc.)
|
|
166
|
+
# @return [Boolean] True if conversion is possible
|
|
167
|
+
def compatible_with?(target_format)
|
|
168
|
+
current_format = detect_format
|
|
169
|
+
variation_type = detect_variation
|
|
170
|
+
|
|
171
|
+
# Same format is always compatible
|
|
172
|
+
return true if current_format == target_format
|
|
173
|
+
|
|
174
|
+
# Collection formats
|
|
175
|
+
if %i[ttc otc].include?(current_format)
|
|
176
|
+
return %i[ttc otc].include?(target_format)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Variable font constraints
|
|
180
|
+
if variation_type == :static
|
|
181
|
+
# Static fonts can convert to any format
|
|
182
|
+
true
|
|
183
|
+
else
|
|
184
|
+
case variation_type
|
|
185
|
+
when :gvar
|
|
186
|
+
# TrueType variable can convert to TrueType formats
|
|
187
|
+
%i[ttf ttc woff woff2].include?(target_format)
|
|
188
|
+
when :cff2
|
|
189
|
+
# OpenType variable can convert to OpenType formats
|
|
190
|
+
%i[otf otc woff woff2].include?(target_format)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
# Load font from file
|
|
198
|
+
def load_font
|
|
199
|
+
# Check if it's a collection first
|
|
200
|
+
@font = if FontLoader.collection?(@file_path)
|
|
201
|
+
FontLoader.load_collection(@file_path)
|
|
202
|
+
else
|
|
203
|
+
FontLoader.load(@file_path, mode: :full)
|
|
204
|
+
end
|
|
205
|
+
rescue StandardError => e
|
|
206
|
+
warn "Failed to load font: #{e.message}"
|
|
207
|
+
@font = nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Detect outline type
|
|
211
|
+
#
|
|
212
|
+
# @param font [Font] Font object
|
|
213
|
+
# @return [Symbol] :truetype or :cff
|
|
214
|
+
def detect_outline_type(font)
|
|
215
|
+
if font.has_table?("glyf") || font.has_table?("gvar")
|
|
216
|
+
:truetype
|
|
217
|
+
elsif font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
218
|
+
:cff
|
|
219
|
+
else
|
|
220
|
+
:unknown
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Get available tables
|
|
225
|
+
#
|
|
226
|
+
# @param font [Font] Font object
|
|
227
|
+
# @return [Array<String>] List of table tags
|
|
228
|
+
def available_tables(font)
|
|
229
|
+
return [] unless font.respond_to?(:table_names)
|
|
230
|
+
|
|
231
|
+
font.table_names
|
|
232
|
+
rescue StandardError
|
|
233
|
+
[]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Default capabilities when font cannot be loaded
|
|
237
|
+
#
|
|
238
|
+
# @return [Hash] Default capabilities
|
|
239
|
+
def default_capabilities
|
|
240
|
+
{
|
|
241
|
+
outline: :unknown,
|
|
242
|
+
variation: false,
|
|
243
|
+
collection: false,
|
|
244
|
+
tables: [],
|
|
245
|
+
}
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../font_writer"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Pipeline
|
|
7
|
+
# Handles writing font tables to various output formats
|
|
8
|
+
#
|
|
9
|
+
# This class abstracts the complexity of writing different font formats:
|
|
10
|
+
# - SFNT formats (TTF, OTF) via FontWriter
|
|
11
|
+
# - WOFF via WoffWriter
|
|
12
|
+
# - WOFF2 via Woff2Encoder
|
|
13
|
+
#
|
|
14
|
+
# Single Responsibility: Coordinate output writing for different formats
|
|
15
|
+
#
|
|
16
|
+
# @example Write TTF font
|
|
17
|
+
# writer = OutputWriter.new("output.ttf", :ttf)
|
|
18
|
+
# writer.write(tables)
|
|
19
|
+
#
|
|
20
|
+
# @example Write OTF font
|
|
21
|
+
# writer = OutputWriter.new("output.otf", :otf)
|
|
22
|
+
# writer.write(tables)
|
|
23
|
+
class OutputWriter
|
|
24
|
+
# @return [String] Output file path
|
|
25
|
+
attr_reader :output_path
|
|
26
|
+
|
|
27
|
+
# @return [Symbol] Target format
|
|
28
|
+
attr_reader :format
|
|
29
|
+
|
|
30
|
+
# @return [Hash] Writing options
|
|
31
|
+
attr_reader :options
|
|
32
|
+
|
|
33
|
+
# Initialize output writer
|
|
34
|
+
#
|
|
35
|
+
# @param output_path [String] Path to write output
|
|
36
|
+
# @param format [Symbol] Target format (:ttf, :otf, :woff, :woff2)
|
|
37
|
+
# @param options [Hash] Writing options
|
|
38
|
+
def initialize(output_path, format, options = {})
|
|
39
|
+
@output_path = output_path
|
|
40
|
+
@format = format
|
|
41
|
+
@options = options
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Write font tables to output file
|
|
45
|
+
#
|
|
46
|
+
# @param tables [Hash<String, String>, Hash] Font tables (tag => binary data) or special format result
|
|
47
|
+
# @return [Integer] Number of bytes written
|
|
48
|
+
# @raise [ArgumentError] If format is unsupported
|
|
49
|
+
def write(tables)
|
|
50
|
+
case @format
|
|
51
|
+
when :ttf, :otf
|
|
52
|
+
write_sfnt(tables)
|
|
53
|
+
when :woff
|
|
54
|
+
write_woff(tables)
|
|
55
|
+
when :woff2
|
|
56
|
+
write_woff2(tables)
|
|
57
|
+
when :svg
|
|
58
|
+
write_svg(tables)
|
|
59
|
+
else
|
|
60
|
+
raise ArgumentError, "Unsupported output format: #{@format}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Write SVG format
|
|
67
|
+
#
|
|
68
|
+
# @param result [Hash] Result with :svg_xml key
|
|
69
|
+
# @return [Integer] Number of bytes written
|
|
70
|
+
def write_svg(result)
|
|
71
|
+
svg_xml = result[:svg_xml] || result["svg_xml"]
|
|
72
|
+
unless svg_xml
|
|
73
|
+
raise ArgumentError,
|
|
74
|
+
"SVG result must contain :svg_xml key"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
File.write(@output_path, svg_xml)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Write SFNT format (TTF or OTF)
|
|
81
|
+
#
|
|
82
|
+
# @param tables [Hash<String, String>] Font tables
|
|
83
|
+
# @return [Integer] Number of bytes written
|
|
84
|
+
def write_sfnt(tables)
|
|
85
|
+
sfnt_version = determine_sfnt_version
|
|
86
|
+
FontWriter.write_to_file(tables, @output_path,
|
|
87
|
+
sfnt_version: sfnt_version)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Write WOFF format
|
|
91
|
+
#
|
|
92
|
+
# @param tables [Hash<String, String>] Font tables
|
|
93
|
+
# @return [Integer] Number of bytes written
|
|
94
|
+
def write_woff(tables)
|
|
95
|
+
require_relative "../converters/woff_writer"
|
|
96
|
+
|
|
97
|
+
writer = Converters::WoffWriter.new
|
|
98
|
+
font = build_font_from_tables(tables)
|
|
99
|
+
result = writer.convert(font, @options)
|
|
100
|
+
|
|
101
|
+
File.binwrite(@output_path, result[:woff_data])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Write WOFF2 format
|
|
105
|
+
#
|
|
106
|
+
# @param tables [Hash<String, String>] Font tables
|
|
107
|
+
# @return [Integer] Number of bytes written
|
|
108
|
+
def write_woff2(tables)
|
|
109
|
+
require_relative "../converters/woff2_encoder"
|
|
110
|
+
|
|
111
|
+
encoder = Converters::Woff2Encoder.new
|
|
112
|
+
font = build_font_from_tables(tables)
|
|
113
|
+
result = encoder.convert(font, @options)
|
|
114
|
+
|
|
115
|
+
File.binwrite(@output_path, result[:woff2_binary])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Determine SFNT version based on format and tables
|
|
119
|
+
#
|
|
120
|
+
# @return [Integer] SFNT version (0x00010000 for TTF, 0x4F54544F for OTF)
|
|
121
|
+
def determine_sfnt_version
|
|
122
|
+
case @format
|
|
123
|
+
when :ttf, :woff, :woff2 then 0x00010000
|
|
124
|
+
when :otf then 0x4F54544F # 'OTTO'
|
|
125
|
+
else raise ArgumentError, "Unsupported format: #{@format}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Build font object from tables
|
|
130
|
+
#
|
|
131
|
+
# Helper to create font object from tables for converters that need it.
|
|
132
|
+
#
|
|
133
|
+
# @param tables [Hash<String, String>] Font tables
|
|
134
|
+
# @return [Font] Font object
|
|
135
|
+
def build_font_from_tables(tables)
|
|
136
|
+
# Detect font type from tables
|
|
137
|
+
has_cff = tables.key?("CFF ") || tables.key?("CFF2")
|
|
138
|
+
has_glyf = tables.key?("glyf")
|
|
139
|
+
|
|
140
|
+
if has_cff
|
|
141
|
+
OpenTypeFont.from_tables(tables)
|
|
142
|
+
elsif has_glyf
|
|
143
|
+
TrueTypeFont.from_tables(tables)
|
|
144
|
+
else
|
|
145
|
+
# Default based on format
|
|
146
|
+
case @format
|
|
147
|
+
when :ttf, :woff, :woff2
|
|
148
|
+
TrueTypeFont.from_tables(tables)
|
|
149
|
+
when :otf
|
|
150
|
+
OpenTypeFont.from_tables(tables)
|
|
151
|
+
else
|
|
152
|
+
raise ArgumentError,
|
|
153
|
+
"Cannot determine font type for format: #{@format}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Pipeline
|
|
5
|
+
module Strategies
|
|
6
|
+
# Base class for variation resolution strategies
|
|
7
|
+
#
|
|
8
|
+
# This abstract class defines the interface that all variation resolution
|
|
9
|
+
# strategies must implement. It follows the Strategy pattern to allow
|
|
10
|
+
# different approaches to handling variable font data during conversion.
|
|
11
|
+
#
|
|
12
|
+
# Subclasses must implement:
|
|
13
|
+
# - resolve(font): Process the font and return tables
|
|
14
|
+
# - preserves_variation?: Indicate if variation data is preserved
|
|
15
|
+
# - strategy_name: Return the strategy identifier
|
|
16
|
+
#
|
|
17
|
+
# @example Implementing a strategy
|
|
18
|
+
# class MyStrategy < BaseStrategy
|
|
19
|
+
# def resolve(font)
|
|
20
|
+
# # Implementation
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def preserves_variation?
|
|
24
|
+
# false
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def strategy_name
|
|
28
|
+
# :my_strategy
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
class BaseStrategy
|
|
32
|
+
# @return [Hash] Strategy options
|
|
33
|
+
attr_reader :options
|
|
34
|
+
|
|
35
|
+
# Initialize strategy with options
|
|
36
|
+
#
|
|
37
|
+
# @param options [Hash] Strategy-specific options
|
|
38
|
+
def initialize(options = {})
|
|
39
|
+
@options = options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Resolve variation data
|
|
43
|
+
#
|
|
44
|
+
# This method must be implemented by subclasses to process the font
|
|
45
|
+
# and return the appropriate tables based on the strategy.
|
|
46
|
+
#
|
|
47
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to process
|
|
48
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
49
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
50
|
+
def resolve(font)
|
|
51
|
+
raise NotImplementedError,
|
|
52
|
+
"#{self.class.name} must implement #resolve"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if strategy preserves variation data
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean] True if variation data is preserved
|
|
58
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
59
|
+
def preserves_variation?
|
|
60
|
+
raise NotImplementedError,
|
|
61
|
+
"#{self.class.name} must implement #preserves_variation?"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get strategy name
|
|
65
|
+
#
|
|
66
|
+
# @return [Symbol] Strategy identifier
|
|
67
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
|
68
|
+
def strategy_name
|
|
69
|
+
raise NotImplementedError,
|
|
70
|
+
"#{self.class.name} must implement #strategy_name"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|