fontisan 0.2.4 → 0.2.6
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 +168 -32
- data/README.adoc +673 -1091
- data/lib/fontisan/cli.rb +94 -13
- data/lib/fontisan/collection/dfont_builder.rb +315 -0
- data/lib/fontisan/commands/convert_command.rb +118 -7
- data/lib/fontisan/commands/pack_command.rb +129 -22
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +175 -1
- data/lib/fontisan/constants.rb +8 -0
- data/lib/fontisan/converters/collection_converter.rb +438 -0
- data/lib/fontisan/converters/woff2_encoder.rb +7 -29
- data/lib/fontisan/dfont_collection.rb +185 -0
- data/lib/fontisan/font_loader.rb +91 -6
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/parsers/dfont_parser.rb +192 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/true_type_font.rb +8 -46
- data/lib/fontisan/validation/collection_validator.rb +265 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +78 -6
- metadata +13 -12
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- data/lib/fontisan/validation/variable_font_validator.rb +0 -218
- data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
- data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
- data/lib/fontisan/validation/woff2_validator.rb +0 -248
|
@@ -177,6 +177,91 @@ conversions:
|
|
|
177
177
|
Same-format copy operation. Decompresses and re-compresses WOFF2 data
|
|
178
178
|
with Brotli, useful for normalizing compression or updating metadata.
|
|
179
179
|
|
|
180
|
+
# Collection format conversions (TTC/OTC/dfont)
|
|
181
|
+
# Same-format collection operations
|
|
182
|
+
- from: ttc
|
|
183
|
+
to: ttc
|
|
184
|
+
strategy: collection_copier
|
|
185
|
+
description: "Copy TrueType Collection"
|
|
186
|
+
status: implemented
|
|
187
|
+
notes: >
|
|
188
|
+
Same-format copy operation for TTC files. Preserves all fonts and
|
|
189
|
+
table sharing structure.
|
|
190
|
+
|
|
191
|
+
- from: otc
|
|
192
|
+
to: otc
|
|
193
|
+
strategy: collection_copier
|
|
194
|
+
description: "Copy OpenType Collection"
|
|
195
|
+
status: implemented
|
|
196
|
+
notes: >
|
|
197
|
+
Same-format copy operation for OTC files. Preserves all fonts and
|
|
198
|
+
table sharing structure.
|
|
199
|
+
|
|
200
|
+
- from: dfont
|
|
201
|
+
to: dfont
|
|
202
|
+
strategy: collection_copier
|
|
203
|
+
description: "Copy Apple dfont suitcase"
|
|
204
|
+
status: implemented
|
|
205
|
+
notes: >
|
|
206
|
+
Same-format copy operation for dfont files. Preserves all fonts in
|
|
207
|
+
the suitcase.
|
|
208
|
+
|
|
209
|
+
# Cross-collection conversions
|
|
210
|
+
- from: ttc
|
|
211
|
+
to: otc
|
|
212
|
+
strategy: collection_converter
|
|
213
|
+
description: "Convert TrueType Collection to OpenType Collection"
|
|
214
|
+
status: implemented
|
|
215
|
+
notes: >
|
|
216
|
+
Unpacks TTC, converts all TrueType fonts to OpenType/CFF format,
|
|
217
|
+
then repacks as OTC with table sharing optimization.
|
|
218
|
+
|
|
219
|
+
- from: otc
|
|
220
|
+
to: ttc
|
|
221
|
+
strategy: collection_converter
|
|
222
|
+
description: "Convert OpenType Collection to TrueType Collection"
|
|
223
|
+
status: implemented
|
|
224
|
+
notes: >
|
|
225
|
+
Unpacks OTC, converts all OpenType/CFF fonts to TrueType format,
|
|
226
|
+
then repacks as TTC with table sharing optimization.
|
|
227
|
+
|
|
228
|
+
- from: ttc
|
|
229
|
+
to: dfont
|
|
230
|
+
strategy: collection_converter
|
|
231
|
+
description: "Convert TrueType Collection to dfont suitcase"
|
|
232
|
+
status: implemented
|
|
233
|
+
notes: >
|
|
234
|
+
Unpacks TTC and repacks as Apple dfont suitcase. No outline conversion
|
|
235
|
+
needed as dfont supports TrueType fonts.
|
|
236
|
+
|
|
237
|
+
- from: otc
|
|
238
|
+
to: dfont
|
|
239
|
+
strategy: collection_converter
|
|
240
|
+
description: "Convert OpenType Collection to dfont suitcase"
|
|
241
|
+
status: implemented
|
|
242
|
+
notes: >
|
|
243
|
+
Unpacks OTC and repacks as Apple dfont suitcase. No outline conversion
|
|
244
|
+
needed as dfont supports OpenType/CFF fonts.
|
|
245
|
+
|
|
246
|
+
- from: dfont
|
|
247
|
+
to: ttc
|
|
248
|
+
strategy: collection_converter
|
|
249
|
+
description: "Convert dfont suitcase to TrueType Collection"
|
|
250
|
+
status: implemented
|
|
251
|
+
notes: >
|
|
252
|
+
Unpacks dfont suitcase and repacks as TTC. If dfont contains OpenType
|
|
253
|
+
fonts, they are converted to TrueType format. Only valid if all fonts
|
|
254
|
+
can be converted to TrueType.
|
|
255
|
+
|
|
256
|
+
- from: dfont
|
|
257
|
+
to: otc
|
|
258
|
+
strategy: collection_converter
|
|
259
|
+
description: "Convert dfont suitcase to OpenType Collection"
|
|
260
|
+
status: implemented
|
|
261
|
+
notes: >
|
|
262
|
+
Unpacks dfont suitcase and repacks as OTC. If dfont contains TrueType
|
|
263
|
+
fonts, they are converted to OpenType/CFF format.
|
|
264
|
+
|
|
180
265
|
# Conversion compatibility matrix
|
|
181
266
|
#
|
|
182
267
|
# This section documents which source features are preserved in conversions.
|
|
@@ -247,4 +332,93 @@ compatibility:
|
|
|
247
332
|
use_cases:
|
|
248
333
|
- fallback: Emergency fallback for very old browsers
|
|
249
334
|
- conversion: Intermediate format for font conversion workflows
|
|
250
|
-
- inspection: Easy-to-read format for font inspection
|
|
335
|
+
- inspection: Easy-to-read format for font inspection
|
|
336
|
+
|
|
337
|
+
collection_conversions:
|
|
338
|
+
ttc_to_otc:
|
|
339
|
+
preserves:
|
|
340
|
+
- all_fonts: All fonts in collection
|
|
341
|
+
- font_metrics: Font metrics for each font
|
|
342
|
+
- layout_features: Layout features for each font
|
|
343
|
+
limitations:
|
|
344
|
+
- outline_conversion_required: TrueType to CFF conversion applied
|
|
345
|
+
- hinting_loss: TrueType hinting not preserved in CFF
|
|
346
|
+
notes: >
|
|
347
|
+
Converts all TrueType fonts to OpenType/CFF format using the standard
|
|
348
|
+
TTF→OTF conversion pipeline, then repacks with table sharing.
|
|
349
|
+
|
|
350
|
+
otc_to_ttc:
|
|
351
|
+
preserves:
|
|
352
|
+
- all_fonts: All fonts in collection
|
|
353
|
+
- font_metrics: Font metrics for each font
|
|
354
|
+
- layout_features: Layout features for each font
|
|
355
|
+
limitations:
|
|
356
|
+
- outline_conversion_required: CFF to TrueType conversion applied
|
|
357
|
+
- hinting_loss: CFF hints not preserved in TrueType
|
|
358
|
+
- approximation: Cubic to quadratic curve approximation
|
|
359
|
+
notes: >
|
|
360
|
+
Converts all OpenType/CFF fonts to TrueType format using the standard
|
|
361
|
+
OTF→TTF conversion pipeline, then repacks with table sharing.
|
|
362
|
+
|
|
363
|
+
to_dfont:
|
|
364
|
+
preserves:
|
|
365
|
+
- all_fonts: All fonts in collection
|
|
366
|
+
- original_formats: Preserves original outline formats (TTF or OTF)
|
|
367
|
+
- font_metrics: All font metrics
|
|
368
|
+
- layout_features: All layout features
|
|
369
|
+
benefits:
|
|
370
|
+
- mixed_formats: dfont supports both TrueType and OpenType fonts
|
|
371
|
+
- mac_compatibility: Native Mac OS X suitcase format
|
|
372
|
+
limitations:
|
|
373
|
+
- platform_specific: Mac OS X only, not cross-platform
|
|
374
|
+
- no_table_sharing: dfont doesn't deduplicate tables like TTC/OTC
|
|
375
|
+
notes: >
|
|
376
|
+
Repackages fonts into Apple dfont suitcase format. No outline conversion
|
|
377
|
+
needed as dfont supports any SFNT font type (TTF, OTF, or mixed).
|
|
378
|
+
|
|
379
|
+
from_dfont:
|
|
380
|
+
preserves:
|
|
381
|
+
- all_fonts: All fonts in suitcase
|
|
382
|
+
- font_metrics: All font metrics
|
|
383
|
+
- layout_features: All layout features
|
|
384
|
+
limitations:
|
|
385
|
+
- conversion_may_be_required: Outline conversion if target requires specific format
|
|
386
|
+
- hinting_loss: If outline conversion occurs
|
|
387
|
+
benefits:
|
|
388
|
+
- cross_platform: TTC/OTC work on all platforms
|
|
389
|
+
- table_sharing: TTC/OTC optimize with table deduplication
|
|
390
|
+
notes: >
|
|
391
|
+
Extracts fonts from dfont suitcase and repacks into TTC or OTC.
|
|
392
|
+
Outline conversion may occur depending on target format requirements.
|
|
393
|
+
|
|
394
|
+
# Collection format rules
|
|
395
|
+
collection_rules:
|
|
396
|
+
ttc:
|
|
397
|
+
required_format: truetype
|
|
398
|
+
allows_mixed: false
|
|
399
|
+
spec_compliance: opentype_spec
|
|
400
|
+
description: >
|
|
401
|
+
TrueType Collection per OpenType specification. Contains only TrueType
|
|
402
|
+
fonts (glyf/loca tables). CFF fonts are not allowed.
|
|
403
|
+
|
|
404
|
+
otc:
|
|
405
|
+
required_format: cff_preferred
|
|
406
|
+
allows_mixed: true
|
|
407
|
+
spec_compliance: opentype_1.8
|
|
408
|
+
description: >
|
|
409
|
+
OpenType Collection per OpenType 1.8 specification. Requires at least
|
|
410
|
+
one CFF font. Fontisan extension allows mixed TTF+OTF for flexibility.
|
|
411
|
+
|
|
412
|
+
dfont:
|
|
413
|
+
required_format: any_sfnt
|
|
414
|
+
allows_mixed: true
|
|
415
|
+
spec_compliance: apple_proprietary
|
|
416
|
+
description: >
|
|
417
|
+
Apple dfont (Data Fork Font) suitcase. Contains Mac font suitcase
|
|
418
|
+
resources (FOND, NFNT, sfnt). Supports any SFNT fonts (TrueType,
|
|
419
|
+
OpenType/CFF, or mixed). Mac OS X specific, not cross-platform.
|
|
420
|
+
|
|
421
|
+
prohibited:
|
|
422
|
+
- web_fonts_in_collections: >
|
|
423
|
+
WOFF and WOFF2 fonts cannot be included in collections (TTC, OTC, or dfont).
|
|
424
|
+
Web fonts are designed for single-font web delivery only.
|
data/lib/fontisan/constants.rb
CHANGED
|
@@ -25,6 +25,14 @@ module Fontisan
|
|
|
25
25
|
# SFNT version for OpenType fonts with CFF outlines ('OTTO')
|
|
26
26
|
SFNT_VERSION_OTTO = 0x4F54544F
|
|
27
27
|
|
|
28
|
+
# Apple 'true' TrueType signature (alternate to 0x00010000)
|
|
29
|
+
SFNT_VERSION_TRUE = 0x74727965 # 'true' in ASCII
|
|
30
|
+
|
|
31
|
+
# dfont resource fork signatures
|
|
32
|
+
DFONT_RESOURCE_HEADER = "\x00\x00\x01\x00"
|
|
33
|
+
SFNT_RESOURCE_TYPE = "sfnt"
|
|
34
|
+
FOND_RESOURCE_TYPE = "FOND"
|
|
35
|
+
|
|
28
36
|
# Head table tag identifier.
|
|
29
37
|
# The 'head' table contains global font header information including
|
|
30
38
|
# the checksum adjustment field.
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "format_converter"
|
|
4
|
+
require_relative "../collection/builder"
|
|
5
|
+
require_relative "../collection/dfont_builder"
|
|
6
|
+
require_relative "../parsers/dfont_parser"
|
|
7
|
+
require_relative "../font_loader"
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Converters
|
|
11
|
+
# CollectionConverter handles conversion between collection formats
|
|
12
|
+
#
|
|
13
|
+
# Main responsibility: Convert between TTC, OTC, and dfont collection
|
|
14
|
+
# formats using a three-step strategy:
|
|
15
|
+
# 1. Unpack: Extract individual fonts from source collection
|
|
16
|
+
# 2. Convert: Transform each font's outline format if requested
|
|
17
|
+
# 3. Repack: Rebuild collection in target format
|
|
18
|
+
#
|
|
19
|
+
# Supported conversions:
|
|
20
|
+
# - TTC ↔ OTC (preserves mixed TTF+OTF by default)
|
|
21
|
+
# - TTC → dfont (repackage)
|
|
22
|
+
# - OTC → dfont (repackage)
|
|
23
|
+
# - dfont → TTC (preserves mixed formats)
|
|
24
|
+
# - dfont → OTC (preserves mixed formats)
|
|
25
|
+
#
|
|
26
|
+
# @example Convert TTC to OTC (preserve formats)
|
|
27
|
+
# converter = CollectionConverter.new
|
|
28
|
+
# result = converter.convert(ttc_path, target_type: :otc, output: 'family.otc')
|
|
29
|
+
#
|
|
30
|
+
# @example Convert TTC to OTC with outline conversion
|
|
31
|
+
# converter = CollectionConverter.new
|
|
32
|
+
# result = converter.convert(ttc_path, target_type: :otc,
|
|
33
|
+
# options: { output: 'family.otc', convert_outlines: true })
|
|
34
|
+
#
|
|
35
|
+
# @example Convert dfont to TTC
|
|
36
|
+
# converter = CollectionConverter.new
|
|
37
|
+
# result = converter.convert(dfont_path, target_type: :ttc, output: 'family.ttc')
|
|
38
|
+
class CollectionConverter
|
|
39
|
+
# Convert collection to target format
|
|
40
|
+
#
|
|
41
|
+
# @param collection_path [String] Path to source collection
|
|
42
|
+
# @param target_type [Symbol] Target collection type (:ttc, :otc, :dfont)
|
|
43
|
+
# @param options [Hash] Conversion options
|
|
44
|
+
# @option options [String] :output Output file path (required)
|
|
45
|
+
# @option options [String] :target_format Target outline format: 'preserve' (default), 'ttf', or 'otf'
|
|
46
|
+
# @option options [Boolean] :optimize Enable table sharing (default: true, TTC/OTC only)
|
|
47
|
+
# @option options [Boolean] :verbose Enable verbose output (default: false)
|
|
48
|
+
# @return [Hash] Conversion result with:
|
|
49
|
+
# - :input [String] - Input collection path
|
|
50
|
+
# - :output [String] - Output collection path
|
|
51
|
+
# - :source_type [Symbol] - Source collection type
|
|
52
|
+
# - :target_type [Symbol] - Target collection type
|
|
53
|
+
# - :num_fonts [Integer] - Number of fonts converted
|
|
54
|
+
# - :conversions [Array<Hash>] - Per-font conversion details
|
|
55
|
+
# @raise [ArgumentError] if parameters invalid
|
|
56
|
+
# @raise [Error] if conversion fails
|
|
57
|
+
def convert(collection_path, target_type:, options: {})
|
|
58
|
+
validate_parameters!(collection_path, target_type, options)
|
|
59
|
+
|
|
60
|
+
verbose = options.fetch(:verbose, false)
|
|
61
|
+
output_path = options[:output]
|
|
62
|
+
target_format = options.fetch(:target_format, 'preserve').to_s
|
|
63
|
+
|
|
64
|
+
# Validate target_format
|
|
65
|
+
unless %w[preserve ttf otf].include?(target_format)
|
|
66
|
+
raise ArgumentError, "Invalid target_format: #{target_format}. Must be 'preserve', 'ttf', or 'otf'"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
puts "Converting collection to #{target_type.to_s.upcase}..." if verbose
|
|
70
|
+
|
|
71
|
+
# Step 1: Unpack - extract fonts from source collection
|
|
72
|
+
puts " Unpacking fonts from source collection..." if verbose
|
|
73
|
+
fonts, source_type = unpack_fonts(collection_path)
|
|
74
|
+
|
|
75
|
+
# Check if conversion is needed
|
|
76
|
+
if source_type == target_type
|
|
77
|
+
puts " Source and target formats are the same, copying collection..." if verbose
|
|
78
|
+
FileUtils.cp(collection_path, output_path)
|
|
79
|
+
return build_result(collection_path, output_path, source_type, target_type, fonts.size, [])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Step 2: Convert - transform fonts if requested
|
|
83
|
+
puts " Converting #{fonts.size} font(s)..." if verbose
|
|
84
|
+
converted_fonts, conversions = convert_fonts(fonts, source_type, target_type, options.merge(target_format: target_format))
|
|
85
|
+
|
|
86
|
+
# Step 3: Repack - build target collection
|
|
87
|
+
puts " Repacking into #{target_type.to_s.upcase} format..." if verbose
|
|
88
|
+
repack_fonts(converted_fonts, target_type, output_path, options)
|
|
89
|
+
|
|
90
|
+
# Build result
|
|
91
|
+
result = build_result(collection_path, output_path, source_type, target_type, fonts.size, conversions)
|
|
92
|
+
|
|
93
|
+
if verbose
|
|
94
|
+
display_result(result)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Validate conversion parameters
|
|
103
|
+
#
|
|
104
|
+
# @param collection_path [String] Collection path
|
|
105
|
+
# @param target_type [Symbol] Target type
|
|
106
|
+
# @param options [Hash] Options
|
|
107
|
+
# @raise [ArgumentError] if invalid
|
|
108
|
+
def validate_parameters!(collection_path, target_type, options)
|
|
109
|
+
unless File.exist?(collection_path)
|
|
110
|
+
raise ArgumentError, "Collection file not found: #{collection_path}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unless %i[ttc otc dfont].include?(target_type)
|
|
114
|
+
raise ArgumentError, "Invalid target type: #{target_type}. Must be :ttc, :otc, or :dfont"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
unless options[:output]
|
|
118
|
+
raise ArgumentError, "Output path is required (:output option)"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Unpack fonts from source collection
|
|
123
|
+
#
|
|
124
|
+
# @param collection_path [String] Collection path
|
|
125
|
+
# @return [Array<(Array<Font>, Symbol)>] Array of [fonts, source_type]
|
|
126
|
+
# @raise [Error] if unpacking fails
|
|
127
|
+
def unpack_fonts(collection_path)
|
|
128
|
+
# Detect collection type
|
|
129
|
+
source_type = detect_collection_type(collection_path)
|
|
130
|
+
|
|
131
|
+
fonts = case source_type
|
|
132
|
+
when :ttc, :otc
|
|
133
|
+
unpack_ttc_otc(collection_path)
|
|
134
|
+
when :dfont
|
|
135
|
+
unpack_dfont(collection_path)
|
|
136
|
+
else
|
|
137
|
+
raise Error, "Unknown collection type: #{source_type}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
[fonts, source_type]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Detect collection type from file
|
|
144
|
+
#
|
|
145
|
+
# @param path [String] Collection path
|
|
146
|
+
# @return [Symbol] Collection type (:ttc, :otc, or :dfont)
|
|
147
|
+
def detect_collection_type(path)
|
|
148
|
+
File.open(path, "rb") do |io|
|
|
149
|
+
signature = io.read(4)
|
|
150
|
+
io.rewind
|
|
151
|
+
|
|
152
|
+
if signature == "ttcf"
|
|
153
|
+
# TTC or OTC - check extension
|
|
154
|
+
ext = File.extname(path).downcase
|
|
155
|
+
ext == ".otc" ? :otc : :ttc
|
|
156
|
+
elsif Parsers::DfontParser.dfont?(io)
|
|
157
|
+
:dfont
|
|
158
|
+
else
|
|
159
|
+
raise Error, "Not a valid collection file: #{path}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Unpack fonts from TTC/OTC
|
|
165
|
+
#
|
|
166
|
+
# @param path [String] TTC/OTC path
|
|
167
|
+
# @return [Array<Font>] Unpacked fonts
|
|
168
|
+
def unpack_ttc_otc(path)
|
|
169
|
+
collection = FontLoader.load_collection(path)
|
|
170
|
+
|
|
171
|
+
File.open(path, "rb") do |io|
|
|
172
|
+
collection.extract_fonts(io)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Unpack fonts from dfont
|
|
177
|
+
#
|
|
178
|
+
# @param path [String] dfont path
|
|
179
|
+
# @return [Array<Font>] Unpacked fonts
|
|
180
|
+
def unpack_dfont(path)
|
|
181
|
+
fonts = []
|
|
182
|
+
|
|
183
|
+
File.open(path, "rb") do |io|
|
|
184
|
+
count = Parsers::DfontParser.sfnt_count(io)
|
|
185
|
+
|
|
186
|
+
count.times do |index|
|
|
187
|
+
sfnt_data = Parsers::DfontParser.extract_sfnt(io, index: index)
|
|
188
|
+
|
|
189
|
+
# Load font from SFNT binary
|
|
190
|
+
font = FontLoader.load_from_binary(sfnt_data)
|
|
191
|
+
fonts << font
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
fonts
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Convert fonts if outline format change needed
|
|
199
|
+
#
|
|
200
|
+
# @param fonts [Array<Font>] Source fonts
|
|
201
|
+
# @param source_type [Symbol] Source collection type
|
|
202
|
+
# @param target_type [Symbol] Target collection type
|
|
203
|
+
# @param options [Hash] Conversion options
|
|
204
|
+
# @return [Array<(Array<Font>, Array<Hash>)>] [converted_fonts, conversions]
|
|
205
|
+
def convert_fonts(fonts, source_type, target_type, options)
|
|
206
|
+
converted_fonts = []
|
|
207
|
+
conversions = []
|
|
208
|
+
|
|
209
|
+
# Determine if outline conversion is needed
|
|
210
|
+
target_format = options.fetch(:target_format, 'preserve').to_s
|
|
211
|
+
|
|
212
|
+
fonts.each_with_index do |font, index|
|
|
213
|
+
source_format = detect_font_format(font)
|
|
214
|
+
needs_conversion = outline_conversion_needed?(source_format, target_format)
|
|
215
|
+
|
|
216
|
+
if needs_conversion
|
|
217
|
+
# Convert outline format
|
|
218
|
+
desired_format = target_format == 'preserve' ? source_format : target_format.to_sym
|
|
219
|
+
converter = FormatConverter.new
|
|
220
|
+
|
|
221
|
+
begin
|
|
222
|
+
tables = converter.convert(font, desired_format, options)
|
|
223
|
+
converted_font = build_font_from_tables(tables, desired_format)
|
|
224
|
+
converted_fonts << converted_font
|
|
225
|
+
|
|
226
|
+
conversions << {
|
|
227
|
+
index: index,
|
|
228
|
+
source_format: source_format,
|
|
229
|
+
target_format: desired_format,
|
|
230
|
+
status: :converted,
|
|
231
|
+
}
|
|
232
|
+
rescue Error => e
|
|
233
|
+
# If conversion fails, keep original for dfont (supports mixed)
|
|
234
|
+
if target_type == :dfont
|
|
235
|
+
converted_fonts << font
|
|
236
|
+
conversions << {
|
|
237
|
+
index: index,
|
|
238
|
+
source_format: source_format,
|
|
239
|
+
target_format: source_format,
|
|
240
|
+
status: :preserved,
|
|
241
|
+
note: "Conversion failed, kept original: #{e.message}",
|
|
242
|
+
}
|
|
243
|
+
else
|
|
244
|
+
raise Error, "Font #{index} conversion failed: #{e.message}"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
else
|
|
248
|
+
# No conversion needed, use original
|
|
249
|
+
converted_fonts << font
|
|
250
|
+
conversions << {
|
|
251
|
+
index: index,
|
|
252
|
+
source_format: source_format,
|
|
253
|
+
target_format: source_format,
|
|
254
|
+
status: :preserved,
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
[converted_fonts, conversions]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Check if outline conversion is needed
|
|
263
|
+
#
|
|
264
|
+
# @param source_format [Symbol] Source font format (:ttf or :otf)
|
|
265
|
+
# @param target_format [String] Target format ('preserve', 'ttf', or 'otf')
|
|
266
|
+
# @return [Boolean] true if conversion needed
|
|
267
|
+
def outline_conversion_needed?(source_format, target_format)
|
|
268
|
+
# 'preserve' means keep original format
|
|
269
|
+
return false if target_format == 'preserve'
|
|
270
|
+
|
|
271
|
+
# Convert if target format differs from source
|
|
272
|
+
target_format.to_sym != source_format
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Determine target outline format for a font
|
|
276
|
+
#
|
|
277
|
+
# @param target_type [Symbol] Target collection type
|
|
278
|
+
# @param font [Font] Font object
|
|
279
|
+
# @return [Symbol] Target outline format (:ttf or :otf)
|
|
280
|
+
def target_outline_format(target_type, font)
|
|
281
|
+
case target_type
|
|
282
|
+
when :ttc
|
|
283
|
+
:ttf # TTC requires TrueType
|
|
284
|
+
when :otc
|
|
285
|
+
:otf # OTC requires OpenType/CFF
|
|
286
|
+
when :dfont
|
|
287
|
+
# dfont preserves original format
|
|
288
|
+
detect_font_format(font)
|
|
289
|
+
else
|
|
290
|
+
detect_font_format(font)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Detect font outline format
|
|
295
|
+
#
|
|
296
|
+
# @param font [Font] Font object
|
|
297
|
+
# @return [Symbol] Format (:ttf or :otf)
|
|
298
|
+
def detect_font_format(font)
|
|
299
|
+
if font.has_table?("CFF ") || font.has_table?("CFF2")
|
|
300
|
+
:otf
|
|
301
|
+
elsif font.has_table?("glyf")
|
|
302
|
+
:ttf
|
|
303
|
+
else
|
|
304
|
+
raise Error, "Cannot detect font format"
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Build font object from tables
|
|
309
|
+
#
|
|
310
|
+
# @param tables [Hash] Table data
|
|
311
|
+
# @param format [Symbol] Font format
|
|
312
|
+
# @return [Font] Font object
|
|
313
|
+
def build_font_from_tables(tables, format)
|
|
314
|
+
# Create temporary font from tables
|
|
315
|
+
require_relative "../font_writer"
|
|
316
|
+
require "stringio"
|
|
317
|
+
|
|
318
|
+
sfnt_version = format == :otf ? 0x4F54544F : 0x00010000
|
|
319
|
+
binary = FontWriter.write_font(tables, sfnt_version: sfnt_version)
|
|
320
|
+
|
|
321
|
+
# Load font from binary using StringIO
|
|
322
|
+
sfnt_io = StringIO.new(binary)
|
|
323
|
+
signature = sfnt_io.read(4)
|
|
324
|
+
sfnt_io.rewind
|
|
325
|
+
|
|
326
|
+
# Create font based on signature
|
|
327
|
+
case signature
|
|
328
|
+
when [Constants::SFNT_VERSION_TRUETYPE].pack("N"), "true"
|
|
329
|
+
font = TrueTypeFont.read(sfnt_io)
|
|
330
|
+
font.initialize_storage
|
|
331
|
+
font.loading_mode = LoadingModes::FULL
|
|
332
|
+
font.lazy_load_enabled = false
|
|
333
|
+
font.read_table_data(sfnt_io)
|
|
334
|
+
font
|
|
335
|
+
when "OTTO"
|
|
336
|
+
font = OpenTypeFont.read(sfnt_io)
|
|
337
|
+
font.initialize_storage
|
|
338
|
+
font.loading_mode = LoadingModes::FULL
|
|
339
|
+
font.lazy_load_enabled = false
|
|
340
|
+
font.read_table_data(sfnt_io)
|
|
341
|
+
font
|
|
342
|
+
else
|
|
343
|
+
raise Error, "Invalid SFNT signature: #{signature.inspect}"
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Repack fonts into target collection
|
|
348
|
+
#
|
|
349
|
+
# @param fonts [Array<Font>] Fonts to pack
|
|
350
|
+
# @param target_type [Symbol] Target type
|
|
351
|
+
# @param output_path [String] Output path
|
|
352
|
+
# @param options [Hash] Packing options
|
|
353
|
+
# @return [void]
|
|
354
|
+
def repack_fonts(fonts, target_type, output_path, options)
|
|
355
|
+
case target_type
|
|
356
|
+
when :ttc, :otc
|
|
357
|
+
repack_ttc_otc(fonts, target_type, output_path, options)
|
|
358
|
+
when :dfont
|
|
359
|
+
repack_dfont(fonts, output_path, options)
|
|
360
|
+
else
|
|
361
|
+
raise Error, "Unknown target type: #{target_type}"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Repack fonts into TTC/OTC
|
|
366
|
+
#
|
|
367
|
+
# @param fonts [Array<Font>] Fonts
|
|
368
|
+
# @param target_type [Symbol] :ttc or :otc
|
|
369
|
+
# @param output_path [String] Output path
|
|
370
|
+
# @param options [Hash] Options
|
|
371
|
+
# @return [void]
|
|
372
|
+
def repack_ttc_otc(fonts, target_type, output_path, options)
|
|
373
|
+
optimize = options.fetch(:optimize, true)
|
|
374
|
+
|
|
375
|
+
builder = Collection::Builder.new(
|
|
376
|
+
fonts,
|
|
377
|
+
format: target_type,
|
|
378
|
+
optimize: optimize,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
builder.build_to_file(output_path)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Repack fonts into dfont
|
|
385
|
+
#
|
|
386
|
+
# @param fonts [Array<Font>] Fonts
|
|
387
|
+
# @param output_path [String] Output path
|
|
388
|
+
# @param options [Hash] Options
|
|
389
|
+
# @return [void]
|
|
390
|
+
def repack_dfont(fonts, output_path, _options)
|
|
391
|
+
builder = Collection::DfontBuilder.new(fonts)
|
|
392
|
+
builder.build_to_file(output_path)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Build conversion result
|
|
396
|
+
#
|
|
397
|
+
# @param input [String] Input path
|
|
398
|
+
# @param output [String] Output path
|
|
399
|
+
# @param source_type [Symbol] Source type
|
|
400
|
+
# @param target_type [Symbol] Target type
|
|
401
|
+
# @param num_fonts [Integer] Number of fonts
|
|
402
|
+
# @param conversions [Array<Hash>] Conversion details
|
|
403
|
+
# @return [Hash] Result
|
|
404
|
+
def build_result(input, output, source_type, target_type, num_fonts, conversions)
|
|
405
|
+
{
|
|
406
|
+
input: input,
|
|
407
|
+
output: output,
|
|
408
|
+
source_type: source_type,
|
|
409
|
+
target_type: target_type,
|
|
410
|
+
num_fonts: num_fonts,
|
|
411
|
+
conversions: conversions,
|
|
412
|
+
}
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Display conversion result
|
|
416
|
+
#
|
|
417
|
+
# @param result [Hash] Result
|
|
418
|
+
# @return [void]
|
|
419
|
+
def display_result(result)
|
|
420
|
+
puts "\n=== Collection Conversion Complete ==="
|
|
421
|
+
puts "Input: #{result[:input]}"
|
|
422
|
+
puts "Output: #{result[:output]}"
|
|
423
|
+
puts "Source format: #{result[:source_type].to_s.upcase}"
|
|
424
|
+
puts "Target format: #{result[:target_type].to_s.upcase}"
|
|
425
|
+
puts "Fonts: #{result[:num_fonts]}"
|
|
426
|
+
|
|
427
|
+
if result[:conversions].any?
|
|
428
|
+
converted_count = result[:conversions].count { |c| c[:status] == :converted }
|
|
429
|
+
if converted_count.positive?
|
|
430
|
+
puts "Outline conversions: #{converted_count}"
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
puts ""
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
@@ -6,7 +6,8 @@ require_relative "../woff2/directory"
|
|
|
6
6
|
require_relative "../woff2/table_transformer"
|
|
7
7
|
require_relative "../utilities/brotli_wrapper"
|
|
8
8
|
require_relative "../utilities/checksum_calculator"
|
|
9
|
-
|
|
9
|
+
# Validation temporarily disabled - will be reimplemented with new DSL framework in Week 3+
|
|
10
|
+
# require_relative "../validation/woff2_validator"
|
|
10
11
|
require "yaml"
|
|
11
12
|
require "stringio"
|
|
12
13
|
|
|
@@ -111,10 +112,11 @@ module Fontisan
|
|
|
111
112
|
result = { woff2_binary: woff2_binary }
|
|
112
113
|
|
|
113
114
|
# Optional validation
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
# Temporarily disabled - will be reimplemented with new DSL framework
|
|
116
|
+
# if options[:validate]
|
|
117
|
+
# validation_report = validate_encoding(woff2_binary, options)
|
|
118
|
+
# result[:validation_report] = validation_report
|
|
119
|
+
# end
|
|
118
120
|
|
|
119
121
|
result
|
|
120
122
|
end
|
|
@@ -162,30 +164,6 @@ module Fontisan
|
|
|
162
164
|
|
|
163
165
|
private
|
|
164
166
|
|
|
165
|
-
# Validate encoded WOFF2 binary
|
|
166
|
-
#
|
|
167
|
-
# @param woff2_binary [String] Encoded WOFF2 data
|
|
168
|
-
# @param options [Hash] Validation options
|
|
169
|
-
# @return [Models::ValidationReport] Validation report
|
|
170
|
-
def validate_encoding(woff2_binary, options)
|
|
171
|
-
# Load the encoded WOFF2 from memory
|
|
172
|
-
io = StringIO.new(woff2_binary)
|
|
173
|
-
woff2_font = Woff2Font.from_file_io(io, "encoded.woff2")
|
|
174
|
-
|
|
175
|
-
# Run validation
|
|
176
|
-
validation_level = options[:validation_level] || :standard
|
|
177
|
-
validator = Validation::Woff2Validator.new(level: validation_level)
|
|
178
|
-
validator.validate(woff2_font, "encoded.woff2")
|
|
179
|
-
rescue StandardError => e
|
|
180
|
-
# If validation fails, create a report with the error
|
|
181
|
-
report = Models::ValidationReport.new(
|
|
182
|
-
font_path: "encoded.woff2",
|
|
183
|
-
valid: false,
|
|
184
|
-
)
|
|
185
|
-
report.add_error("woff2_validation", "Validation failed: #{e.message}", nil)
|
|
186
|
-
report
|
|
187
|
-
end
|
|
188
|
-
|
|
189
167
|
# Helper method to load WOFF2 from StringIO
|
|
190
168
|
#
|
|
191
169
|
# This is added to Woff2Font to support in-memory validation
|