fontisan 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- data/benchmark/variation_quick_bench.rb +47 -0
- data/docs/EXTRACT_TTC_MIGRATION.md +549 -0
- data/fontisan.gemspec +4 -1
- data/lib/fontisan/binary/base_record.rb +22 -1
- data/lib/fontisan/cli.rb +309 -0
- data/lib/fontisan/collection/builder.rb +260 -0
- data/lib/fontisan/collection/offset_calculator.rb +227 -0
- data/lib/fontisan/collection/table_analyzer.rb +204 -0
- data/lib/fontisan/collection/table_deduplicator.rb +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -0
- data/lib/fontisan/commands/export_command.rb +161 -0
- data/lib/fontisan/commands/info_command.rb +40 -6
- data/lib/fontisan/commands/instance_command.rb +295 -0
- data/lib/fontisan/commands/ls_command.rb +113 -0
- data/lib/fontisan/commands/pack_command.rb +241 -0
- data/lib/fontisan/commands/subset_command.rb +245 -0
- data/lib/fontisan/commands/unpack_command.rb +338 -0
- data/lib/fontisan/commands/validate_command.rb +178 -0
- data/lib/fontisan/commands/variable_command.rb +30 -1
- data/lib/fontisan/config/collection_settings.yml +56 -0
- data/lib/fontisan/config/conversion_matrix.yml +212 -0
- data/lib/fontisan/config/export_settings.yml +66 -0
- data/lib/fontisan/config/subset_profiles.yml +100 -0
- data/lib/fontisan/config/svg_settings.yml +60 -0
- data/lib/fontisan/config/validation_rules.yml +149 -0
- data/lib/fontisan/config/variable_settings.yml +99 -0
- data/lib/fontisan/config/woff2_settings.yml +77 -0
- data/lib/fontisan/constants.rb +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -0
- data/lib/fontisan/converters/svg_generator.rb +244 -0
- data/lib/fontisan/converters/table_copier.rb +117 -0
- data/lib/fontisan/converters/woff2_encoder.rb +416 -0
- data/lib/fontisan/converters/woff_writer.rb +391 -0
- data/lib/fontisan/error.rb +203 -0
- data/lib/fontisan/export/exporter.rb +262 -0
- data/lib/fontisan/export/table_serializer.rb +255 -0
- data/lib/fontisan/export/transformers/font_to_ttx.rb +172 -0
- data/lib/fontisan/export/transformers/head_transformer.rb +96 -0
- data/lib/fontisan/export/transformers/hhea_transformer.rb +59 -0
- data/lib/fontisan/export/transformers/maxp_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/name_transformer.rb +63 -0
- data/lib/fontisan/export/transformers/os2_transformer.rb +121 -0
- data/lib/fontisan/export/transformers/post_transformer.rb +51 -0
- data/lib/fontisan/export/ttx_generator.rb +527 -0
- data/lib/fontisan/export/ttx_parser.rb +300 -0
- data/lib/fontisan/font_loader.rb +121 -12
- data/lib/fontisan/font_writer.rb +301 -0
- data/lib/fontisan/formatters/text_formatter.rb +102 -0
- data/lib/fontisan/glyph_accessor.rb +503 -0
- data/lib/fontisan/hints/hint_converter.rb +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -0
- data/lib/fontisan/metrics_calculator.rb +277 -0
- data/lib/fontisan/models/collection_font_summary.rb +52 -0
- data/lib/fontisan/models/collection_info.rb +76 -0
- data/lib/fontisan/models/collection_list_info.rb +37 -0
- data/lib/fontisan/models/font_export.rb +158 -0
- data/lib/fontisan/models/font_summary.rb +48 -0
- data/lib/fontisan/models/glyph_outline.rb +343 -0
- data/lib/fontisan/models/hint.rb +233 -0
- data/lib/fontisan/models/outline.rb +664 -0
- data/lib/fontisan/models/table_sharing_info.rb +40 -0
- data/lib/fontisan/models/ttx/glyph_order.rb +31 -0
- data/lib/fontisan/models/ttx/tables/binary_table.rb +67 -0
- data/lib/fontisan/models/ttx/tables/head_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/hhea_table.rb +74 -0
- data/lib/fontisan/models/ttx/tables/maxp_table.rb +55 -0
- data/lib/fontisan/models/ttx/tables/name_table.rb +45 -0
- data/lib/fontisan/models/ttx/tables/os2_table.rb +157 -0
- data/lib/fontisan/models/ttx/tables/post_table.rb +50 -0
- data/lib/fontisan/models/ttx/ttfont.rb +49 -0
- data/lib/fontisan/models/validation_report.rb +203 -0
- data/lib/fontisan/open_type_collection.rb +156 -2
- data/lib/fontisan/open_type_font.rb +296 -10
- data/lib/fontisan/optimizers/charstring_rewriter.rb +161 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +308 -0
- data/lib/fontisan/optimizers/stack_tracker.rb +246 -0
- data/lib/fontisan/optimizers/subroutine_builder.rb +134 -0
- data/lib/fontisan/optimizers/subroutine_generator.rb +207 -0
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +107 -0
- data/lib/fontisan/outline_extractor.rb +423 -0
- data/lib/fontisan/subset/builder.rb +268 -0
- data/lib/fontisan/subset/glyph_mapping.rb +215 -0
- data/lib/fontisan/subset/options.rb +142 -0
- data/lib/fontisan/subset/profile.rb +152 -0
- data/lib/fontisan/subset/table_subsetter.rb +461 -0
- data/lib/fontisan/svg/font_face_generator.rb +278 -0
- data/lib/fontisan/svg/font_generator.rb +264 -0
- data/lib/fontisan/svg/glyph_generator.rb +168 -0
- data/lib/fontisan/svg/view_box_calculator.rb +137 -0
- data/lib/fontisan/tables/cff/cff_glyph.rb +176 -0
- data/lib/fontisan/tables/cff/charset.rb +282 -0
- data/lib/fontisan/tables/cff/charstring.rb +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -0
- data/lib/fontisan/tables/cff/charstrings_index.rb +162 -0
- data/lib/fontisan/tables/cff/dict.rb +351 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/index.rb +237 -0
- data/lib/fontisan/tables/cff/index_builder.rb +170 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -0
- data/lib/fontisan/tables/cff2/blend_operator.rb +240 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +591 -0
- data/lib/fontisan/tables/cff2/operand_stack.rb +232 -0
- data/lib/fontisan/tables/cff2.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -0
- data/lib/fontisan/tables/fvar.rb +2 -2
- data/lib/fontisan/tables/glyf/compound_glyph.rb +483 -0
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +136 -0
- data/lib/fontisan/tables/glyf/curve_converter.rb +343 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +450 -0
- data/lib/fontisan/tables/glyf/simple_glyph.rb +382 -0
- data/lib/fontisan/tables/glyf.rb +235 -0
- data/lib/fontisan/tables/gvar.rb +270 -0
- data/lib/fontisan/tables/hhea.rb +124 -0
- data/lib/fontisan/tables/hmtx.rb +287 -0
- data/lib/fontisan/tables/hvar.rb +191 -0
- data/lib/fontisan/tables/loca.rb +322 -0
- data/lib/fontisan/tables/maxp.rb +192 -0
- data/lib/fontisan/tables/mvar.rb +185 -0
- data/lib/fontisan/tables/name.rb +99 -30
- data/lib/fontisan/tables/variation_common.rb +346 -0
- data/lib/fontisan/tables/vvar.rb +234 -0
- data/lib/fontisan/true_type_collection.rb +156 -2
- data/lib/fontisan/true_type_font.rb +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -0
- data/lib/fontisan/utils/thread_pool.rb +134 -0
- data/lib/fontisan/validation/checksum_validator.rb +170 -0
- data/lib/fontisan/validation/consistency_validator.rb +197 -0
- data/lib/fontisan/validation/structure_validator.rb +198 -0
- data/lib/fontisan/validation/table_validator.rb +158 -0
- data/lib/fontisan/validation/validator.rb +152 -0
- data/lib/fontisan/variable/axis_normalizer.rb +215 -0
- data/lib/fontisan/variable/delta_applicator.rb +313 -0
- data/lib/fontisan/variable/glyph_delta_processor.rb +218 -0
- data/lib/fontisan/variable/instancer.rb +344 -0
- data/lib/fontisan/variable/metric_delta_processor.rb +282 -0
- data/lib/fontisan/variable/region_matcher.rb +208 -0
- data/lib/fontisan/variable/static_font_builder.rb +213 -0
- data/lib/fontisan/variable/table_updater.rb +219 -0
- data/lib/fontisan/variation/blend_applier.rb +199 -0
- data/lib/fontisan/variation/cache.rb +298 -0
- data/lib/fontisan/variation/cache_key_builder.rb +162 -0
- data/lib/fontisan/variation/converter.rb +268 -0
- data/lib/fontisan/variation/data_extractor.rb +86 -0
- data/lib/fontisan/variation/delta_applier.rb +266 -0
- data/lib/fontisan/variation/delta_parser.rb +228 -0
- data/lib/fontisan/variation/inspector.rb +275 -0
- data/lib/fontisan/variation/instance_generator.rb +273 -0
- data/lib/fontisan/variation/interpolator.rb +231 -0
- data/lib/fontisan/variation/metrics_adjuster.rb +318 -0
- data/lib/fontisan/variation/optimizer.rb +418 -0
- data/lib/fontisan/variation/parallel_generator.rb +150 -0
- data/lib/fontisan/variation/region_matcher.rb +221 -0
- data/lib/fontisan/variation/subsetter.rb +463 -0
- data/lib/fontisan/variation/table_accessor.rb +105 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_command"
|
|
4
|
+
require_relative "../export/exporter"
|
|
5
|
+
require_relative "../font_loader"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Commands
|
|
9
|
+
# ExportCommand provides CLI interface for font export to YAML/JSON
|
|
10
|
+
#
|
|
11
|
+
# This command exports fonts to TTX-like YAML/JSON formats for debugging
|
|
12
|
+
# and font analysis. Supports selective table export and both formats.
|
|
13
|
+
#
|
|
14
|
+
# @example Exporting entire font
|
|
15
|
+
# command = ExportCommand.new(
|
|
16
|
+
# input: "font.ttf",
|
|
17
|
+
# output: "font.yaml"
|
|
18
|
+
# )
|
|
19
|
+
# command.run
|
|
20
|
+
#
|
|
21
|
+
# @example Exporting specific tables
|
|
22
|
+
# command = ExportCommand.new(
|
|
23
|
+
# input: "font.ttf",
|
|
24
|
+
# output: "meta.yaml",
|
|
25
|
+
# tables: ["head", "name", "cmap"]
|
|
26
|
+
# )
|
|
27
|
+
# command.run
|
|
28
|
+
class ExportCommand < BaseCommand
|
|
29
|
+
# Initialize export command
|
|
30
|
+
#
|
|
31
|
+
# @param input [String] Path to input font file
|
|
32
|
+
# @param output [String, nil] Path to output file (default: stdout)
|
|
33
|
+
# @param format [Symbol] Output format (:yaml or :json)
|
|
34
|
+
# @param tables [Array<String>, nil] Specific tables to export
|
|
35
|
+
# @param binary_format [Symbol] Binary encoding (:hex or :base64)
|
|
36
|
+
# @param pretty [Boolean] Pretty-print output
|
|
37
|
+
def initialize(input:, output: nil, format: :yaml, tables: nil,
|
|
38
|
+
binary_format: :hex, pretty: true)
|
|
39
|
+
super()
|
|
40
|
+
@input = input
|
|
41
|
+
@output = output
|
|
42
|
+
@format = format.to_sym
|
|
43
|
+
@tables = tables
|
|
44
|
+
@binary_format = binary_format.to_sym
|
|
45
|
+
@pretty = pretty
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Run the export command
|
|
49
|
+
#
|
|
50
|
+
# @return [Integer] Exit code (0 = success, 1 = error)
|
|
51
|
+
def run
|
|
52
|
+
validate_params!
|
|
53
|
+
|
|
54
|
+
# Load font
|
|
55
|
+
font = load_font
|
|
56
|
+
return 1 unless font
|
|
57
|
+
|
|
58
|
+
# Create exporter
|
|
59
|
+
exporter = Export::Exporter.new(
|
|
60
|
+
font,
|
|
61
|
+
@input,
|
|
62
|
+
binary_format: @binary_format,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Export to model
|
|
66
|
+
export_model = exporter.export(
|
|
67
|
+
tables: @tables || :all,
|
|
68
|
+
format: @format,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Output result
|
|
72
|
+
output_export(export_model)
|
|
73
|
+
|
|
74
|
+
0
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
puts "Error: #{e.message}"
|
|
77
|
+
puts e.backtrace.join("\n") if ENV["DEBUG"]
|
|
78
|
+
1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Validate command parameters
|
|
84
|
+
#
|
|
85
|
+
# @raise [ArgumentError] if parameters are invalid
|
|
86
|
+
# @return [void]
|
|
87
|
+
def validate_params!
|
|
88
|
+
if @input.nil? || @input.empty?
|
|
89
|
+
raise ArgumentError,
|
|
90
|
+
"Input file is required"
|
|
91
|
+
end
|
|
92
|
+
unless File.exist?(@input)
|
|
93
|
+
raise ArgumentError,
|
|
94
|
+
"Input file does not exist: #{@input}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
valid_formats = %i[yaml json ttx]
|
|
98
|
+
unless valid_formats.include?(@format)
|
|
99
|
+
raise ArgumentError,
|
|
100
|
+
"Invalid format: #{@format}. Must be one of: #{valid_formats.join(', ')}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
valid_binary_formats = %i[hex base64]
|
|
104
|
+
unless valid_binary_formats.include?(@binary_format)
|
|
105
|
+
raise ArgumentError,
|
|
106
|
+
"Invalid binary format: #{@binary_format}. " \
|
|
107
|
+
"Must be one of: #{valid_binary_formats.join(', ')}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Validate output directory exists
|
|
111
|
+
if @output
|
|
112
|
+
output_dir = File.dirname(@output)
|
|
113
|
+
unless Dir.exist?(output_dir)
|
|
114
|
+
raise ArgumentError,
|
|
115
|
+
"Output directory does not exist: #{output_dir}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Load the font file
|
|
121
|
+
#
|
|
122
|
+
# @return [TrueTypeFont, OpenTypeFont, nil] The loaded font or nil on error
|
|
123
|
+
def load_font
|
|
124
|
+
FontLoader.load(@input)
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
puts "Error loading font: #{e.message}"
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Output the export
|
|
131
|
+
#
|
|
132
|
+
# @param export_model [Models::FontExport, String] The export model or TTX XML
|
|
133
|
+
# @return [void]
|
|
134
|
+
def output_export(export_model)
|
|
135
|
+
content = if export_model.is_a?(String)
|
|
136
|
+
# TTX XML string
|
|
137
|
+
export_model
|
|
138
|
+
else
|
|
139
|
+
# FontExport model
|
|
140
|
+
case @format
|
|
141
|
+
when :yaml
|
|
142
|
+
export_model.to_yaml
|
|
143
|
+
when :json
|
|
144
|
+
if @pretty
|
|
145
|
+
JSON.pretty_generate(JSON.parse(export_model.to_json))
|
|
146
|
+
else
|
|
147
|
+
export_model.to_json
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if @output
|
|
153
|
+
File.write(@output, content)
|
|
154
|
+
puts "Exported to #{@output}"
|
|
155
|
+
else
|
|
156
|
+
puts content
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Fontisan
|
|
4
4
|
module Commands
|
|
5
|
-
# Command to extract font metadata information.
|
|
5
|
+
# Command to extract font or collection metadata information.
|
|
6
6
|
#
|
|
7
|
-
# This command
|
|
7
|
+
# This command auto-detects whether the input is a collection (TTC/OTC)
|
|
8
|
+
# or individual font (TTF/OTF) and returns the appropriate model:
|
|
9
|
+
# - CollectionInfo for TTC/OTC files
|
|
10
|
+
# - FontInfo for TTF/OTF files
|
|
11
|
+
#
|
|
12
|
+
# For individual fonts, extracts comprehensive information from various tables:
|
|
8
13
|
# - name table: family names, version, copyright, etc.
|
|
9
14
|
# - OS/2 table: vendor ID, embedding permissions
|
|
10
15
|
# - head table: font revision, units per em
|
|
@@ -13,11 +18,42 @@ module Fontisan
|
|
|
13
18
|
# command = InfoCommand.new("path/to/font.ttf")
|
|
14
19
|
# info = command.run
|
|
15
20
|
# puts info.family_name
|
|
21
|
+
#
|
|
22
|
+
# @example Extract collection information
|
|
23
|
+
# command = InfoCommand.new("path/to/fonts.ttc")
|
|
24
|
+
# info = command.run
|
|
25
|
+
# puts "Collection has #{info.num_fonts} fonts"
|
|
16
26
|
class InfoCommand < BaseCommand
|
|
17
|
-
# Extract
|
|
27
|
+
# Extract information from font or collection.
|
|
28
|
+
#
|
|
29
|
+
# Auto-detects file type and returns appropriate model.
|
|
18
30
|
#
|
|
19
|
-
# @return [Models::FontInfo]
|
|
31
|
+
# @return [Models::FontInfo, Models::CollectionInfo] Metadata information
|
|
20
32
|
def run
|
|
33
|
+
if FontLoader.collection?(@font_path)
|
|
34
|
+
collection_info
|
|
35
|
+
else
|
|
36
|
+
font_info
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Get collection information
|
|
43
|
+
#
|
|
44
|
+
# @return [Models::CollectionInfo] Collection metadata
|
|
45
|
+
def collection_info
|
|
46
|
+
collection = FontLoader.load_collection(@font_path)
|
|
47
|
+
|
|
48
|
+
File.open(@font_path, "rb") do |io|
|
|
49
|
+
collection.collection_info(io, @font_path)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get individual font information
|
|
54
|
+
#
|
|
55
|
+
# @return [Models::FontInfo] Font metadata
|
|
56
|
+
def font_info
|
|
21
57
|
info = Models::FontInfo.new
|
|
22
58
|
populate_font_format(info)
|
|
23
59
|
populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
|
|
@@ -26,8 +62,6 @@ module Fontisan
|
|
|
26
62
|
info
|
|
27
63
|
end
|
|
28
64
|
|
|
29
|
-
private
|
|
30
|
-
|
|
31
65
|
# Populate font format and variable status based on font class and table presence.
|
|
32
66
|
#
|
|
33
67
|
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "base_command"
|
|
5
|
+
require_relative "../variable/instancer"
|
|
6
|
+
require_relative "../variation/validator"
|
|
7
|
+
require_relative "../variation/parallel_generator"
|
|
8
|
+
require_relative "../converters/format_converter"
|
|
9
|
+
require_relative "../error"
|
|
10
|
+
|
|
11
|
+
module Fontisan
|
|
12
|
+
module Commands
|
|
13
|
+
# CLI command for generating static font instances from variable fonts
|
|
14
|
+
#
|
|
15
|
+
# Provides command-line interface for:
|
|
16
|
+
# - Instancing at specific coordinates
|
|
17
|
+
# - Using named instances
|
|
18
|
+
# - Converting output format during instancing
|
|
19
|
+
# - Listing available instances
|
|
20
|
+
# - Validation before generation
|
|
21
|
+
# - Dry-run mode for previewing
|
|
22
|
+
# - Progress tracking
|
|
23
|
+
# - Parallel batch generation
|
|
24
|
+
#
|
|
25
|
+
# @example Instance at coordinates
|
|
26
|
+
# fontisan instance variable.ttf --wght=700 --output=bold.ttf
|
|
27
|
+
#
|
|
28
|
+
# @example Instance with validation
|
|
29
|
+
# fontisan instance variable.ttf --wght=700 --validate --output=bold.ttf
|
|
30
|
+
#
|
|
31
|
+
# @example Dry-run to preview
|
|
32
|
+
# fontisan instance variable.ttf --wght=700 --dry-run
|
|
33
|
+
#
|
|
34
|
+
# @example Instance with progress
|
|
35
|
+
# fontisan instance variable.ttf --wght=700 --progress --output=bold.ttf
|
|
36
|
+
class InstanceCommand < BaseCommand
|
|
37
|
+
# Instance a variable font at specified coordinates
|
|
38
|
+
#
|
|
39
|
+
# @param input_path [String] Path to variable font file
|
|
40
|
+
def execute(input_path, options = {})
|
|
41
|
+
# Load variable font
|
|
42
|
+
font = load_font(input_path)
|
|
43
|
+
|
|
44
|
+
# Validate font if requested
|
|
45
|
+
validate_font(font) if options[:validate]
|
|
46
|
+
|
|
47
|
+
# Create instancer
|
|
48
|
+
instancer = Variable::Instancer.new(font)
|
|
49
|
+
|
|
50
|
+
# Handle list-instances option
|
|
51
|
+
if options[:list_instances]
|
|
52
|
+
list_instances(instancer)
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Handle dry-run mode
|
|
57
|
+
if options[:dry_run]
|
|
58
|
+
preview_instance(instancer, options)
|
|
59
|
+
return
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Determine output path
|
|
63
|
+
output_path = determine_output_path(input_path, options)
|
|
64
|
+
|
|
65
|
+
# Generate instance
|
|
66
|
+
if options[:named_instance]
|
|
67
|
+
instance_named(instancer, options[:named_instance], output_path,
|
|
68
|
+
options)
|
|
69
|
+
else
|
|
70
|
+
instance_coords(instancer, extract_coordinates(options), output_path,
|
|
71
|
+
options)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts "Static font instance written to: #{output_path}"
|
|
75
|
+
rescue VariationError => e
|
|
76
|
+
$stderr.puts "Variation Error: #{e.detailed_message}"
|
|
77
|
+
exit 1
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
$stderr.puts "Error: #{e.message}"
|
|
80
|
+
$stderr.puts e.backtrace.first(5).join("\n") if options[:verbose]
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Validate font before generating instance
|
|
87
|
+
#
|
|
88
|
+
# @param font [Object] Font object
|
|
89
|
+
def validate_font(font)
|
|
90
|
+
puts "Validating font..." if @options[:verbose]
|
|
91
|
+
|
|
92
|
+
validator = Variation::Validator.new(font)
|
|
93
|
+
errors = validator.validate
|
|
94
|
+
|
|
95
|
+
if errors.any?
|
|
96
|
+
$stderr.puts "Validation errors found:"
|
|
97
|
+
errors.each do |error|
|
|
98
|
+
$stderr.puts " - #{error}"
|
|
99
|
+
end
|
|
100
|
+
exit 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
puts "Font validation passed" if @options[:verbose]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Preview instance without generating
|
|
107
|
+
#
|
|
108
|
+
# @param instancer [Variable::Instancer] Instancer object
|
|
109
|
+
# @param options [Hash] Command options
|
|
110
|
+
def preview_instance(instancer, options)
|
|
111
|
+
coords = extract_coordinates(options)
|
|
112
|
+
|
|
113
|
+
if coords.empty?
|
|
114
|
+
raise ArgumentError,
|
|
115
|
+
"No coordinates specified. Use --wght=700, --wdth=100, etc."
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
puts "Dry-run mode: Preview of instance generation"
|
|
119
|
+
puts
|
|
120
|
+
puts "Coordinates:"
|
|
121
|
+
coords.each do |axis, value|
|
|
122
|
+
puts " #{axis}: #{value}"
|
|
123
|
+
end
|
|
124
|
+
puts
|
|
125
|
+
puts "Output would be written to: #{determine_output_path(@input_path, options)}"
|
|
126
|
+
puts
|
|
127
|
+
puts "Use without --dry-run to actually generate the instance."
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Instance at specific coordinates
|
|
131
|
+
#
|
|
132
|
+
# @param instancer [Variable::Instancer] Instancer object
|
|
133
|
+
# @param coords [Hash] User coordinates
|
|
134
|
+
# @param output_path [String] Output file path
|
|
135
|
+
# @param options [Hash] Command options
|
|
136
|
+
def instance_coords(instancer, coords, output_path, options)
|
|
137
|
+
if coords.empty?
|
|
138
|
+
raise ArgumentError,
|
|
139
|
+
"No coordinates specified. Use --wght=700, --wdth=100, etc."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Show progress if requested
|
|
143
|
+
print "Generating instance..." if options[:progress]
|
|
144
|
+
|
|
145
|
+
# Generate instance
|
|
146
|
+
binary = instancer.instance(coords)
|
|
147
|
+
|
|
148
|
+
puts " done" if options[:progress]
|
|
149
|
+
|
|
150
|
+
# Convert format if requested
|
|
151
|
+
if options[:to]
|
|
152
|
+
print "Converting format..." if options[:progress]
|
|
153
|
+
binary = convert_format(binary, options)
|
|
154
|
+
puts " done" if options[:progress]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Write to file
|
|
158
|
+
print "Writing output..." if options[:progress]
|
|
159
|
+
File.binwrite(output_path, binary)
|
|
160
|
+
puts " done" if options[:progress]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Instance using named instance
|
|
164
|
+
#
|
|
165
|
+
# @param instancer [Variable::Instancer] Instancer object
|
|
166
|
+
# @param instance_name [String] Named instance name
|
|
167
|
+
# @param output_path [String] Output file path
|
|
168
|
+
# @param options [Hash] Command options
|
|
169
|
+
def instance_named(instancer, instance_name, output_path, options)
|
|
170
|
+
# Generate instance
|
|
171
|
+
binary = instancer.instance_named(instance_name)
|
|
172
|
+
|
|
173
|
+
# Convert format if requested
|
|
174
|
+
binary = convert_format(binary, options) if options[:to]
|
|
175
|
+
|
|
176
|
+
# Write to file
|
|
177
|
+
File.binwrite(output_path, binary)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# List available named instances
|
|
181
|
+
#
|
|
182
|
+
# @param instancer [Variable::Instancer] Instancer object
|
|
183
|
+
def list_instances(instancer)
|
|
184
|
+
instances = instancer.named_instances
|
|
185
|
+
|
|
186
|
+
if instances.empty?
|
|
187
|
+
puts "No named instances defined in font."
|
|
188
|
+
return
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
puts "Available named instances:"
|
|
192
|
+
puts
|
|
193
|
+
|
|
194
|
+
instances.each do |instance|
|
|
195
|
+
puts " #{instance[:name]}"
|
|
196
|
+
puts " Coordinates:"
|
|
197
|
+
instance[:coordinates].each do |axis, value|
|
|
198
|
+
puts " #{axis}: #{value}"
|
|
199
|
+
end
|
|
200
|
+
puts
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Extract axis coordinates from options
|
|
205
|
+
#
|
|
206
|
+
# @param options [Hash] Command options
|
|
207
|
+
# @return [Hash] Coordinates hash
|
|
208
|
+
def extract_coordinates(options)
|
|
209
|
+
coords = {}
|
|
210
|
+
|
|
211
|
+
# Check for common axis options
|
|
212
|
+
coords["wght"] = options[:wght].to_f if options[:wght]
|
|
213
|
+
coords["wdth"] = options[:wdth].to_f if options[:wdth]
|
|
214
|
+
coords["slnt"] = options[:slnt].to_f if options[:slnt]
|
|
215
|
+
coords["ital"] = options[:ital].to_f if options[:ital]
|
|
216
|
+
coords["opsz"] = options[:opsz].to_f if options[:opsz]
|
|
217
|
+
|
|
218
|
+
# Allow arbitrary axis coordinates via --axis-TAG=value
|
|
219
|
+
options.each do |key, value|
|
|
220
|
+
key_str = key.to_s
|
|
221
|
+
if key_str.start_with?("axis_")
|
|
222
|
+
axis_tag = key_str.sub("axis_", "")
|
|
223
|
+
coords[axis_tag] = value.to_f
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
coords
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Determine output path
|
|
231
|
+
#
|
|
232
|
+
# @param input_path [String] Input file path
|
|
233
|
+
# @param options [Hash] Command options
|
|
234
|
+
# @return [String] Output path
|
|
235
|
+
def determine_output_path(input_path, options)
|
|
236
|
+
return options[:output] if options[:output]
|
|
237
|
+
|
|
238
|
+
# Generate default output name
|
|
239
|
+
base = File.basename(input_path, ".*")
|
|
240
|
+
ext = options[:to] || File.extname(input_path)[1..]
|
|
241
|
+
dir = File.dirname(input_path)
|
|
242
|
+
|
|
243
|
+
"#{dir}/#{base}-instance.#{ext}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Convert format using FormatConverter
|
|
247
|
+
#
|
|
248
|
+
# @param binary [String] Font binary
|
|
249
|
+
# @param options [Hash] Command options
|
|
250
|
+
# @return [String] Converted binary
|
|
251
|
+
def convert_format(binary, options)
|
|
252
|
+
target_format = options[:to].to_sym
|
|
253
|
+
|
|
254
|
+
# Load font from binary
|
|
255
|
+
require "tempfile"
|
|
256
|
+
Tempfile.create(["instance", ".ttf"]) do |temp_file|
|
|
257
|
+
temp_file.binmode
|
|
258
|
+
temp_file.write(binary)
|
|
259
|
+
temp_file.flush
|
|
260
|
+
|
|
261
|
+
font = FontLoader.load(temp_file.path)
|
|
262
|
+
converter = Converters::FormatConverter.new
|
|
263
|
+
|
|
264
|
+
result = converter.convert(font, target_format)
|
|
265
|
+
|
|
266
|
+
case target_format
|
|
267
|
+
when :woff, :woff2
|
|
268
|
+
result[:font_data]
|
|
269
|
+
when :svg
|
|
270
|
+
result[:svg_xml]
|
|
271
|
+
else
|
|
272
|
+
binary
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
warn "Format conversion failed: #{e.message}"
|
|
277
|
+
binary
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Load font from file
|
|
281
|
+
#
|
|
282
|
+
# @param path [String] Font file path
|
|
283
|
+
# @return [Object] Font object
|
|
284
|
+
def load_font(path)
|
|
285
|
+
unless File.exist?(path)
|
|
286
|
+
raise ArgumentError, "Font file not found: #{path}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
FontLoader.load(path)
|
|
290
|
+
rescue StandardError => e
|
|
291
|
+
raise ArgumentError, "Failed to load font: #{e.message}"
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../font_loader"
|
|
4
|
+
require_relative "../models/collection_list_info"
|
|
5
|
+
require_relative "../models/font_summary"
|
|
6
|
+
require_relative "../tables/name"
|
|
7
|
+
require_relative "../error"
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Commands
|
|
11
|
+
# Command to list contents of font files (collections or individual fonts).
|
|
12
|
+
#
|
|
13
|
+
# This command provides a universal "ls" interface that auto-detects
|
|
14
|
+
# whether the input is a collection (TTC/OTC) or individual font (TTF/OTF)
|
|
15
|
+
# and returns the appropriate listing:
|
|
16
|
+
# - For collections: Lists all fonts in the collection
|
|
17
|
+
# - For individual fonts: Shows a quick summary
|
|
18
|
+
#
|
|
19
|
+
# @example List fonts in collection
|
|
20
|
+
# command = LsCommand.new("fonts.ttc")
|
|
21
|
+
# list = command.run
|
|
22
|
+
# puts "Contains #{list.num_fonts} fonts"
|
|
23
|
+
#
|
|
24
|
+
# @example Get font summary
|
|
25
|
+
# command = LsCommand.new("font.ttf")
|
|
26
|
+
# summary = command.run
|
|
27
|
+
# puts "#{summary.family_name} - #{summary.num_glyphs} glyphs"
|
|
28
|
+
class LsCommand
|
|
29
|
+
# Initialize ls command
|
|
30
|
+
#
|
|
31
|
+
# @param file_path [String] Path to font or collection file
|
|
32
|
+
# @param options [Hash] Command options
|
|
33
|
+
# @option options [Integer] :font_index Index for TTC/OTC (unused for ls)
|
|
34
|
+
def initialize(file_path, options = {})
|
|
35
|
+
@file_path = file_path
|
|
36
|
+
@options = options
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Execute the ls command
|
|
40
|
+
#
|
|
41
|
+
# Auto-detects file type and returns appropriate model:
|
|
42
|
+
# - CollectionListInfo for TTC/OTC files
|
|
43
|
+
# - FontSummary for TTF/OTF files
|
|
44
|
+
#
|
|
45
|
+
# @return [CollectionListInfo, FontSummary] List or summary
|
|
46
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
47
|
+
# @raise [Error] for loading or processing failures
|
|
48
|
+
def run
|
|
49
|
+
if FontLoader.collection?(@file_path)
|
|
50
|
+
list_collection
|
|
51
|
+
else
|
|
52
|
+
font_summary
|
|
53
|
+
end
|
|
54
|
+
rescue Errno::ENOENT
|
|
55
|
+
raise
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
raise Error, "Failed to list file contents: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# List fonts in a collection
|
|
63
|
+
#
|
|
64
|
+
# @return [CollectionListInfo] List of fonts with metadata
|
|
65
|
+
def list_collection
|
|
66
|
+
collection = FontLoader.load_collection(@file_path)
|
|
67
|
+
|
|
68
|
+
File.open(@file_path, "rb") do |io|
|
|
69
|
+
list = collection.list_fonts(io)
|
|
70
|
+
list.collection_path = @file_path
|
|
71
|
+
list
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Create summary for individual font
|
|
76
|
+
#
|
|
77
|
+
# @return [FontSummary] Quick font summary
|
|
78
|
+
def font_summary
|
|
79
|
+
font = FontLoader.load(@file_path)
|
|
80
|
+
|
|
81
|
+
# Extract basic info
|
|
82
|
+
name_table = font.table("name")
|
|
83
|
+
post_table = font.table("post")
|
|
84
|
+
|
|
85
|
+
family_name = name_table&.english_name(Tables::Name::FAMILY) || "Unknown"
|
|
86
|
+
subfamily_name = name_table&.english_name(Tables::Name::SUBFAMILY) || "Regular"
|
|
87
|
+
|
|
88
|
+
# Determine font format
|
|
89
|
+
sfnt = font.header.sfnt_version
|
|
90
|
+
font_format = case sfnt
|
|
91
|
+
when 0x00010000, 0x74727565 # 0x74727565 = 'true'
|
|
92
|
+
"TrueType"
|
|
93
|
+
when 0x4F54544F # 'OTTO'
|
|
94
|
+
"OpenType"
|
|
95
|
+
else
|
|
96
|
+
"Unknown"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
num_glyphs = post_table&.glyph_names&.length || 0
|
|
100
|
+
num_tables = font.table_names.length
|
|
101
|
+
|
|
102
|
+
Models::FontSummary.new(
|
|
103
|
+
font_path: @file_path,
|
|
104
|
+
family_name: family_name,
|
|
105
|
+
subfamily_name: subfamily_name,
|
|
106
|
+
font_format: font_format,
|
|
107
|
+
num_glyphs: num_glyphs,
|
|
108
|
+
num_tables: num_tables,
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|