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
data/README.adoc
CHANGED
|
@@ -71,7 +71,7 @@ gem install fontisan
|
|
|
71
71
|
* Collection management (pack/unpack TTC/OTC files with table deduplication)
|
|
72
72
|
* Support for TTF, OTF, TTC, OTC font formats (production ready)
|
|
73
73
|
* WOFF format support (reading complete, writing functional, pending full integration)
|
|
74
|
-
* WOFF2 format support (reading
|
|
74
|
+
* WOFF2 format support (reading complete with table transformations, writing planned)
|
|
75
75
|
* SVG font generation (complete)
|
|
76
76
|
* TTX/YAML/JSON export (complete)
|
|
77
77
|
* Command-line interface with 18 commands
|
|
@@ -85,8 +85,14 @@ gem install fontisan
|
|
|
85
85
|
* Compound glyph decomposition with transformation support (complete)
|
|
86
86
|
* CFF subroutine optimization for space-efficient OTF generation (preview mode)
|
|
87
87
|
* Various loading modes for high-performance font indexing (5x faster)
|
|
88
|
+
* Hint preservation during TTF ↔ OTF conversion (optional, partially complete)
|
|
89
|
+
* CFF2 variable font support for PostScript hint conversion (complete)
|
|
88
90
|
|
|
89
|
-
NOTE: TTF ↔ OTF outline format conversion is in active development (
|
|
91
|
+
NOTE: TTF ↔ OTF outline format conversion is in active development (~80%
|
|
92
|
+
complete). The universal outline model, CFF builders, curve converter, and
|
|
93
|
+
compound glyph support are fully functional. Simple and compound glyphs convert
|
|
94
|
+
successfully. See link:docs/IMPLEMENTATION_STATUS_V4.md[Implementation Status]
|
|
95
|
+
for detailed progress tracking.
|
|
90
96
|
|
|
91
97
|
|
|
92
98
|
== Loading Modes
|
|
@@ -160,6 +166,7 @@ font.table_available?(tag) # => boolean
|
|
|
160
166
|
# Access restricted based on mode
|
|
161
167
|
font.table(tag) # => Returns table or raises error
|
|
162
168
|
----
|
|
169
|
+
"
|
|
163
170
|
|
|
164
171
|
|
|
165
172
|
|
|
@@ -256,7 +263,7 @@ Fontisan supports bidirectional conversion between TrueType (TTF) and OpenType/C
|
|
|
256
263
|
|
|
257
264
|
The outline converter enables transformation between glyph outline formats:
|
|
258
265
|
|
|
259
|
-
* **TrueType (TTF)**: Uses quadratic Bézier curves stored in glyf/
|
|
266
|
+
* **TrueType (TTF)**: Uses quadratic Bézier curves stored in glyf/loca tables
|
|
260
267
|
* **OpenType/CFF (OTF)**: Uses cubic Bézier curves stored in CFF table
|
|
261
268
|
|
|
262
269
|
Conversion uses a format-agnostic universal outline model as an intermediate representation, ensuring high-quality results while preserving glyph metrics and bounding boxes.
|
|
@@ -297,6 +304,11 @@ fontisan convert font.ttf --to cff --output font.otf
|
|
|
297
304
|
|
|
298
305
|
==== Validation
|
|
299
306
|
|
|
307
|
+
Font integrity validation is now enabled by default for all conversions.
|
|
308
|
+
The validator ensures proper OpenType checksum calculation including correct
|
|
309
|
+
handling of the head table's checksumAdjustment field per the OpenType
|
|
310
|
+
specification.
|
|
311
|
+
|
|
300
312
|
After conversion, validate the output font:
|
|
301
313
|
|
|
302
314
|
[source,bash]
|
|
@@ -1325,6 +1337,148 @@ Instance 2 position: 75 300
|
|
|
1325
1337
|
----
|
|
1326
1338
|
====
|
|
1327
1339
|
|
|
1340
|
+
==== Generate static instances from variable fonts
|
|
1341
|
+
|
|
1342
|
+
Generate static font instances from variable fonts at specific variation coordinates
|
|
1343
|
+
and output in any supported format (TTF, OTF, WOFF).
|
|
1344
|
+
|
|
1345
|
+
Syntax:
|
|
1346
|
+
|
|
1347
|
+
[source,shell]
|
|
1348
|
+
----
|
|
1349
|
+
$ fontisan instance VARIABLE_FONT [OPTIONS]
|
|
1350
|
+
----
|
|
1351
|
+
|
|
1352
|
+
Where,
|
|
1353
|
+
|
|
1354
|
+
`VARIABLE_FONT`:: Path to the variable font file
|
|
1355
|
+
`OPTIONS`:: Instance generation options
|
|
1356
|
+
|
|
1357
|
+
Options:
|
|
1358
|
+
|
|
1359
|
+
`--wght VALUE`:: Weight axis value
|
|
1360
|
+
`--wdth VALUE`:: Width axis value
|
|
1361
|
+
`--slnt VALUE`:: Slant axis value
|
|
1362
|
+
`--ital VALUE`:: Italic axis value
|
|
1363
|
+
`--opsz VALUE`:: Optical size axis value
|
|
1364
|
+
`--to FORMAT`:: Output format: `ttf` (default), `otf`, `woff`, or `woff2`
|
|
1365
|
+
`--output FILE`:: Output file path
|
|
1366
|
+
`--optimize`:: Enable CFF optimization for OTF output
|
|
1367
|
+
`--named-instance INDEX`:: Use named instance by index
|
|
1368
|
+
`--list-instances`:: List available named instances
|
|
1369
|
+
`--validate`:: Validate font before generation
|
|
1370
|
+
`--dry-run`:: Preview instance without generating
|
|
1371
|
+
`--progress`:: Show progress during generation
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
.Generate bold instance at wght=700
|
|
1375
|
+
[example]
|
|
1376
|
+
====
|
|
1377
|
+
[source,shell]
|
|
1378
|
+
----
|
|
1379
|
+
$ fontisan instance variable.ttf --wght 700 --output bold.ttf
|
|
1380
|
+
|
|
1381
|
+
Generating instance... done
|
|
1382
|
+
Writing output... done
|
|
1383
|
+
Static font instance written to: bold.ttf
|
|
1384
|
+
----
|
|
1385
|
+
====
|
|
1386
|
+
|
|
1387
|
+
.Generate instance and convert to OTF
|
|
1388
|
+
[example]
|
|
1389
|
+
====
|
|
1390
|
+
[source,shell]
|
|
1391
|
+
----
|
|
1392
|
+
$ fontisan instance variable.ttf --wght 300 --to otf --output light.otf
|
|
1393
|
+
|
|
1394
|
+
Generating instance... done
|
|
1395
|
+
Writing output... done
|
|
1396
|
+
Static font instance written to: light.otf
|
|
1397
|
+
----
|
|
1398
|
+
====
|
|
1399
|
+
|
|
1400
|
+
.Generate instance and convert to WOFF
|
|
1401
|
+
[example]
|
|
1402
|
+
====
|
|
1403
|
+
[source,shell]
|
|
1404
|
+
----
|
|
1405
|
+
$ fontisan instance variable.ttf --wght 600 --to woff --output semibold.woff
|
|
1406
|
+
|
|
1407
|
+
Generating instance... done
|
|
1408
|
+
Writing output... done
|
|
1409
|
+
Static font instance written to: semibold.woff
|
|
1410
|
+
----
|
|
1411
|
+
====
|
|
1412
|
+
|
|
1413
|
+
.Generate instance with multiple axes
|
|
1414
|
+
[example]
|
|
1415
|
+
====
|
|
1416
|
+
[source,shell]
|
|
1417
|
+
----
|
|
1418
|
+
$ fontisan instance variable.ttf --wght 600 --wdth 75 --output condensed.ttf
|
|
1419
|
+
|
|
1420
|
+
Generating instance... done
|
|
1421
|
+
Writing output... done
|
|
1422
|
+
Static font instance written to: condensed.ttf
|
|
1423
|
+
----
|
|
1424
|
+
====
|
|
1425
|
+
|
|
1426
|
+
.List available named instances
|
|
1427
|
+
[example]
|
|
1428
|
+
====
|
|
1429
|
+
[source,shell]
|
|
1430
|
+
----
|
|
1431
|
+
$ fontisan instance variable.ttf --list-instances
|
|
1432
|
+
|
|
1433
|
+
Available named instances:
|
|
1434
|
+
|
|
1435
|
+
[0] Instance 4
|
|
1436
|
+
Coordinates:
|
|
1437
|
+
wdth: 75.0
|
|
1438
|
+
wght: 200.0
|
|
1439
|
+
|
|
1440
|
+
[1] Instance 5
|
|
1441
|
+
Coordinates:
|
|
1442
|
+
wdth: 75.0
|
|
1443
|
+
wght: 250.0
|
|
1444
|
+
|
|
1445
|
+
[2] Instance 6
|
|
1446
|
+
Coordinates:
|
|
1447
|
+
wdth: 75.0
|
|
1448
|
+
wght: 300.0
|
|
1449
|
+
----
|
|
1450
|
+
====
|
|
1451
|
+
|
|
1452
|
+
.Use named instance
|
|
1453
|
+
[example]
|
|
1454
|
+
====
|
|
1455
|
+
[source,shell]
|
|
1456
|
+
----
|
|
1457
|
+
$ fontisan instance variable.ttf --named-instance 0 --output thin.ttf
|
|
1458
|
+
----
|
|
1459
|
+
====
|
|
1460
|
+
|
|
1461
|
+
.Preview instance generation (dry-run)
|
|
1462
|
+
[example]
|
|
1463
|
+
====
|
|
1464
|
+
[source,shell]
|
|
1465
|
+
----
|
|
1466
|
+
$ fontisan instance variable.ttf --wght 700 --dry-run
|
|
1467
|
+
|
|
1468
|
+
Dry-run mode: Preview of instance generation
|
|
1469
|
+
|
|
1470
|
+
Coordinates:
|
|
1471
|
+
wght: 700.0
|
|
1472
|
+
|
|
1473
|
+
Output would be written to: variable-instance.ttf
|
|
1474
|
+
Output
|
|
1475
|
+
|
|
1476
|
+
format: same as input
|
|
1477
|
+
|
|
1478
|
+
Use without --dry-run to actually generate the instance.
|
|
1479
|
+
----
|
|
1480
|
+
====
|
|
1481
|
+
|
|
1328
1482
|
==== Optical size information
|
|
1329
1483
|
|
|
1330
1484
|
Display optical size range from the OS/2 table for fonts designed for specific
|
|
@@ -1644,7 +1798,7 @@ Font 3: Noto Serif CJK TC
|
|
|
1644
1798
|
|
|
1645
1799
|
===== Show collection info
|
|
1646
1800
|
|
|
1647
|
-
Show detailed information about a TrueType Collection (TTC)
|
|
1801
|
+
Show detailed information about a TrueType Collection (TTC) or OpenType Collection
|
|
1648
1802
|
(OTC), including the number of fonts and metadata for each font.
|
|
1649
1803
|
|
|
1650
1804
|
[source,shell]
|
data/Rakefile
CHANGED
|
@@ -9,7 +9,8 @@ require "rubocop/rake_task"
|
|
|
9
9
|
RuboCop::RakeTask.new
|
|
10
10
|
|
|
11
11
|
namespace :fixtures do
|
|
12
|
-
|
|
12
|
+
# Load centralized fixture configuration
|
|
13
|
+
require_relative "spec/support/fixture_fonts"
|
|
13
14
|
|
|
14
15
|
# Helper method to download a single file
|
|
15
16
|
def download_single_file(name, url, target_path)
|
|
@@ -30,61 +31,58 @@ namespace :fixtures do
|
|
|
30
31
|
require "open-uri"
|
|
31
32
|
require "zip"
|
|
32
33
|
|
|
33
|
-
zip_file = "#{target_dir}/#{name}.zip"
|
|
34
|
-
|
|
35
34
|
puts "[fixtures:download] Downloading #{name}..."
|
|
36
35
|
FileUtils.mkdir_p(target_dir)
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
# Create a manual temp file path - OS will clean up temp files automatically
|
|
38
|
+
temp_path = File.join(Dir.tmpdir, "fontisan_#{name}_#{Process.pid}_#{rand(10000)}.zip")
|
|
39
|
+
|
|
40
|
+
# Download using IO.copy_stream for better Windows compatibility
|
|
41
|
+
URI.open(url, "rb") do |remote|
|
|
42
|
+
File.open(temp_path, "wb") do |file|
|
|
43
|
+
IO.copy_stream(remote, file)
|
|
44
|
+
end
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
puts "[fixtures:download] Extracting #{name}..."
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
|
|
49
|
+
# Open zip file and ensure it's fully closed before we're done
|
|
50
|
+
zip_file = Zip::File.open(temp_path)
|
|
51
|
+
begin
|
|
52
|
+
zip_file.each do |entry|
|
|
53
|
+
# Skip macOS metadata files and directories
|
|
54
|
+
next if entry.name.start_with?("__MACOSX/") || entry.name.include?("/._")
|
|
55
|
+
next if entry.directory?
|
|
56
|
+
|
|
57
|
+
# Ensure entry.name is relative by stripping leading slashes
|
|
58
|
+
relative_name = entry.name.sub(%r{^/+}, "")
|
|
59
|
+
|
|
60
|
+
dest_path = File.join(target_dir, relative_name)
|
|
46
61
|
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
47
|
-
|
|
62
|
+
|
|
63
|
+
# Skip if file already exists
|
|
64
|
+
next if File.exist?(dest_path)
|
|
65
|
+
|
|
66
|
+
# Write the file content directly using binary mode
|
|
67
|
+
File.open(dest_path, "wb") do |file|
|
|
68
|
+
IO.copy_stream(entry.get_input_stream, file)
|
|
69
|
+
end
|
|
48
70
|
end
|
|
71
|
+
ensure
|
|
72
|
+
# Explicitly close the zip file to release file handle on Windows
|
|
73
|
+
zip_file.close if zip_file
|
|
49
74
|
end
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
# Temp file left in Dir.tmpdir - OS will clean it up automatically
|
|
77
|
+
|
|
52
78
|
puts "[fixtures:download] #{name} downloaded successfully"
|
|
53
79
|
rescue LoadError => e
|
|
54
80
|
warn "[fixtures:download] Error: Required gem not installed. Please run: gem install rubyzip"
|
|
55
81
|
raise e
|
|
56
82
|
end
|
|
57
83
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
fonts = {
|
|
61
|
-
"NotoSans" => {
|
|
62
|
-
url: "https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Regular.ttf",
|
|
63
|
-
target_dir: fixtures_dir.to_s,
|
|
64
|
-
marker: "#{fixtures_dir}/NotoSans-Regular.ttf",
|
|
65
|
-
single_file: true,
|
|
66
|
-
},
|
|
67
|
-
"Libertinus" => {
|
|
68
|
-
url: "https://github.com/alerque/libertinus/releases/download/v7.051/Libertinus-7.051.zip",
|
|
69
|
-
target_dir: "#{fixtures_dir}/libertinus",
|
|
70
|
-
marker: "#{fixtures_dir}/libertinus/Libertinus-7.051/static/OTF/LibertinusSerif-Regular.otf",
|
|
71
|
-
},
|
|
72
|
-
"MonaSans" => {
|
|
73
|
-
url: "https://github.com/github/mona-sans/releases/download/v2.0/MonaSans.zip",
|
|
74
|
-
target_dir: "#{fixtures_dir}/MonaSans",
|
|
75
|
-
marker: "#{fixtures_dir}/MonaSans/MonaSans/variable/MonaSans[wdth,wght].ttf",
|
|
76
|
-
},
|
|
77
|
-
"NotoSerifCJK" => {
|
|
78
|
-
url: "https://github.com/notofonts/noto-cjk/releases/download/Serif2.003/01_NotoSerifCJK.ttc.zip",
|
|
79
|
-
target_dir: "#{fixtures_dir}/NotoSerifCJK",
|
|
80
|
-
marker: "#{fixtures_dir}/NotoSerifCJK/NotoSerifCJK.ttc",
|
|
81
|
-
},
|
|
82
|
-
"NotoSerifCJK-VF" => {
|
|
83
|
-
url: "https://github.com/notofonts/noto-cjk/releases/download/Serif2.003/02_NotoSerifCJK-OTF-VF.zip",
|
|
84
|
-
target_dir: "#{fixtures_dir}/NotoSerifCJK-VF",
|
|
85
|
-
marker: "#{fixtures_dir}/NotoSerifCJK-VF/Variable/OTC/NotoSerifCJK-VF.otf.ttc",
|
|
86
|
-
},
|
|
87
|
-
}
|
|
84
|
+
# Get font configurations from centralized source
|
|
85
|
+
fonts = FixtureFonts.rakefile_config
|
|
88
86
|
|
|
89
87
|
# Create file tasks for each font
|
|
90
88
|
fonts.each do |name, config|
|
|
@@ -102,13 +100,12 @@ namespace :fixtures do
|
|
|
102
100
|
|
|
103
101
|
desc "Clean downloaded fixtures"
|
|
104
102
|
task :clean do
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
end
|
|
103
|
+
fonts.values.map { |config| config[:target_dir] }.each do |path|
|
|
104
|
+
next unless File.exist?(path)
|
|
105
|
+
|
|
106
|
+
puts "[fixtures:clean] Removing #{path}..."
|
|
107
|
+
FileUtils.rm_rf(path)
|
|
108
|
+
puts "[fixtures:clean] Removed #{path}"
|
|
112
109
|
end
|
|
113
110
|
end
|
|
114
111
|
end
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -25,16 +25,21 @@ module Fontisan
|
|
|
25
25
|
desc: "Suppress non-error output",
|
|
26
26
|
aliases: "-q"
|
|
27
27
|
|
|
28
|
-
desc "info
|
|
28
|
+
desc "info PATH", "Display font information"
|
|
29
29
|
# Extract and display comprehensive font metadata.
|
|
30
30
|
#
|
|
31
|
-
# @param
|
|
32
|
-
def info(
|
|
33
|
-
command = Commands::InfoCommand.new(
|
|
34
|
-
|
|
35
|
-
output_result(
|
|
36
|
-
rescue Errno::ENOENT
|
|
37
|
-
|
|
31
|
+
# @param path [String] Path to the font file or collection
|
|
32
|
+
def info(path)
|
|
33
|
+
command = Commands::InfoCommand.new(path, options)
|
|
34
|
+
info = command.run
|
|
35
|
+
output_result(info) unless options[:quiet]
|
|
36
|
+
rescue Errno::ENOENT => e
|
|
37
|
+
if options[:verbose]
|
|
38
|
+
raise
|
|
39
|
+
else
|
|
40
|
+
warn "File not found: #{path}" unless options[:quiet]
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
desc "ls FILE", "List contents (fonts in collection or font summary)"
|
|
@@ -195,47 +200,79 @@ module Fontisan
|
|
|
195
200
|
|
|
196
201
|
desc "convert FONT_FILE", "Convert font to different format"
|
|
197
202
|
option :to, type: :string, required: true,
|
|
198
|
-
desc: "Target format (ttf, otf,
|
|
203
|
+
desc: "Target format (ttf, otf, woff, woff2)",
|
|
199
204
|
aliases: "-t"
|
|
200
205
|
option :output, type: :string, required: true,
|
|
201
206
|
desc: "Output file path",
|
|
202
207
|
aliases: "-o"
|
|
203
|
-
option :
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
option :
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
option :coordinates, type: :string,
|
|
209
|
+
desc: "Instance coordinates (e.g., wght=700,wdth=100)",
|
|
210
|
+
aliases: "-c"
|
|
211
|
+
option :instance_index, type: :numeric,
|
|
212
|
+
desc: "Named instance index",
|
|
213
|
+
aliases: "-n"
|
|
214
|
+
option :preserve_variation, type: :boolean,
|
|
215
|
+
desc: "Force variation preservation (auto-detected by default)"
|
|
216
|
+
option :no_validate, type: :boolean, default: false,
|
|
217
|
+
desc: "Skip output validation"
|
|
218
|
+
option :preserve_hints, type: :boolean, default: false,
|
|
219
|
+
desc: "Preserve rendering hints during conversion (TTF→OTF preservations may be limited)"
|
|
220
|
+
option :wght, type: :numeric,
|
|
221
|
+
desc: "Weight axis value (alternative to --coordinates)"
|
|
222
|
+
option :wdth, type: :numeric,
|
|
223
|
+
desc: "Width axis value (alternative to --coordinates)"
|
|
224
|
+
option :slnt, type: :numeric,
|
|
225
|
+
desc: "Slant axis value (alternative to --coordinates)"
|
|
226
|
+
option :ital, type: :numeric,
|
|
227
|
+
desc: "Italic axis value (alternative to --coordinates)"
|
|
228
|
+
option :opsz, type: :numeric,
|
|
229
|
+
desc: "Optical size axis value (alternative to --coordinates)"
|
|
230
|
+
# Convert a font to a different format using the universal transformation pipeline.
|
|
212
231
|
#
|
|
213
232
|
# Supported conversions:
|
|
214
|
-
# -
|
|
215
|
-
# -
|
|
216
|
-
# -
|
|
233
|
+
# - TTF ↔ OTF: Outline format conversion
|
|
234
|
+
# - WOFF/WOFF2: Web font packaging
|
|
235
|
+
# - Variable fonts: Automatic variation preservation or instance generation
|
|
236
|
+
#
|
|
237
|
+
# Variable Font Operations:
|
|
238
|
+
# The pipeline automatically detects whether variation data can be preserved based on
|
|
239
|
+
# source and target formats. For same outline family (TTF→WOFF or OTF→WOFF2), variation
|
|
240
|
+
# is preserved automatically. For cross-family conversions (TTF↔OTF), an instance is
|
|
241
|
+
# generated unless --preserve-variation is explicitly set.
|
|
217
242
|
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
# to
|
|
221
|
-
#
|
|
243
|
+
# Instance Generation:
|
|
244
|
+
# Use --coordinates to specify exact axis values (e.g., wght=700,wdth=100) or
|
|
245
|
+
# --instance-index to use a named instance. Individual axis options (--wght, --wdth)
|
|
246
|
+
# are also supported for convenience.
|
|
222
247
|
#
|
|
223
248
|
# @param font_file [String] Path to the font file
|
|
224
249
|
#
|
|
225
250
|
# @example Convert TTF to OTF
|
|
226
251
|
# fontisan convert font.ttf --to otf --output font.otf
|
|
227
252
|
#
|
|
228
|
-
# @example
|
|
229
|
-
# fontisan convert
|
|
253
|
+
# @example Generate bold instance at specific coordinates
|
|
254
|
+
# fontisan convert variable.ttf --to ttf --output bold.ttf --coordinates "wght=700,wdth=100"
|
|
255
|
+
#
|
|
256
|
+
# @example Generate bold instance using individual axis options
|
|
257
|
+
# fontisan convert variable.ttf --to ttf --output bold.ttf --wght 700
|
|
258
|
+
#
|
|
259
|
+
# @example Use named instance
|
|
260
|
+
# fontisan convert variable.ttf --to woff2 --output bold.woff2 --instance-index 0
|
|
230
261
|
#
|
|
231
|
-
# @example
|
|
232
|
-
# fontisan convert
|
|
233
|
-
# --min-pattern-length 15 --max-subroutines 10000
|
|
262
|
+
# @example Force variation preservation (if compatible)
|
|
263
|
+
# fontisan convert variable.ttf --to woff2 --output variable.woff2 --preserve-variation
|
|
234
264
|
#
|
|
235
|
-
# @example
|
|
236
|
-
# fontisan convert font.ttf --to
|
|
265
|
+
# @example Convert without validation
|
|
266
|
+
# fontisan convert font.ttf --to otf --output font.otf --no-validate
|
|
237
267
|
def convert(font_file)
|
|
238
|
-
|
|
268
|
+
# Build instance coordinates from axis options
|
|
269
|
+
instance_coords = build_instance_coordinates(options)
|
|
270
|
+
|
|
271
|
+
# Merge coordinates into options
|
|
272
|
+
convert_options = options.to_h.dup
|
|
273
|
+
convert_options[:instance_coordinates] = instance_coords if instance_coords.any?
|
|
274
|
+
|
|
275
|
+
command = Commands::ConvertCommand.new(font_file, convert_options)
|
|
239
276
|
command.run
|
|
240
277
|
rescue Errno::ENOENT, Error => e
|
|
241
278
|
handle_error(e)
|
|
@@ -444,6 +481,20 @@ module Fontisan
|
|
|
444
481
|
|
|
445
482
|
private
|
|
446
483
|
|
|
484
|
+
# Build instance coordinates from CLI axis options
|
|
485
|
+
#
|
|
486
|
+
# @param options [Hash] CLI options
|
|
487
|
+
# @return [Hash] Coordinates hash
|
|
488
|
+
def build_instance_coordinates(options)
|
|
489
|
+
coords = {}
|
|
490
|
+
coords["wght"] = options[:wght].to_f if options[:wght]
|
|
491
|
+
coords["wdth"] = options[:wdth].to_f if options[:wdth]
|
|
492
|
+
coords["slnt"] = options[:slnt].to_f if options[:slnt]
|
|
493
|
+
coords["ital"] = options[:ital].to_f if options[:ital]
|
|
494
|
+
coords["opsz"] = options[:opsz].to_f if options[:opsz]
|
|
495
|
+
coords
|
|
496
|
+
end
|
|
497
|
+
|
|
447
498
|
# Output the result in the requested format.
|
|
448
499
|
#
|
|
449
500
|
# @param result [Object] The result object to output
|
|
@@ -164,6 +164,9 @@ module Fontisan
|
|
|
164
164
|
raise Error, "Format mismatch: #{incompatible.join(', ')}"
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
+
# Check variable font compatibility
|
|
168
|
+
validate_variation_compatibility! if variable_fonts_in_collection?
|
|
169
|
+
|
|
167
170
|
# Check all fonts have required tables
|
|
168
171
|
@fonts.each_with_index do |font, index|
|
|
169
172
|
required_tables = %w[head hhea maxp]
|
|
@@ -177,6 +180,26 @@ module Fontisan
|
|
|
177
180
|
true
|
|
178
181
|
end
|
|
179
182
|
|
|
183
|
+
# Check if collection contains variable fonts
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean] true if any font has fvar table
|
|
186
|
+
def variable_fonts_in_collection?
|
|
187
|
+
@fonts.any? { |font| font.has_table?("fvar") }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Validate variable font compatibility
|
|
191
|
+
#
|
|
192
|
+
# Ensures all variable fonts in the collection are compatible:
|
|
193
|
+
# - All must be same variation type (TrueType or CFF2)
|
|
194
|
+
# - All must have the same axes
|
|
195
|
+
#
|
|
196
|
+
# @return [void]
|
|
197
|
+
# @raise [Error] if variable fonts are incompatible
|
|
198
|
+
def validate_variation_compatibility!
|
|
199
|
+
validate_all_same_variation_type!
|
|
200
|
+
validate_same_axes!
|
|
201
|
+
end
|
|
202
|
+
|
|
180
203
|
private
|
|
181
204
|
|
|
182
205
|
# Load configuration from file
|
|
@@ -255,6 +278,64 @@ module Fontisan
|
|
|
255
278
|
|
|
256
279
|
incompatible
|
|
257
280
|
end
|
|
281
|
+
|
|
282
|
+
# Validate all variable fonts use same variation type
|
|
283
|
+
#
|
|
284
|
+
# @return [void]
|
|
285
|
+
# @raise [Error] if mixing TrueType and CFF2 variable fonts
|
|
286
|
+
def validate_all_same_variation_type!
|
|
287
|
+
variable_fonts = @fonts.select { |f| f.has_table?("fvar") }
|
|
288
|
+
return if variable_fonts.empty?
|
|
289
|
+
|
|
290
|
+
ttf_count = variable_fonts.count { |f| f.has_table?("glyf") }
|
|
291
|
+
otf_count = variable_fonts.count { |f| f.has_table?("CFF2") }
|
|
292
|
+
|
|
293
|
+
if ttf_count.positive? && otf_count.positive?
|
|
294
|
+
raise Error, "Cannot mix TrueType and CFF2 variable fonts in collection"
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Validate all variable fonts have same axes
|
|
299
|
+
#
|
|
300
|
+
# @return [void]
|
|
301
|
+
# @raise [Error] if variable fonts have different axes
|
|
302
|
+
def validate_same_axes!
|
|
303
|
+
variable_fonts = @fonts.select { |f| f.has_table?("fvar") }
|
|
304
|
+
return if variable_fonts.size < 2
|
|
305
|
+
|
|
306
|
+
first_axes = extract_axes(variable_fonts.first)
|
|
307
|
+
variable_fonts.each_with_index do |font, index|
|
|
308
|
+
font_axes = extract_axes(font)
|
|
309
|
+
unless axes_match?(font_axes, first_axes)
|
|
310
|
+
raise Error,
|
|
311
|
+
"Variable font #{index} has different axes. " \
|
|
312
|
+
"Expected: #{first_axes.join(', ')}, " \
|
|
313
|
+
"Got: #{font_axes.join(', ')}"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Extract axis tags from a font's fvar table
|
|
319
|
+
#
|
|
320
|
+
# @param font [TrueTypeFont, OpenTypeFont] Font to extract axes from
|
|
321
|
+
# @return [Array<String>] Sorted array of axis tags
|
|
322
|
+
def extract_axes(font)
|
|
323
|
+
return [] unless font.has_table?("fvar")
|
|
324
|
+
|
|
325
|
+
fvar_table = font.table("fvar")
|
|
326
|
+
return [] unless fvar_table.respond_to?(:axes)
|
|
327
|
+
|
|
328
|
+
fvar_table.axes.map(&:axis_tag).sort
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Check if two axis arrays match
|
|
332
|
+
#
|
|
333
|
+
# @param axes1 [Array<String>] First axis array
|
|
334
|
+
# @param axes2 [Array<String>] Second axis array
|
|
335
|
+
# @return [Boolean] true if axes match
|
|
336
|
+
def axes_match?(axes1, axes2)
|
|
337
|
+
axes1 == axes2
|
|
338
|
+
end
|
|
258
339
|
end
|
|
259
340
|
end
|
|
260
341
|
end
|