fontist 2.1.6 → 2.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/.gitignore +5 -0
- data/.rubocop_todo.yml +184 -120
- data/README.adoc +1 -1
- data/TODO.fontist-v5.md +196 -0
- data/fontist.gemspec +1 -1
- data/lib/fontist/cache/store.rb +17 -7
- data/lib/fontist/cli.rb +116 -0
- data/lib/fontist/errors.rb +61 -0
- data/lib/fontist/extract.rb +13 -0
- data/lib/fontist/font.rb +10 -1
- data/lib/fontist/font_collection.rb +1 -0
- data/lib/fontist/font_finder.rb +154 -0
- data/lib/fontist/font_installer.rb +173 -15
- data/lib/fontist/font_model.rb +1 -0
- data/lib/fontist/font_path.rb +4 -4
- data/lib/fontist/font_style.rb +17 -0
- data/lib/fontist/format_matcher.rb +176 -0
- data/lib/fontist/format_spec.rb +80 -0
- data/lib/fontist/formula.rb +133 -215
- data/lib/fontist/formula_picker.rb +39 -4
- data/lib/fontist/import/create_formula.rb +80 -3
- data/lib/fontist/import/formula_builder.rb +5 -1
- data/lib/fontist/import/google/font_database.rb +15 -153
- data/lib/fontist/import/google/formula_builder.rb +26 -0
- data/lib/fontist/import/google/formula_builders/base_formula_builder.rb +93 -0
- data/lib/fontist/import/google/formula_builders/formula_builder_v4.rb +155 -0
- data/lib/fontist/import/google/formula_builders/formula_builder_v5.rb +193 -0
- data/lib/fontist/import/google_fonts_importer.rb +17 -5
- data/lib/fontist/import/{macos.rb → macos_importer.rb} +4 -2
- data/lib/fontist/import/recursive_extraction.rb +2 -0
- data/lib/fontist/import/{sil_import.rb → sil_importer.rb} +3 -1
- data/lib/fontist/import/upgrade_formulas.rb +1 -1
- data/lib/fontist/import/v4_to_v5_migrator.rb +263 -0
- data/lib/fontist/import_cli.rb +20 -2
- data/lib/fontist/indexes/index_mixin.rb +8 -4
- data/lib/fontist/manifest.rb +27 -1
- data/lib/fontist/path_scanning.rb +1 -1
- data/lib/fontist/resource.rb +54 -0
- data/lib/fontist/resource_collection.rb +18 -0
- data/lib/fontist/system_index.rb +18 -9
- data/lib/fontist/utils/downloader.rb +0 -2
- data/lib/fontist/utils/github_client.rb +5 -2
- data/lib/fontist/utils/github_url.rb +4 -3
- data/lib/fontist/utils.rb +1 -1
- data/lib/fontist/version.rb +1 -1
- data/lib/fontist.rb +2 -2
- metadata +19 -8
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
require "excavate"
|
|
2
|
+
require_relative "format_matcher"
|
|
2
3
|
|
|
3
4
|
module Fontist
|
|
4
5
|
class FontInstaller
|
|
5
6
|
attr_reader :location
|
|
6
7
|
|
|
7
|
-
def initialize(formula, font_name: nil, no_progress: false, location: nil
|
|
8
|
+
def initialize(formula, font_name: nil, no_progress: false, location: nil,
|
|
9
|
+
format_spec: nil, confirmation: "no")
|
|
8
10
|
@formula = formula
|
|
9
11
|
@font_name = font_name
|
|
10
12
|
@no_progress = no_progress
|
|
11
13
|
@location = InstallLocation.create(formula, location_type: location)
|
|
14
|
+
@format_spec = format_spec
|
|
15
|
+
@confirmation = confirmation
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def install(confirmation:)
|
|
@@ -54,6 +58,7 @@ module Fontist
|
|
|
54
58
|
|
|
55
59
|
def license_is_accepted?(confirmation)
|
|
56
60
|
return true unless @formula.license_required?
|
|
61
|
+
return true if @formula.licensed_for_current_platform?
|
|
57
62
|
|
|
58
63
|
"yes".casecmp?(confirmation)
|
|
59
64
|
end
|
|
@@ -93,7 +98,20 @@ module Fontist
|
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
def resource_options
|
|
96
|
-
@
|
|
101
|
+
@resource_options ||= begin
|
|
102
|
+
if @formula.resources.size == 1 || !@formula.v5?
|
|
103
|
+
@formula.resources.first
|
|
104
|
+
elsif @format_spec&.has_constraints?
|
|
105
|
+
matcher = FormatMatcher.new(@format_spec)
|
|
106
|
+
matcher.select_preferred_resource(@formula.resources)
|
|
107
|
+
else
|
|
108
|
+
find_desktop_resource || @formula.resources.first
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def find_desktop_resource
|
|
114
|
+
@formula.resources.find { |r| r.format && FormatMatcher::DESKTOP_FORMATS.include?(r.format) }
|
|
97
115
|
end
|
|
98
116
|
|
|
99
117
|
def font_file?(path)
|
|
@@ -105,13 +123,32 @@ module Fontist
|
|
|
105
123
|
end
|
|
106
124
|
|
|
107
125
|
def source_files
|
|
108
|
-
@source_files ||=
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
@source_files ||= begin
|
|
127
|
+
styles = filtered_styles
|
|
128
|
+
|
|
129
|
+
# Use FormatMatcher for filtering
|
|
130
|
+
if @format_spec&.has_constraints? && @formula.v5?
|
|
131
|
+
matcher = FormatMatcher.new(@format_spec)
|
|
132
|
+
styles = matcher.filter_styles(styles)
|
|
111
133
|
end
|
|
134
|
+
|
|
135
|
+
file_names = styles.map { |s| s.source_font || s.font }
|
|
136
|
+
|
|
137
|
+
if @formula.v5? && resource_options&.source == "google" && file_names.any?
|
|
138
|
+
resource_basenames = Array(resource_options.files).map { |f| File.basename(f) }
|
|
139
|
+
unless file_names.any? { |f| resource_basenames.include?(f) }
|
|
140
|
+
return resource_basenames
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
file_names
|
|
112
145
|
end
|
|
113
146
|
end
|
|
114
147
|
|
|
148
|
+
def filtered_styles
|
|
149
|
+
fonts.flat_map(&:styles)
|
|
150
|
+
end
|
|
151
|
+
|
|
115
152
|
def fonts
|
|
116
153
|
@formula.all_fonts.select do |font|
|
|
117
154
|
@font_name.nil? || font.name.casecmp?(@font_name)
|
|
@@ -129,23 +166,144 @@ module Fontist
|
|
|
129
166
|
end
|
|
130
167
|
|
|
131
168
|
def subdirectories
|
|
132
|
-
@subdirectories ||=
|
|
169
|
+
@subdirectories ||= begin
|
|
170
|
+
extracts = [@formula.extract].flatten.compact
|
|
171
|
+
# options is a collection, so we need to flatten it too
|
|
172
|
+
options = extracts.flat_map { |e| e.options }.compact
|
|
173
|
+
options.filter_map(&:fonts_sub_dir)
|
|
174
|
+
end
|
|
133
175
|
end
|
|
134
176
|
|
|
135
177
|
def install_font_file(source)
|
|
136
178
|
source_basename = File.basename(source)
|
|
137
179
|
target_name = target_filename(source_basename) || source_basename
|
|
180
|
+
source_format = detect_font_format(source)
|
|
181
|
+
|
|
182
|
+
# Check if transcoding is needed (format requested but not available)
|
|
183
|
+
if @format_spec&.format && @format_spec.format != source_format
|
|
184
|
+
check_transcode_license_warning!
|
|
185
|
+
install_with_conversion(source, target_name, source_format)
|
|
186
|
+
else
|
|
187
|
+
@location.install_font(source, target_name)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def detect_font_format(path)
|
|
192
|
+
ext = File.extname(path).downcase.delete(".")
|
|
193
|
+
case ext
|
|
194
|
+
when "ttf", "otf", "woff", "woff2", "ttc", "otc", "dfont"
|
|
195
|
+
ext
|
|
196
|
+
else
|
|
197
|
+
"ttf" # Default
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Check and warn about license implications of transcoding
|
|
202
|
+
def check_transcode_license_warning!
|
|
203
|
+
return unless @formula.license_required?
|
|
204
|
+
|
|
205
|
+
if @confirmation != "yes"
|
|
206
|
+
raise Errors::TranscodeLicenseNotAcceptedError.new(
|
|
207
|
+
@formula.fonts.first&.name || @formula.name,
|
|
208
|
+
)
|
|
209
|
+
end
|
|
138
210
|
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
211
|
+
# User has accepted the license, but we still warn them
|
|
212
|
+
# that transcoding may not be permitted by all licenses
|
|
213
|
+
Fontist.ui.warn("\n#{'=' * 60}")
|
|
214
|
+
Fontist.ui.warn("LICENSE TRANSCODING NOTICE")
|
|
215
|
+
Fontist.ui.warn("=" * 60)
|
|
216
|
+
Fontist.ui.warn(
|
|
217
|
+
"You are transcoding a font that requires a license agreement.",
|
|
218
|
+
)
|
|
219
|
+
Fontist.ui.warn(
|
|
220
|
+
"Some font licenses do not permit conversion or modification,",
|
|
221
|
+
)
|
|
222
|
+
Fontist.ui.warn(
|
|
223
|
+
"of which transcoding is a type. Please ensure your use of",
|
|
224
|
+
)
|
|
225
|
+
Fontist.ui.warn("this font complies with the license terms.")
|
|
226
|
+
Fontist.ui.warn("#{'=' * 60}\n")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def install_with_conversion(source, target_name, source_format)
|
|
230
|
+
target_format = @format_spec.format
|
|
231
|
+
|
|
232
|
+
# Check if Fontisan can convert between formats
|
|
233
|
+
matcher = FormatMatcher.new(@format_spec)
|
|
234
|
+
unless matcher.can_convert?(source_format, target_format)
|
|
235
|
+
Fontist.ui.warn(
|
|
236
|
+
"Cannot convert from #{source_format} to #{target_format}",
|
|
237
|
+
)
|
|
238
|
+
Fontist.ui.warn("Installing original format instead")
|
|
239
|
+
return @location.install_font(source, target_name)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
begin
|
|
243
|
+
converted_path = convert_with_fontisan(source, target_format)
|
|
244
|
+
|
|
245
|
+
# Determine where to save converted font
|
|
246
|
+
converted_name = target_name.sub(/\.[^.]+$/, ".#{target_format}")
|
|
247
|
+
|
|
248
|
+
Fontist.ui.success(
|
|
249
|
+
"Converted #{source_format} to #{target_format}: #{converted_name}",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Install converted font
|
|
253
|
+
result = @location.install_font(converted_path, converted_name)
|
|
254
|
+
|
|
255
|
+
# Keep original if requested and transcode_path specified
|
|
256
|
+
if @format_spec.keep_original && @format_spec.transcode_path
|
|
257
|
+
@location.install_font(source, target_name)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Clean up temp converted file if Fontisan created one
|
|
261
|
+
cleanup_temp_file(converted_path) if converted_path != source
|
|
262
|
+
|
|
263
|
+
result
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
Fontist.ui.warn("Could not convert to #{target_format}: #{e.message}")
|
|
266
|
+
Fontist.ui.warn("Installing original format instead")
|
|
267
|
+
@location.install_font(source, target_name)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Convert font using Fontisan library
|
|
272
|
+
def convert_with_fontisan(source_path, target_format)
|
|
273
|
+
require "fontisan"
|
|
274
|
+
|
|
275
|
+
font = Fontisan::FontLoader.load(source_path)
|
|
276
|
+
|
|
277
|
+
case target_format
|
|
278
|
+
when "woff"
|
|
279
|
+
font.to_woff(path: temp_path_for(source_path, "woff"))
|
|
280
|
+
when "woff2"
|
|
281
|
+
font.to_woff2(path: temp_path_for(source_path, "woff2"))
|
|
282
|
+
else
|
|
283
|
+
raise Errors::UnsupportedTranscodeError.new(
|
|
284
|
+
File.extname(source_path),
|
|
285
|
+
target_format,
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
rescue LoadError
|
|
289
|
+
Fontist.ui.error(
|
|
290
|
+
"Fontisan gem not found. Transcoding requires the fontisan gem.",
|
|
291
|
+
)
|
|
292
|
+
Fontist.ui.error("Add it to your Gemfile or run: gem install fontisan")
|
|
293
|
+
raise
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def temp_path_for(source_path, format)
|
|
297
|
+
base = File.basename(source_path, ".*")
|
|
298
|
+
dir = @format_spec&.transcode_path || Dir.mktmpdir
|
|
299
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
|
300
|
+
File.join(dir, "#{base}.#{format}")
|
|
301
|
+
end
|
|
147
302
|
|
|
148
|
-
|
|
303
|
+
def cleanup_temp_file(path)
|
|
304
|
+
File.delete(path) if File.exist?(path)
|
|
305
|
+
rescue StandardError
|
|
306
|
+
# Ignore cleanup errors
|
|
149
307
|
end
|
|
150
308
|
|
|
151
309
|
def macos_asset_directory
|
data/lib/fontist/font_model.rb
CHANGED
data/lib/fontist/font_path.rb
CHANGED
|
@@ -37,10 +37,10 @@ module Fontist
|
|
|
37
37
|
|
|
38
38
|
# On Windows, use case-insensitive comparison; on Unix, case-sensitive
|
|
39
39
|
result = if Fontist::Utils::System.windows?
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
normalized_path.downcase.start_with?(normalized_fonts_path.downcase)
|
|
41
|
+
else
|
|
42
|
+
normalized_path.start_with?(normalized_fonts_path)
|
|
43
|
+
end
|
|
44
44
|
|
|
45
45
|
puts " result: #{result.inspect}" if ENV["DEBUG_FONT_PATH"]
|
|
46
46
|
result
|
data/lib/fontist/font_style.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "lutaml/model"
|
|
2
2
|
|
|
3
3
|
module Fontist
|
|
4
|
+
# FontStyle - v5 font style with format metadata for multi-format support
|
|
4
5
|
class FontStyle < Lutaml::Model::Serializable
|
|
5
6
|
attribute :family_name, :string
|
|
6
7
|
attribute :type, :string
|
|
@@ -17,6 +18,12 @@ module Fontist
|
|
|
17
18
|
attribute :default_type, :string
|
|
18
19
|
attribute :override, :string
|
|
19
20
|
|
|
21
|
+
# v5 format metadata
|
|
22
|
+
attribute :formats, :string, collection: true # ["ttf", "woff2"]
|
|
23
|
+
attribute :variable_font, :boolean # true/false
|
|
24
|
+
attribute :variable_axes, :string, collection: true # ["wght", "wdth"]
|
|
25
|
+
attribute :source_resource, :string
|
|
26
|
+
|
|
20
27
|
key_value do
|
|
21
28
|
map "family_name", to: :family_name
|
|
22
29
|
map "type", to: :type
|
|
@@ -32,6 +39,16 @@ module Fontist
|
|
|
32
39
|
map "default_family_name", to: :default_family_name
|
|
33
40
|
map "default_type", to: :default_type
|
|
34
41
|
map "override", to: :override
|
|
42
|
+
map "formats", to: :formats
|
|
43
|
+
map "variable_font", to: :variable_font
|
|
44
|
+
map "variable_axes", to: :variable_axes
|
|
45
|
+
map "source_resource", to: :source_resource
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Helper to check if this is a variable font
|
|
49
|
+
# Returns false if not set, true only if explicitly set to true
|
|
50
|
+
def variable_font?
|
|
51
|
+
variable_font == true
|
|
35
52
|
end
|
|
36
53
|
end
|
|
37
54
|
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
module Fontist
|
|
2
|
+
# Centralized format matching service
|
|
3
|
+
#
|
|
4
|
+
# All format matching logic exists here. Other classes delegate to this.
|
|
5
|
+
# Supports matching across:
|
|
6
|
+
# - Resources (formula download sources)
|
|
7
|
+
# - Styles (font style entries)
|
|
8
|
+
# - Indexed fonts (system/user/fontist indexes)
|
|
9
|
+
# - Collections (TTC/OTC with multiple fonts)
|
|
10
|
+
#
|
|
11
|
+
# For transcoding, delegates to Fontisan library.
|
|
12
|
+
class FormatMatcher
|
|
13
|
+
# All supported formats
|
|
14
|
+
DESKTOP_FORMATS = %w[ttf otf ttc otc dfont].freeze
|
|
15
|
+
WEB_FORMATS = %w[woff woff2].freeze
|
|
16
|
+
ALL_FORMATS = (DESKTOP_FORMATS + WEB_FORMATS).freeze
|
|
17
|
+
|
|
18
|
+
def initialize(format_spec)
|
|
19
|
+
@spec = format_spec || FormatSpec.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if a resource matches the format spec
|
|
23
|
+
def matches_resource?(resource)
|
|
24
|
+
return true unless @spec.has_constraints?
|
|
25
|
+
|
|
26
|
+
if @spec.format && resource.format && resource.format != @spec.format
|
|
27
|
+
return false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @spec.variable_requested?
|
|
31
|
+
return false unless resource.variable_font?
|
|
32
|
+
return axes_match?(resource.variable_axes) if @spec.axes.any?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if a style matches the format spec
|
|
39
|
+
# @param style [FontStyle, SystemIndexFont] Style object to check
|
|
40
|
+
def matches_style?(style)
|
|
41
|
+
return true unless @spec.has_constraints?
|
|
42
|
+
|
|
43
|
+
# Check format constraint for FontStyle (v5 formulas have formats)
|
|
44
|
+
if @spec.format && style.is_a?(FontStyle) && style.formats && !Array(style.formats).include?(@spec.format)
|
|
45
|
+
return false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if @spec.variable_requested?
|
|
49
|
+
# Check if style is variable
|
|
50
|
+
is_variable = style.variable_font?
|
|
51
|
+
return false unless is_variable
|
|
52
|
+
|
|
53
|
+
return axes_match?(style.variable_axes) if @spec.axes.any?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if an indexed font matches the format spec
|
|
60
|
+
def matches_indexed_font?(indexed_font)
|
|
61
|
+
return true unless @spec.has_constraints?
|
|
62
|
+
return false if @spec.format && indexed_font.format != @spec.format
|
|
63
|
+
|
|
64
|
+
if @spec.variable_requested?
|
|
65
|
+
return false unless indexed_font.variable_font
|
|
66
|
+
|
|
67
|
+
return axes_match?(indexed_font.variable_axes) if @spec.axes.any?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Filter resources to only matching ones
|
|
74
|
+
def filter_resources(resources)
|
|
75
|
+
resources.select { |r| matches_resource?(r) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Filter styles to only matching ones
|
|
79
|
+
def filter_styles(styles)
|
|
80
|
+
styles.select { |s| matches_style?(s) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Filter indexed fonts to only matching ones
|
|
84
|
+
def filter_indexed_fonts(fonts)
|
|
85
|
+
fonts.select { |f| matches_indexed_font?(f) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Select best resource based on preferences
|
|
89
|
+
def select_preferred_resource(resources)
|
|
90
|
+
return resources.first if resources.empty?
|
|
91
|
+
return resources.first unless @spec.has_constraints?
|
|
92
|
+
|
|
93
|
+
# Match exact format first (e.g. format: "ttf" selects the ttf resource)
|
|
94
|
+
exact = find_exact_format(resources)
|
|
95
|
+
return exact if exact
|
|
96
|
+
|
|
97
|
+
preferred = find_preferred_format(resources)
|
|
98
|
+
return preferred if preferred
|
|
99
|
+
|
|
100
|
+
variable = find_variable_resource(resources)
|
|
101
|
+
return variable if variable
|
|
102
|
+
|
|
103
|
+
resources.first
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if requested format needs transcoding from available formats
|
|
107
|
+
def installation_strategy(available_formats)
|
|
108
|
+
requested = @spec.format
|
|
109
|
+
|
|
110
|
+
if !requested
|
|
111
|
+
return { strategy: :install, format: available_formats.first }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if available_formats.include?(requested)
|
|
115
|
+
return { strategy: :install, format: requested }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check if Fontisan can convert from any available format
|
|
119
|
+
convertible = available_formats.find { |f| self.class.can_convert?(f, requested) }
|
|
120
|
+
if convertible
|
|
121
|
+
return {
|
|
122
|
+
strategy: :convert,
|
|
123
|
+
from: convertible,
|
|
124
|
+
to: requested,
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
strategy: :unavailable,
|
|
130
|
+
requested: requested,
|
|
131
|
+
available: available_formats,
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check if Fontisan can convert between formats (instance method for convenience)
|
|
136
|
+
def can_convert?(from_format, to_format)
|
|
137
|
+
self.class.can_convert?(from_format, to_format)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Class method for checking Fontisan conversion capability
|
|
141
|
+
def self.can_convert?(from_format, to_format)
|
|
142
|
+
return false unless from_format && to_format
|
|
143
|
+
|
|
144
|
+
# Fontisan supports conversion from desktop to web formats
|
|
145
|
+
DESKTOP_FORMATS.include?(from_format.to_s) &&
|
|
146
|
+
WEB_FORMATS.include?(to_format.to_s)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def find_exact_format(resources)
|
|
152
|
+
return nil unless @spec.format
|
|
153
|
+
|
|
154
|
+
resources.find { |r| r.format == @spec.format }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def find_preferred_format(resources)
|
|
158
|
+
return nil unless @spec.prefer_format
|
|
159
|
+
|
|
160
|
+
resources.find { |r| r.format == @spec.prefer_format }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def find_variable_resource(resources)
|
|
164
|
+
return nil unless @spec.prefer_variable
|
|
165
|
+
|
|
166
|
+
resources.find { |r| r.variable_font? }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def axes_match?(available_axes)
|
|
170
|
+
return true if @spec.axes.empty?
|
|
171
|
+
|
|
172
|
+
available = Array(available_axes).map(&:to_s)
|
|
173
|
+
@spec.axes.all? { |axis| available.include?(axis) }
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require "lutaml/model"
|
|
2
|
+
|
|
3
|
+
module Fontist
|
|
4
|
+
# Encapsulates format requirements for font installation/lookup
|
|
5
|
+
#
|
|
6
|
+
# Supports:
|
|
7
|
+
# - Format selection (install specific format)
|
|
8
|
+
# - Variable axes selection
|
|
9
|
+
# - Automatic transcoding (if format not available, transcode from available)
|
|
10
|
+
# - Transcode destination specification
|
|
11
|
+
#
|
|
12
|
+
# This model is passed through the entire pipeline:
|
|
13
|
+
# CLI -> Font -> FormulaPicker -> FontInstaller -> Index
|
|
14
|
+
class FormatSpec < Lutaml::Model::Serializable
|
|
15
|
+
# Format preference (what format to install)
|
|
16
|
+
# If format not available in formula, will transcode from available format
|
|
17
|
+
attribute :format, :string
|
|
18
|
+
|
|
19
|
+
# Variable font selection
|
|
20
|
+
attribute :variable_axes, :string, collection: true
|
|
21
|
+
attribute :prefer_variable, :boolean, default: false
|
|
22
|
+
|
|
23
|
+
# Format preferences
|
|
24
|
+
attribute :prefer_format, :string
|
|
25
|
+
|
|
26
|
+
# Transcoding options (used when format not available)
|
|
27
|
+
attribute :transcode_path, :string
|
|
28
|
+
attribute :keep_original, :boolean, default: true
|
|
29
|
+
|
|
30
|
+
# Collection handling
|
|
31
|
+
attribute :collection_index, :integer
|
|
32
|
+
|
|
33
|
+
# Convenience constructor for CLI
|
|
34
|
+
def self.from_options(options = {})
|
|
35
|
+
new(
|
|
36
|
+
format: options[:format],
|
|
37
|
+
variable_axes: parse_variable_axes(options[:variable_axes]),
|
|
38
|
+
prefer_variable: options[:prefer_variable] || false,
|
|
39
|
+
prefer_format: options[:prefer_format],
|
|
40
|
+
transcode_path: options[:transcode_path],
|
|
41
|
+
keep_original: options.fetch(:keep_original, true),
|
|
42
|
+
collection_index: options[:collection_index],
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.parse_variable_axes(value)
|
|
47
|
+
return nil if value.nil?
|
|
48
|
+
return value if value.is_a?(Array)
|
|
49
|
+
|
|
50
|
+
value.to_s.split(",").map(&:strip).compact
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if any format constraints are specified
|
|
54
|
+
def has_constraints?
|
|
55
|
+
!!(format || variable_axes&.any? || prefer_variable || prefer_format)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if variable font is requested
|
|
59
|
+
def variable_requested?
|
|
60
|
+
!!(variable_axes&.any? || prefer_variable)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get axes as array (never nil)
|
|
64
|
+
def axes
|
|
65
|
+
Array(variable_axes)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if transcoding might be needed (format specified but not available)
|
|
69
|
+
def needs_transcode?(available_formats)
|
|
70
|
+
return false unless format
|
|
71
|
+
|
|
72
|
+
!available_formats.include?(format)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if we need a specific collection index
|
|
76
|
+
def specific_collection_index?
|
|
77
|
+
!collection_index.nil?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|