fontisan 0.4.7 → 0.4.8
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/CHANGELOG.md +13 -0
- data/docs/.vitepress/config.ts +0 -7
- data/docs/cli/index.md +5 -28
- data/docs/index.md +0 -2
- data/lib/fontisan/cli.rb +29 -8
- data/lib/fontisan/collection/reader/stats.rb +23 -0
- data/lib/fontisan/collection/reader.rb +90 -0
- data/lib/fontisan/collection.rb +1 -0
- data/lib/fontisan/commands/convert_command.rb +96 -18
- data/lib/fontisan/commands/multi_format_output.rb +59 -0
- data/lib/fontisan/commands/validate_collection_command.rb +121 -0
- data/lib/fontisan/commands.rb +2 -0
- data/lib/fontisan/error.rb +25 -0
- data/lib/fontisan/models.rb +0 -1
- data/lib/fontisan/stitcher/collection_result.rb +18 -0
- data/lib/fontisan/stitcher/partition_strategy/base.rb +23 -0
- data/lib/fontisan/stitcher/partition_strategy/blueprint.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy/by_plane.rb +131 -0
- data/lib/fontisan/stitcher/partition_strategy/partition.rb +24 -0
- data/lib/fontisan/stitcher/partition_strategy.rb +22 -0
- data/lib/fontisan/stitcher.rb +44 -10
- data/lib/fontisan/ufo/compile/name.rb +2 -2
- data/lib/fontisan/ufo/info.rb +48 -0
- data/lib/fontisan/unicode/plane.rb +56 -0
- data/lib/fontisan/unicode.rb +17 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan.rb +2 -2
- metadata +13 -18
- data/docs/cli/audit.md +0 -337
- data/lib/fontisan/cldr/aggregator.rb +0 -33
- data/lib/fontisan/cldr/cache_manager.rb +0 -110
- data/lib/fontisan/cldr/config.rb +0 -59
- data/lib/fontisan/cldr/download_error.rb +0 -9
- data/lib/fontisan/cldr/downloader.rb +0 -79
- data/lib/fontisan/cldr/error.rb +0 -8
- data/lib/fontisan/cldr/index.rb +0 -64
- data/lib/fontisan/cldr/index_builder.rb +0 -72
- data/lib/fontisan/cldr/unicode_set_parser.rb +0 -189
- data/lib/fontisan/cldr/unknown_version_error.rb +0 -9
- data/lib/fontisan/cldr/version_resolver.rb +0 -91
- data/lib/fontisan/cldr.rb +0 -23
- data/lib/fontisan/cli/cldr_cli.rb +0 -85
- data/lib/fontisan/config/cldr.yml +0 -22
- data/lib/fontisan/models/cldr/language_coverage.rb +0 -31
- data/lib/fontisan/models/cldr.rb +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a519f7a53629decc2b968e9c99b5979459fe33c49750aa851818a330e6fd6af
|
|
4
|
+
data.tar.gz: 805f840f58f042288c34610fa355b1be74b428a8669abc15188c08e635494148
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06edc2ca7cf97a919095afee9b5bc7327d0f01f4ce304c537f1ac4a920dd7950cce67beaba34d0bf93aa99d6cb21c385ad5238c8077d7aebd87545a8c35437eb
|
|
7
|
+
data.tar.gz: 8a43a243ad4aeae715f63bf94c5e907d07b2330f59b4b37f3bfafa1ee8af063796c8ab84547f92b674f46340bfb5c96686e7b25b88f490f18ff77848c7c30d6a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- Audit/UCD documentation residue from the v0.3.0 audit/UCD subsystem
|
|
13
|
+
removal: deleted `docs/cli/audit.md`, dropped the "🔍 Font Audit"
|
|
14
|
+
feature card and "Font Audit" sidebar group, removed audit entries
|
|
15
|
+
from `docs/cli/index.md`. No behavior change.
|
|
16
|
+
- CLDR subsystem (`Fontisan::Cldr`, `Models::Cldr::LanguageCoverage`,
|
|
17
|
+
`lib/fontisan/config/cldr.yml`) and the `fontisan cldr` CLI
|
|
18
|
+
subcommand. CLDR was added in TODO 21 as infrastructure for
|
|
19
|
+
`fontisan audit`'s per-language coverage extractor; with the audit
|
|
20
|
+
subsystem moved to ucode (v0.3.0), CLDR had no remaining consumer
|
|
21
|
+
in fontisan. Not released in a published gem version.
|
|
22
|
+
|
|
10
23
|
### Added
|
|
11
24
|
|
|
12
25
|
- `Stitcher` explicit subfont declaration model: every `include_*`
|
data/docs/.vitepress/config.ts
CHANGED
|
@@ -263,13 +263,6 @@ export default defineConfig({
|
|
|
263
263
|
{ text: "Overview", link: "/cli/" },
|
|
264
264
|
],
|
|
265
265
|
},
|
|
266
|
-
{
|
|
267
|
-
text: "Font Audit",
|
|
268
|
-
collapsed: true,
|
|
269
|
-
items: [
|
|
270
|
-
{ text: "audit", link: "/cli/audit" },
|
|
271
|
-
],
|
|
272
|
-
},
|
|
273
266
|
{
|
|
274
267
|
text: "Font Information",
|
|
275
268
|
collapsed: true,
|
data/docs/cli/index.md
CHANGED
|
@@ -20,18 +20,6 @@ Always check your font's End User License Agreement (EULA) before processing. Ma
|
|
|
20
20
|
|
|
21
21
|
## Quick Reference
|
|
22
22
|
|
|
23
|
-
### Font Audit
|
|
24
|
-
|
|
25
|
-
| Command | Description | Example |
|
|
26
|
-
|---------|-------------|---------|
|
|
27
|
-
| `audit` | Comprehensive per-face audit (replaces `otfinfo`) | `fontisan audit font.ttf` |
|
|
28
|
-
|
|
29
|
-
The `audit` command covers identity, style, metrics, codepoint coverage,
|
|
30
|
-
licensing, hinting, color capabilities, variable-font detail, and
|
|
31
|
-
OpenType layout — and supports collections, `--compare` mode, and
|
|
32
|
-
whole-library `--recursive --summary` mode. See
|
|
33
|
-
[audit](/cli/audit) for details.
|
|
34
|
-
|
|
35
23
|
### Font Information Commands
|
|
36
24
|
|
|
37
25
|
| Command | Description | Example |
|
|
@@ -179,19 +167,11 @@ fontisan validate font.ttf --profile production
|
|
|
179
167
|
|
|
180
168
|
### Audit a Font (or Library)
|
|
181
169
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
fontisan audit --compare baseline.yaml new.ttf
|
|
188
|
-
|
|
189
|
-
# Whole-library summary
|
|
190
|
-
fontisan audit lib/ --recursive --summary
|
|
191
|
-
|
|
192
|
-
# Fast inventory pass
|
|
193
|
-
fontisan audit lib/ --recursive --summary --brief
|
|
194
|
-
```
|
|
170
|
+
The `audit` and `ucd` commands have been removed from fontisan. The
|
|
171
|
+
audit/UCD subsystem moved to the
|
|
172
|
+
[ucode](https://github.com/fontist/ucode) gem. For per-face font
|
|
173
|
+
metadata, use [`info`](/cli/info); for pass/fail quality checks, use
|
|
174
|
+
[`validate`](/cli/validate).
|
|
195
175
|
|
|
196
176
|
### Export Font Data
|
|
197
177
|
|
|
@@ -210,9 +190,6 @@ fontisan export font.ttf --format ttx --tables head,name,cmap
|
|
|
210
190
|
|
|
211
191
|
Detailed documentation for each command:
|
|
212
192
|
|
|
213
|
-
### Font Audit
|
|
214
|
-
- [audit](/cli/audit) — Comprehensive per-face audit (replaces `otfinfo`); supports collections, compare, library summary
|
|
215
|
-
|
|
216
193
|
### Font Information
|
|
217
194
|
- [info](/cli/info) — Extract font metadata and properties (includes brief mode)
|
|
218
195
|
- [ls](/cli/ls) — List fonts in collections
|
data/docs/index.md
CHANGED
|
@@ -18,8 +18,6 @@ hero:
|
|
|
18
18
|
link: /guide/migrations/fonttools
|
|
19
19
|
|
|
20
20
|
features:
|
|
21
|
-
- title: "🔍 Font Audit"
|
|
22
|
-
details: Comprehensive per-face audit (replaces otfinfo) — identity, style, metrics, coverage, licensing, hinting, color, variation, OpenType layout, UCD/CLDR aggregation, compare, and library-summary modes.
|
|
23
21
|
- title: "🔄 Font Conversion"
|
|
24
22
|
details: Convert between TTF, OTF, WOFF, WOFF2, Type 1 (PFB/PFA), and SVG formats with curve conversion and optimization.
|
|
25
23
|
- title: "✅ Font Validation"
|
data/lib/fontisan/cli.rb
CHANGED
|
@@ -25,9 +25,6 @@ module Fontisan
|
|
|
25
25
|
desc: "Suppress non-error output",
|
|
26
26
|
aliases: "-q"
|
|
27
27
|
|
|
28
|
-
desc "cldr", "Manage local CLDR cache (subcommands)", hide: true
|
|
29
|
-
subcommand "cldr", CldrCli
|
|
30
|
-
|
|
31
28
|
desc "ufo", "UFO source operations (build, convert, validate)"
|
|
32
29
|
subcommand "ufo", Fontisan::Ufo::Cli
|
|
33
30
|
|
|
@@ -211,10 +208,12 @@ module Fontisan
|
|
|
211
208
|
end
|
|
212
209
|
|
|
213
210
|
desc "convert FONT_FILE", "Convert font to different format"
|
|
214
|
-
option :to, type: :
|
|
215
|
-
desc: "Target format: ttf, otf, type1, t1, ttc, otc, dfont, svg, " \
|
|
211
|
+
option :to, type: :array, required: true,
|
|
212
|
+
desc: "Target format(s): ttf, otf, type1, t1, ttc, otc, dfont, svg, " \
|
|
216
213
|
"woff (zlib — works on all browsers incl. legacy), " \
|
|
217
|
-
"woff2 (Brotli — ~30% smaller, modern browsers only)"
|
|
214
|
+
"woff2 (Brotli — ~30% smaller, modern browsers only). " \
|
|
215
|
+
"Pass multiple times (--to woff --to woff2) or comma-separated " \
|
|
216
|
+
"(--to woff,woff2) for multi-format output.",
|
|
218
217
|
aliases: "-t"
|
|
219
218
|
option :output, type: :string,
|
|
220
219
|
desc: "Output file path (required unless --show-options)",
|
|
@@ -361,7 +360,7 @@ module Fontisan
|
|
|
361
360
|
|
|
362
361
|
# Handle --show-options
|
|
363
362
|
if options[:show_options]
|
|
364
|
-
show_recommended_options(source_format, options[:to])
|
|
363
|
+
show_recommended_options(source_format, options[:to].is_a?(Array) ? options[:to].first : options[:to])
|
|
365
364
|
return
|
|
366
365
|
end
|
|
367
366
|
|
|
@@ -371,7 +370,8 @@ module Fontisan
|
|
|
371
370
|
end
|
|
372
371
|
|
|
373
372
|
# Build ConversionOptions
|
|
374
|
-
conv_options = build_conversion_options(source_format,
|
|
373
|
+
conv_options = build_conversion_options(source_format,
|
|
374
|
+
Array(options[:to]).first,
|
|
375
375
|
options)
|
|
376
376
|
|
|
377
377
|
# Build instance coordinates from axis options
|
|
@@ -573,6 +573,27 @@ module Fontisan
|
|
|
573
573
|
exit command.run
|
|
574
574
|
end
|
|
575
575
|
|
|
576
|
+
desc "validate-collection PATH",
|
|
577
|
+
"Validate a TTC/OTC/dfont (face count, glyph cap, cmap union)"
|
|
578
|
+
option :expected_faces, type: :numeric,
|
|
579
|
+
desc: "Required face count"
|
|
580
|
+
option :max_glyphs, type: :numeric, default: Fontisan::Commands::ValidateCollectionCommand::DEFAULT_MAX_GLYPHS,
|
|
581
|
+
desc: "Per-face glyph cap"
|
|
582
|
+
option :expected_cmap_union, type: :numeric,
|
|
583
|
+
desc: "Minimum cmap union size across all faces"
|
|
584
|
+
def validate_collection(path)
|
|
585
|
+
cmd = Commands::ValidateCollectionCommand.new(
|
|
586
|
+
input: path,
|
|
587
|
+
expected_faces: options[:expected_faces],
|
|
588
|
+
max_glyphs: options[:max_glyphs],
|
|
589
|
+
expected_cmap_union: options[:expected_cmap_union],
|
|
590
|
+
)
|
|
591
|
+
exit cmd.run
|
|
592
|
+
rescue ArgumentError => e
|
|
593
|
+
warn "ERROR: #{e.message}"
|
|
594
|
+
exit 1
|
|
595
|
+
end
|
|
596
|
+
|
|
576
597
|
desc "version", "Display version information"
|
|
577
598
|
# Display the Fontisan version.
|
|
578
599
|
def version
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Collection
|
|
5
|
+
class Reader
|
|
6
|
+
# Snapshot of one face's headline metrics. Pure value object —
|
|
7
|
+
# no methods beyond accessors. Separate file from {Reader} so
|
|
8
|
+
# the Stats struct can be referenced without pulling in the
|
|
9
|
+
# full Reader (and its FontLoader dependency).
|
|
10
|
+
#
|
|
11
|
+
# @!attribute [r] index
|
|
12
|
+
# @return [Integer] 0-based face index inside the collection
|
|
13
|
+
# @!attribute [r] glyph_count
|
|
14
|
+
# @return [Integer] face's maxp.numGlyphs
|
|
15
|
+
# @!attribute [r] codepoint_count
|
|
16
|
+
# @return [Integer] count of keys in face's cmap unicode_mappings
|
|
17
|
+
# @!attribute [r] sfnt_version
|
|
18
|
+
# @return [Integer] face sfnt version (0x00010000 for TTF, 0x4F54544F for OTF)
|
|
19
|
+
Stats = Struct.new(:index, :glyph_count, :codepoint_count, :sfnt_version,
|
|
20
|
+
keyword_init: true)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Collection
|
|
5
|
+
# Read-only counterpart to {Builder}. Opens an existing TTC/OTC/dfont
|
|
6
|
+
# and exposes per-face metadata (glyph count, codepoint count, sfnt
|
|
7
|
+
# version) and the cmap union across all faces.
|
|
8
|
+
#
|
|
9
|
+
# Delegates header parsing to {FontLoader} — never hand-rolls the
|
|
10
|
+
# TTC header bytes (that would duplicate BaseCollection.from_file
|
|
11
|
+
# and only handle TTC/OTC, not dfont).
|
|
12
|
+
#
|
|
13
|
+
# @example Per-face glyph counts
|
|
14
|
+
# reader = Collection::Reader.open("family.ttc")
|
|
15
|
+
# reader.stats.map { |s| [s.index, s.glyph_count] }
|
|
16
|
+
#
|
|
17
|
+
# @example Cmap union
|
|
18
|
+
# Collection::Reader.open("family.otc").cmap_union.size
|
|
19
|
+
class Reader
|
|
20
|
+
autoload :Stats, "fontisan/collection/reader/stats"
|
|
21
|
+
|
|
22
|
+
# @!attribute [r] path
|
|
23
|
+
# @return [String] the file path this reader was opened with
|
|
24
|
+
attr_reader :path
|
|
25
|
+
|
|
26
|
+
# Collection formats this reader accepts. Single source of truth —
|
|
27
|
+
# matches {FontLoader::COLLECTION_CLASSES}.
|
|
28
|
+
FORMATS = %i[ttc otc dfont].freeze
|
|
29
|
+
|
|
30
|
+
# @param path [String] path to a TTC/OTC/dfont file
|
|
31
|
+
# @raise [ArgumentError] if the file is not a recognized collection
|
|
32
|
+
def initialize(path)
|
|
33
|
+
@path = path
|
|
34
|
+
detected = FontLoader.detect_format(path)
|
|
35
|
+
unless FORMATS.include?(detected)
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"#{path} is not a TTC/OTC/dfont (detected: #{detected.inspect})"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@collection = FontLoader.load_collection(path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Convenience constructor.
|
|
44
|
+
# @param path [String]
|
|
45
|
+
# @return [Reader]
|
|
46
|
+
def self.open(path)
|
|
47
|
+
new(path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Integer] number of faces in the collection
|
|
51
|
+
def face_count
|
|
52
|
+
@collection.num_fonts
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Yields each face as a loaded TTF/OTF font. Without a block,
|
|
56
|
+
# returns an Enumerator (so callers can chain +each_with_index+).
|
|
57
|
+
# The Enumerator's size is set to +face_count+ so size-based
|
|
58
|
+
# assertions work without consuming it.
|
|
59
|
+
#
|
|
60
|
+
# @yieldparam face [TrueTypeFont, OpenTypeFont]
|
|
61
|
+
# @return [Enumerator, void]
|
|
62
|
+
def each_face
|
|
63
|
+
return enum_for(:each_face) { face_count } unless block_given?
|
|
64
|
+
|
|
65
|
+
face_count.times { |i| yield FontLoader.load(@path, font_index: i) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Array<Stats>] one Stats per face, in face-index order
|
|
69
|
+
def stats
|
|
70
|
+
each_face.map.with_index do |face, i|
|
|
71
|
+
Stats.new(
|
|
72
|
+
index: i,
|
|
73
|
+
glyph_count: face.table("maxp")&.num_glyphs || 0,
|
|
74
|
+
codepoint_count: (face.table("cmap")&.unicode_mappings || {}).size,
|
|
75
|
+
sfnt_version: face.header.sfnt_version,
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Union of every face's cmap keys.
|
|
81
|
+
#
|
|
82
|
+
# @return [Set<Integer>]
|
|
83
|
+
def cmap_union
|
|
84
|
+
each_face.with_object(Set.new) do |face, union|
|
|
85
|
+
union.merge((face.table("cmap")&.unicode_mappings || {}).keys)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/fontisan/collection.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Fontisan
|
|
|
7
7
|
autoload :Builder, "fontisan/collection/builder"
|
|
8
8
|
autoload :DfontBuilder, "fontisan/collection/dfont_builder"
|
|
9
9
|
autoload :OffsetCalculator, "fontisan/collection/offset_calculator"
|
|
10
|
+
autoload :Reader, "fontisan/collection/reader"
|
|
10
11
|
autoload :SharedLogic, "fontisan/collection/shared_logic"
|
|
11
12
|
autoload :TableAnalyzer, "fontisan/collection/table_analyzer"
|
|
12
13
|
autoload :TableDeduplicator, "fontisan/collection/table_deduplicator"
|
|
@@ -42,11 +42,20 @@ module Fontisan
|
|
|
42
42
|
# )
|
|
43
43
|
# command.run
|
|
44
44
|
class ConvertCommand < BaseCommand
|
|
45
|
+
# Public readers for the parsed target formats and per-format output
|
|
46
|
+
# paths. Exposed (rather than relying on @ivar access from specs) so
|
|
47
|
+
# multi-format behaviour can be tested through the public surface.
|
|
48
|
+
attr_reader :target_formats, :output_paths
|
|
49
|
+
|
|
45
50
|
# Initialize convert command
|
|
46
51
|
#
|
|
47
52
|
# @param font_path [String] Path to input font file
|
|
48
53
|
# @param options [Hash] Conversion options
|
|
49
|
-
# @option options [String] :to Target format
|
|
54
|
+
# @option options [String, Array<String>] :to Target format(s):
|
|
55
|
+
# ttf, otf, woff, woff2, type1, ttc, otc, dfont, svg. Pass a
|
|
56
|
+
# comma-separated string ("woff,woff2") or an array (["woff",
|
|
57
|
+
# "woff2"]) for multi-format output. Single-font → single-font
|
|
58
|
+
# only; multi-format + collection input is rejected.
|
|
50
59
|
# @option options [String] :output Output file path (required)
|
|
51
60
|
# @option options [Integer] :font_index Index for TTC/OTC (default: 0)
|
|
52
61
|
# @option options [String] :coordinates Coordinate string (e.g., "wght=700,wdth=100")
|
|
@@ -69,8 +78,13 @@ module Fontisan
|
|
|
69
78
|
|
|
70
79
|
@output_path = @options[:output]
|
|
71
80
|
|
|
72
|
-
# Parse target format
|
|
73
|
-
|
|
81
|
+
# Parse target format(s). Always an Array<Symbol>, deduped, order
|
|
82
|
+
# preserved. Single-format callsites get a one-element array.
|
|
83
|
+
@target_formats = parse_target_formats(@options[:to])
|
|
84
|
+
|
|
85
|
+
# Resolve per-format output paths up front so filename ambiguity
|
|
86
|
+
# surfaces before any pipeline work is done.
|
|
87
|
+
@output_paths = resolve_output_paths
|
|
74
88
|
|
|
75
89
|
# Extract ConversionOptions if provided
|
|
76
90
|
@conv_options = extract_conversion_options(@options)
|
|
@@ -92,17 +106,26 @@ module Fontisan
|
|
|
92
106
|
|
|
93
107
|
# Execute the conversion
|
|
94
108
|
#
|
|
95
|
-
# @return [Hash] Result information
|
|
109
|
+
# @return [Hash, Array<Hash>] Result information. Single-format
|
|
110
|
+
# returns one result hash (back-compat). Multi-format returns
|
|
111
|
+
# an array of result hashes, one per target format.
|
|
96
112
|
# @raise [ArgumentError] If output path is not specified
|
|
97
113
|
# @raise [Error] If conversion fails
|
|
98
114
|
def run
|
|
99
115
|
validate_options!
|
|
100
116
|
|
|
101
|
-
# Check if input is a collection
|
|
102
117
|
if collection_file?
|
|
118
|
+
if multi_format?
|
|
119
|
+
raise ArgumentError,
|
|
120
|
+
"Multi-format conversion is not supported for collection " \
|
|
121
|
+
"input. Specify a single target format."
|
|
122
|
+
end
|
|
123
|
+
|
|
103
124
|
convert_collection
|
|
125
|
+
elsif multi_format?
|
|
126
|
+
convert_multi_format
|
|
104
127
|
else
|
|
105
|
-
convert_single_font
|
|
128
|
+
convert_single_font(@target_formats.first, @output_paths.first)
|
|
106
129
|
end
|
|
107
130
|
rescue ArgumentError
|
|
108
131
|
# Let ArgumentError propagate for validation errors
|
|
@@ -113,6 +136,11 @@ module Fontisan
|
|
|
113
136
|
|
|
114
137
|
private
|
|
115
138
|
|
|
139
|
+
# @return [Boolean] true when more than one target format was given
|
|
140
|
+
def multi_format?
|
|
141
|
+
@target_formats.size > 1
|
|
142
|
+
end
|
|
143
|
+
|
|
116
144
|
# Check if input file is a collection
|
|
117
145
|
#
|
|
118
146
|
# @return [Boolean] true if collection
|
|
@@ -123,15 +151,36 @@ module Fontisan
|
|
|
123
151
|
false
|
|
124
152
|
end
|
|
125
153
|
|
|
126
|
-
# Convert
|
|
154
|
+
# Convert to each target format and aggregate results.
|
|
127
155
|
#
|
|
156
|
+
# @return [Array<Hash>]
|
|
157
|
+
def convert_multi_format
|
|
158
|
+
unless @options[:quiet]
|
|
159
|
+
puts "Converting #{File.basename(font_path)} to " \
|
|
160
|
+
"#{@target_formats.join(', ')}..."
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
@target_formats.zip(@output_paths).map do |fmt, out_path|
|
|
164
|
+
result = convert_single_font(fmt, out_path)
|
|
165
|
+
unless @options[:quiet]
|
|
166
|
+
puts " #{fmt}: wrote #{File.basename(out_path)} " \
|
|
167
|
+
"(#{format_size(File.size(out_path))})"
|
|
168
|
+
end
|
|
169
|
+
result
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Convert a single font to a single target format at +out_path+.
|
|
174
|
+
#
|
|
175
|
+
# @param target_format [Symbol]
|
|
176
|
+
# @param out_path [String]
|
|
128
177
|
# @return [Hash] Result information
|
|
129
|
-
def convert_single_font
|
|
130
|
-
puts "Converting #{File.basename(font_path)} to #{
|
|
178
|
+
def convert_single_font(target_format, out_path)
|
|
179
|
+
puts "Converting #{File.basename(font_path)} to #{target_format}..." unless @options[:quiet]
|
|
131
180
|
|
|
132
181
|
# Build pipeline options
|
|
133
182
|
pipeline_options = {
|
|
134
|
-
target_format:
|
|
183
|
+
target_format: target_format,
|
|
135
184
|
validate: @validate,
|
|
136
185
|
verbose: @options[:verbose],
|
|
137
186
|
}
|
|
@@ -160,7 +209,7 @@ module Fontisan
|
|
|
160
209
|
# Use TransformationPipeline for universal conversion
|
|
161
210
|
pipeline = Pipeline::TransformationPipeline.new(
|
|
162
211
|
font_path,
|
|
163
|
-
|
|
212
|
+
out_path,
|
|
164
213
|
pipeline_options,
|
|
165
214
|
)
|
|
166
215
|
|
|
@@ -168,12 +217,12 @@ module Fontisan
|
|
|
168
217
|
|
|
169
218
|
# Display results
|
|
170
219
|
unless @options[:quiet]
|
|
171
|
-
output_size = File.size(
|
|
220
|
+
output_size = File.size(out_path)
|
|
172
221
|
input_size = File.size(font_path)
|
|
173
222
|
|
|
174
223
|
puts "Conversion complete!"
|
|
175
224
|
puts " Input: #{font_path} (#{format_size(input_size)})"
|
|
176
|
-
puts " Output: #{
|
|
225
|
+
puts " Output: #{out_path} (#{format_size(output_size)})"
|
|
177
226
|
puts " Format: #{result[:details][:source_format]} → #{result[:details][:target_format]}"
|
|
178
227
|
|
|
179
228
|
if result[:details][:variation_preserved]
|
|
@@ -186,11 +235,11 @@ module Fontisan
|
|
|
186
235
|
{
|
|
187
236
|
success: true,
|
|
188
237
|
input_path: font_path,
|
|
189
|
-
output_path:
|
|
238
|
+
output_path: out_path,
|
|
190
239
|
source_format: result[:details][:source_format],
|
|
191
240
|
target_format: result[:details][:target_format],
|
|
192
241
|
input_size: File.size(font_path),
|
|
193
|
-
output_size: File.size(
|
|
242
|
+
output_size: File.size(out_path),
|
|
194
243
|
variation_strategy: result[:details][:variation_strategy],
|
|
195
244
|
}
|
|
196
245
|
end
|
|
@@ -200,11 +249,11 @@ module Fontisan
|
|
|
200
249
|
# @return [Hash] Result information
|
|
201
250
|
def convert_collection
|
|
202
251
|
# Determine target collection type from target format
|
|
203
|
-
target_type = collection_type_from_format(@
|
|
252
|
+
target_type = collection_type_from_format(@target_formats.first)
|
|
204
253
|
|
|
205
254
|
unless target_type
|
|
206
255
|
raise ArgumentError,
|
|
207
|
-
"Target format #{@
|
|
256
|
+
"Target format #{@target_formats.first} is not a collection format. " \
|
|
208
257
|
"Use ttc, otc, or dfont for collection conversion."
|
|
209
258
|
end
|
|
210
259
|
|
|
@@ -302,12 +351,41 @@ module Fontisan
|
|
|
302
351
|
"Output path is required. Use --output option."
|
|
303
352
|
end
|
|
304
353
|
|
|
305
|
-
|
|
354
|
+
if @target_formats.empty?
|
|
306
355
|
raise ArgumentError,
|
|
307
356
|
"Target format is required. Use --to option."
|
|
308
357
|
end
|
|
309
358
|
end
|
|
310
359
|
|
|
360
|
+
# Normalize the +--to+ value into a deduplicated Array<Symbol>.
|
|
361
|
+
#
|
|
362
|
+
# Accepts:
|
|
363
|
+
# - "woff" → [:woff]
|
|
364
|
+
# - "woff,woff2" → [:woff, :woff2]
|
|
365
|
+
# - ["woff", "woff2"] → [:woff, :woff2]
|
|
366
|
+
# - ["woff,woff2", "ttf"] → [:woff, :woff2, :ttf]
|
|
367
|
+
# - nil / "" → []
|
|
368
|
+
#
|
|
369
|
+
# @return [Array<Symbol>]
|
|
370
|
+
def parse_target_formats(raw)
|
|
371
|
+
array = Array(raw).flat_map { |s| s.to_s.split(",") }
|
|
372
|
+
.map { |s| s.strip.downcase }
|
|
373
|
+
.reject(&:empty?)
|
|
374
|
+
.map { |s| parse_target_format(s) }
|
|
375
|
+
array.uniq
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Resolve the per-format output paths via {MultiFormatOutput}.
|
|
379
|
+
# Returns nil when no target formats have been parsed yet (the
|
|
380
|
+
# validate_options! path will surface the missing-format error).
|
|
381
|
+
#
|
|
382
|
+
# @return [Array<String>, nil]
|
|
383
|
+
def resolve_output_paths
|
|
384
|
+
return nil if @target_formats.empty? || @output_path.nil?
|
|
385
|
+
|
|
386
|
+
MultiFormatOutput.new(@output_path, @target_formats).paths
|
|
387
|
+
end
|
|
388
|
+
|
|
311
389
|
# Parse target format from string/symbol
|
|
312
390
|
#
|
|
313
391
|
# @param format [String, Symbol, nil] Target format
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Commands
|
|
5
|
+
# Resolves the on-disk output path for each target format in a
|
|
6
|
+
# multi-format conversion (TODO 72).
|
|
7
|
+
#
|
|
8
|
+
# Rules:
|
|
9
|
+
# - One format + path has extension → use the path as-is.
|
|
10
|
+
# - One format + path has no extension → append ".<format>".
|
|
11
|
+
# - Many formats + path has extension → +ArgumentError+ (ambiguous).
|
|
12
|
+
# - Many formats + path has no extension → append ".<format>" per target.
|
|
13
|
+
#
|
|
14
|
+
# Pure value object: no I/O, no mutation. Extracted from
|
|
15
|
+
# {ConvertCommand} so multi-format path resolution can be tested
|
|
16
|
+
# independently of the transformation pipeline.
|
|
17
|
+
class MultiFormatOutput
|
|
18
|
+
# @param base_path [String] user-supplied --output value
|
|
19
|
+
# @param target_formats [Array<Symbol>] non-empty, deduplicated
|
|
20
|
+
def initialize(base_path, target_formats)
|
|
21
|
+
@base_path = base_path
|
|
22
|
+
@target_formats = target_formats
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Array<String>] one resolved path per target format,
|
|
26
|
+
# in the same order as +target_formats+
|
|
27
|
+
# @raise [ArgumentError] if the base path is ambiguous
|
|
28
|
+
def paths
|
|
29
|
+
single? ? [single_format_path] : multi_format_paths
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def single?
|
|
35
|
+
@target_formats.size == 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def single_format_path
|
|
39
|
+
has_extension? ? @base_path : "#{@base_path}.#{@target_formats.first}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def multi_format_paths
|
|
43
|
+
if has_extension?
|
|
44
|
+
raise ArgumentError,
|
|
45
|
+
"Output path #{@base_path.inspect} has an extension but " \
|
|
46
|
+
"#{@target_formats.size} target formats were given " \
|
|
47
|
+
"(#{@target_formats.join(', ')}). Drop the extension or " \
|
|
48
|
+
"specify a single format."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@target_formats.map { |fmt| "#{@base_path}.#{fmt}" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def has_extension?
|
|
55
|
+
!File.extname(@base_path).strip.downcase.empty?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|