fontist 3.0.28 → 3.0.30
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/README.adoc +148 -0
- data/fontist.gemspec +4 -3
- data/lib/fontist/cache/store.rb +13 -6
- data/lib/fontist/cli.rb +106 -6
- data/lib/fontist/css/google_family.rb +50 -0
- data/lib/fontist/css/local.rb +157 -0
- data/lib/fontist/css/styles.rb +79 -0
- data/lib/fontist/css.rb +110 -0
- data/lib/fontist/errors.rb +3 -0
- data/lib/fontist/font_file.rb +2 -1
- data/lib/fontist/font_finder.rb +15 -2
- data/lib/fontist/font_installer.rb +97 -94
- data/lib/fontist/font_operations/convert.rb +55 -0
- data/lib/fontist/font_operations/extract.rb +53 -0
- data/lib/fontist/font_operations/instance.rb +92 -0
- data/lib/fontist/font_operations/pack.rb +49 -0
- data/lib/fontist/font_operations/subset.rb +68 -0
- data/lib/fontist/font_operations.rb +25 -0
- data/lib/fontist/format_spec.rb +35 -0
- data/lib/fontist/formula.rb +33 -5
- data/lib/fontist/formula_picker.rb +27 -8
- data/lib/fontist/from_file_cache.rb +4 -0
- data/lib/fontist/helpers.rb +1 -1
- data/lib/fontist/import/convert_formulas.rb +1 -1
- data/lib/fontist/import/google/data_sources/base.rb +3 -3
- data/lib/fontist/import/google/data_sources/github.rb +28 -0
- data/lib/fontist/import/google/formula_builders/formula_builder_v5.rb +30 -57
- data/lib/fontist/import/google/models/font_family.rb +7 -4
- data/lib/fontist/import/google/variable_font_source.rb +105 -0
- data/lib/fontist/import/google.rb +1 -0
- data/lib/fontist/import/helpers/hash_helper.rb +1 -1
- data/lib/fontist/import/sil_importer.rb +18 -5
- data/lib/fontist/import/v4_to_v5_migrator.rb +1 -1
- data/lib/fontist/import/windows.rb +13 -2
- data/lib/fontist/import_cli.rb +19 -0
- data/lib/fontist/index_cli.rb +1 -1
- data/lib/fontist/indexes/index_mixin.rb +4 -4
- data/lib/fontist/json.rb +76 -0
- data/lib/fontist/manifest.rb +63 -0
- data/lib/fontist/resource.rb +6 -1
- data/lib/fontist/validate_cli.rb +1 -1
- data/lib/fontist/version.rb +1 -1
- data/lib/fontist/yaml_backend.rb +18 -5
- data/lib/fontist.rb +3 -0
- metadata +17 -19
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Fontist
|
|
4
|
+
module FontOperations
|
|
5
|
+
# Bundles several font files into one TTC/OTC (or dfont) collection.
|
|
6
|
+
module Pack
|
|
7
|
+
FORMATS = %w[ttc otc dfont].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# @param output [String] collection path; extension picks the format
|
|
12
|
+
# unless +format+ is given
|
|
13
|
+
# @param files [Array<String>] two or more font files
|
|
14
|
+
# @return [String] the written collection path
|
|
15
|
+
def call(output, files, format: nil)
|
|
16
|
+
FontOperations.guard("Packing collection") do
|
|
17
|
+
format = (format || File.extname(output).delete(".")).to_s.downcase
|
|
18
|
+
validate!(files, format)
|
|
19
|
+
write(output, files, format)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate!(files, format)
|
|
24
|
+
unless FORMATS.include?(format)
|
|
25
|
+
raise Error, "Unsupported collection format '#{format}'. " \
|
|
26
|
+
"Use one of: #{FORMATS.join(', ')}"
|
|
27
|
+
end
|
|
28
|
+
if files.size < 2
|
|
29
|
+
raise Error, "A collection needs at least two font files"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
missing = files.reject { |f| File.exist?(f) }
|
|
33
|
+
raise Errors::SourceNotFoundError, missing.first if missing.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def write(output, files, format)
|
|
37
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
38
|
+
Fontisan::Commands::PackCommand.new(
|
|
39
|
+
files, output: output, format: format
|
|
40
|
+
).run
|
|
41
|
+
unless File.size?(output)
|
|
42
|
+
raise Error, "No collection was written to #{output}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
output
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Fontist
|
|
4
|
+
module FontOperations
|
|
5
|
+
# Keeps only the glyphs for the given characters.
|
|
6
|
+
module Subset
|
|
7
|
+
UNICODE_PART = /\A(u\+)?\h+(-(u\+)?\h+)?\z/i
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# @param source [String] font file
|
|
12
|
+
# @param output [String] where to write the subset font
|
|
13
|
+
# @param unicodes [String, Array<Integer>, nil] "U+0020-007E,U+00E9"
|
|
14
|
+
# or codepoints
|
|
15
|
+
# @param text [String, nil] characters to keep
|
|
16
|
+
# @return [String] output path
|
|
17
|
+
def call(source, output, unicodes: nil, text: nil)
|
|
18
|
+
FontOperations.guard("Subsetting") do
|
|
19
|
+
raise Errors::SourceNotFoundError, source unless File.exist?(source)
|
|
20
|
+
|
|
21
|
+
codepoints = parse_unicodes(unicodes) + text.to_s.codepoints
|
|
22
|
+
if codepoints.empty?
|
|
23
|
+
raise Error, "No characters to keep: give unicodes or text"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
write(source, output, codepoints)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write(source, output, codepoints)
|
|
31
|
+
font = Fontisan::FontLoader.load(source)
|
|
32
|
+
gids = glyph_ids(font, codepoints, File.basename(source))
|
|
33
|
+
options = Fontisan::Subset::Options.new(profile: "web")
|
|
34
|
+
binary = Fontisan::Subset::Builder.new(font, gids, options).build
|
|
35
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
36
|
+
File.binwrite(output, binary)
|
|
37
|
+
output
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def glyph_ids(font, codepoints, name)
|
|
41
|
+
mappings = font.table("cmap").unicode_mappings
|
|
42
|
+
gids = codepoints.uniq.filter_map { |cp| mappings[cp] }.uniq.sort
|
|
43
|
+
return gids if gids.any?
|
|
44
|
+
|
|
45
|
+
raise Error, "None of the requested characters exist in #{name}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# "U+0020-007E,U+00E9" => [32..126, 233].flatten
|
|
49
|
+
def parse_unicodes(value)
|
|
50
|
+
return Array(value).map(&:to_i) unless value.nil? || value.is_a?(String)
|
|
51
|
+
|
|
52
|
+
value.to_s.split(/[,\s]+/).reject(&:empty?).flat_map do |part|
|
|
53
|
+
parse_part(part)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_part(part)
|
|
58
|
+
unless part.match?(UNICODE_PART)
|
|
59
|
+
raise Error, "Invalid unicode '#{part}'. Use e.g. U+0020-007E,U+00E9"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
first, last = part.split("-", 2)
|
|
63
|
+
.map { |h| h.sub(/\Au\+/i, "").to_i(16) }
|
|
64
|
+
last ? (first..last).to_a : [first]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Fontist
|
|
2
|
+
# Font file operations backed by the Fontisan gem: collection member
|
|
3
|
+
# extraction, collection bundling, format conversion, subsetting and
|
|
4
|
+
# variable-font instancing.
|
|
5
|
+
module FontOperations
|
|
6
|
+
class Error < Errors::GeneralError; end
|
|
7
|
+
|
|
8
|
+
autoload :Convert, "#{__dir__}/font_operations/convert"
|
|
9
|
+
autoload :Extract, "#{__dir__}/font_operations/extract"
|
|
10
|
+
autoload :Instance, "#{__dir__}/font_operations/instance"
|
|
11
|
+
autoload :Pack, "#{__dir__}/font_operations/pack"
|
|
12
|
+
autoload :Subset, "#{__dir__}/font_operations/subset"
|
|
13
|
+
|
|
14
|
+
# Runs the block, turning any Fontisan failure into FontOperations::Error
|
|
15
|
+
# that names the operation.
|
|
16
|
+
def self.guard(operation)
|
|
17
|
+
require "fontisan"
|
|
18
|
+
yield
|
|
19
|
+
rescue Error, Errors::GeneralError
|
|
20
|
+
raise
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
raise Error, "#{operation} failed: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/fontist/format_spec.rb
CHANGED
|
@@ -30,6 +30,16 @@ module Fontist
|
|
|
30
30
|
# Collection handling
|
|
31
31
|
attribute :collection_index, :integer
|
|
32
32
|
|
|
33
|
+
# Subsetting: keep only these characters (U+0020-007E,U+00E9 / literal text)
|
|
34
|
+
attribute :subset_unicodes, :string
|
|
35
|
+
attribute :subset_text, :string
|
|
36
|
+
|
|
37
|
+
# Static instance of a variable font: "wght=300,wdth=90"
|
|
38
|
+
attribute :instance, :string
|
|
39
|
+
|
|
40
|
+
# Explicit opt-in: install the original format when conversion fails
|
|
41
|
+
attribute :fallback_to_original, :boolean, default: false
|
|
42
|
+
|
|
33
43
|
# Convenience constructor for CLI
|
|
34
44
|
def self.from_options(options = {})
|
|
35
45
|
new(
|
|
@@ -40,9 +50,19 @@ module Fontist
|
|
|
40
50
|
transcode_path: options[:transcode_path],
|
|
41
51
|
keep_original: options.fetch(:keep_original, true),
|
|
42
52
|
collection_index: options[:collection_index],
|
|
53
|
+
**operation_options(options),
|
|
43
54
|
)
|
|
44
55
|
end
|
|
45
56
|
|
|
57
|
+
def self.operation_options(options)
|
|
58
|
+
{
|
|
59
|
+
subset_unicodes: options[:subset_unicodes],
|
|
60
|
+
subset_text: options[:subset_text],
|
|
61
|
+
instance: options[:instance],
|
|
62
|
+
fallback_to_original: options[:fallback_to_original] || false,
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
46
66
|
def self.parse_variable_axes(value)
|
|
47
67
|
return nil if value.nil?
|
|
48
68
|
return value if value.is_a?(Array)
|
|
@@ -72,6 +92,21 @@ module Fontist
|
|
|
72
92
|
!available_formats.include?(format)
|
|
73
93
|
end
|
|
74
94
|
|
|
95
|
+
# Check if a subset of characters was requested
|
|
96
|
+
def subset_requested?
|
|
97
|
+
!(subset_unicodes.to_s.strip.empty? && subset_text.to_s.empty?)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if a static instance of a variable font was requested
|
|
101
|
+
def instance_requested?
|
|
102
|
+
!instance.to_s.strip.empty?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Axis coordinates of the requested instance: {"wght" => 300.0}
|
|
106
|
+
def instance_coordinates
|
|
107
|
+
FontOperations::Instance.parse(instance)
|
|
108
|
+
end
|
|
109
|
+
|
|
75
110
|
# Check if we need a specific collection index
|
|
76
111
|
def specific_collection_index?
|
|
77
112
|
!collection_index.nil?
|
data/lib/fontist/formula.rb
CHANGED
|
@@ -106,10 +106,16 @@ module Fontist
|
|
|
106
106
|
# Parsing thousands of formula files dominates index rebuilds;
|
|
107
107
|
# worker threads overlap the libyaml C parse, which releases
|
|
108
108
|
# the GVL. The pool is capped like every other fontist pool, so
|
|
109
|
-
# worker counts never scale with host core counts
|
|
109
|
+
# worker counts never scale with host core counts.
|
|
110
|
+
#
|
|
111
|
+
# The parses bypass the shared parse cache: a corpus sweep never
|
|
112
|
+
# hits it (the LRU holds 64 entries against thousands of files),
|
|
113
|
+
# so caching would only evict entries that lookups re-use and
|
|
114
|
+
# pin dozens of parsed formulas in long-lived host processes
|
|
115
|
+
# after the sweep ends
|
|
110
116
|
workers = [Parallel.processor_count, 8].min
|
|
111
117
|
formulas = Parallel.map(paths, in_threads: workers) do |path|
|
|
112
|
-
Formula.from_file(path)
|
|
118
|
+
Formula.from_file(path, cache: false)
|
|
113
119
|
end.compact
|
|
114
120
|
|
|
115
121
|
FormulaCollection.new(formulas)
|
|
@@ -181,7 +187,7 @@ module Fontist
|
|
|
181
187
|
find_by_key(key)
|
|
182
188
|
end
|
|
183
189
|
|
|
184
|
-
def from_file(path)
|
|
190
|
+
def from_file(path, cache: true)
|
|
185
191
|
unless File.exist?(path)
|
|
186
192
|
raise Fontist::Errors::FormulaNotFoundError,
|
|
187
193
|
"Formula file not found: #{path}"
|
|
@@ -192,8 +198,13 @@ module Fontist
|
|
|
192
198
|
# Parsing a formula costs ~10-20ms and the same formula is
|
|
193
199
|
# re-read on every lookup, so the parsed instance is memoized
|
|
194
200
|
# (mtime-validated) and shared between callers; treat as
|
|
195
|
-
# read-only
|
|
196
|
-
|
|
201
|
+
# read-only. Callers that sweep the whole corpus pass
|
|
202
|
+
# cache: false, since their parses would only thrash the cache
|
|
203
|
+
formula = if cache
|
|
204
|
+
Fontist::FromFileCache.memoize(path) { from_yaml(content) }
|
|
205
|
+
else
|
|
206
|
+
from_yaml(content)
|
|
207
|
+
end
|
|
197
208
|
|
|
198
209
|
formula.tap do |f|
|
|
199
210
|
f.path = path
|
|
@@ -218,6 +229,23 @@ module Fontist
|
|
|
218
229
|
schema_version == 5
|
|
219
230
|
end
|
|
220
231
|
|
|
232
|
+
# Formats of the style files, as recorded or read from file extensions.
|
|
233
|
+
# Archive resources often carry no format of their own.
|
|
234
|
+
def style_formats
|
|
235
|
+
fonts.flat_map(&:styles).flat_map do |style|
|
|
236
|
+
extension = File.extname(style.font.to_s).delete(".").downcase
|
|
237
|
+
Array(style.formats) + [extension]
|
|
238
|
+
end.reject(&:empty?).uniq
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Whether a resource offers a format, falling back to the style files
|
|
242
|
+
# when the resource records none.
|
|
243
|
+
def resource_offers_format?(resource, format)
|
|
244
|
+
return resource.format == format if resource.format
|
|
245
|
+
|
|
246
|
+
style_formats.include?(format)
|
|
247
|
+
end
|
|
248
|
+
|
|
221
249
|
# Get the effective schema version (default to 4 if not set)
|
|
222
250
|
def effective_schema_version
|
|
223
251
|
schema_version || 4
|
|
@@ -21,10 +21,9 @@ module Fontist
|
|
|
21
21
|
list = filter(formulas)
|
|
22
22
|
return [] if list.empty?
|
|
23
23
|
|
|
24
|
-
# Use FormatMatcher for format filtering
|
|
25
24
|
if @format_spec&.has_constraints?
|
|
26
25
|
filtered = filter_by_format_spec(list)
|
|
27
|
-
raise_format_not_available_error(list) if filtered.empty?
|
|
26
|
+
raise_format_not_available_error(list) if filtered.empty?
|
|
28
27
|
|
|
29
28
|
list = filtered
|
|
30
29
|
end
|
|
@@ -45,17 +44,37 @@ module Fontist
|
|
|
45
44
|
ensure_fontist_version(list)
|
|
46
45
|
end
|
|
47
46
|
|
|
47
|
+
# Formulas offering the requested format come first. When none does, a
|
|
48
|
+
# plain format request falls back to every formula so the installer can
|
|
49
|
+
# convert; a variable-font request cannot be met by conversion.
|
|
48
50
|
def filter_by_format_spec(formulas)
|
|
49
51
|
matcher = FormatMatcher.new(@format_spec)
|
|
52
|
+
offering = formulas.filter_map do |formula|
|
|
53
|
+
narrow_to_offered_format(formula, matcher)
|
|
54
|
+
end
|
|
55
|
+
return offering if offering.any?
|
|
56
|
+
return formulas if convertible_request?
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
[]
|
|
59
|
+
end
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
def narrow_to_offered_format(formula, matcher)
|
|
62
|
+
return formula unless formula.v5?
|
|
63
|
+
|
|
64
|
+
matching = formula.resources.select do |resource|
|
|
65
|
+
offers_format?(formula, resource) && matcher.matches_resource?(resource)
|
|
58
66
|
end
|
|
67
|
+
formula.dup.tap { |f| f.resources = matching } if matching.any?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def offers_format?(formula, resource)
|
|
71
|
+
return true unless @format_spec.format
|
|
72
|
+
|
|
73
|
+
formula.resource_offers_format?(resource, @format_spec.format)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def convertible_request?
|
|
77
|
+
!@format_spec.format.nil? && !@format_spec.variable_requested?
|
|
59
78
|
end
|
|
60
79
|
|
|
61
80
|
def ensure_fontist_version(formulas)
|
data/lib/fontist/helpers.rb
CHANGED
|
@@ -58,8 +58,8 @@ module Fontist
|
|
|
58
58
|
raise "API request failed: #{response.code} #{response.message}"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
JSON.parse(response.body)
|
|
62
|
-
rescue JSON::
|
|
61
|
+
Fontist::JSON.parse(response.body)
|
|
62
|
+
rescue Fontist::JSON::ParseError => e
|
|
63
63
|
raise "Failed to parse API response: #{e.message}"
|
|
64
64
|
rescue StandardError => e
|
|
65
65
|
raise "Failed to fetch from API: #{e.message}"
|
|
@@ -90,7 +90,7 @@ module Fontist
|
|
|
90
90
|
# @param item [Hash] the raw item data
|
|
91
91
|
# @return [FontFamily] the parsed font family
|
|
92
92
|
def parse_item(item)
|
|
93
|
-
Models::FontFamily.from_json(item
|
|
93
|
+
Models::FontFamily.from_json(Fontist::JSON.dump(item))
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
end
|
|
@@ -51,6 +51,23 @@ module Fontist
|
|
|
51
51
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
|
+
# HEAD commit of the google/fonts clone, used to pin raw file URLs
|
|
55
|
+
def repository_commit
|
|
56
|
+
return @repository_commit if defined?(@repository_commit)
|
|
57
|
+
|
|
58
|
+
@repository_commit = git_head_commit
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# No shell involved, so this also works on Windows, where
|
|
62
|
+
# `2>/dev/null` is not a valid redirect.
|
|
63
|
+
def git_head_commit
|
|
64
|
+
sha = IO.popen(%w[git rev-parse HEAD], chdir: @source_path.to_s,
|
|
65
|
+
err: File::NULL, &:read)
|
|
66
|
+
sha.strip[/\A\h{40}\z/]
|
|
67
|
+
rescue SystemCallError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
54
71
|
# Validate that the source path exists and contains fonts
|
|
55
72
|
def validate_source_path!
|
|
56
73
|
unless @source_path.exist?
|
|
@@ -155,6 +172,17 @@ module Fontist
|
|
|
155
172
|
description: description,
|
|
156
173
|
homepage: license_info[:homepage],
|
|
157
174
|
font_file_data: font_files_data,
|
|
175
|
+
).tap do |family|
|
|
176
|
+
family.variable_source = variable_source_for(metadata, family_dir)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Real variable font files of the family, pinned to the clone's commit
|
|
181
|
+
def variable_source_for(metadata, family_dir)
|
|
182
|
+
VariableFontSource.from_metadata(
|
|
183
|
+
metadata,
|
|
184
|
+
directory: "#{family_dir.parent.basename}/#{family_dir.basename}",
|
|
185
|
+
commit: repository_commit,
|
|
158
186
|
)
|
|
159
187
|
end
|
|
160
188
|
|
|
@@ -50,48 +50,32 @@ module Fontist
|
|
|
50
50
|
def build_resources
|
|
51
51
|
resources = {}
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
resources["woff2_variable"] = build_resource_entry(
|
|
61
|
-
woff2_files[family.family],
|
|
62
|
-
format: "woff2",
|
|
63
|
-
variable: true,
|
|
64
|
-
axes: family.axes,
|
|
65
|
-
)
|
|
66
|
-
end
|
|
53
|
+
# The only genuine variable files are the ones in google/fonts.
|
|
54
|
+
# The API serves static per-weight instances for every family,
|
|
55
|
+
# including variable ones, so those always go to *_static.
|
|
56
|
+
if variable_source
|
|
57
|
+
resources["ttf_variable"] =
|
|
58
|
+
variable_source.resource(family.family)
|
|
59
|
+
end
|
|
67
60
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
resources["ttf_variable"] = build_resource_entry(
|
|
71
|
-
ttf_files[family.family],
|
|
72
|
-
format: "ttf",
|
|
73
|
-
variable: true,
|
|
74
|
-
axes: family.axes,
|
|
75
|
-
)
|
|
76
|
-
end
|
|
77
|
-
else
|
|
78
|
-
# Static fonts: separate TTF and WOFF2
|
|
79
|
-
static_ttf = filter_static_files(ttf_files[family.family])
|
|
80
|
-
if static_ttf.any?
|
|
81
|
-
resources["ttf_static"] = build_resource_entry(
|
|
82
|
-
static_ttf, format: "ttf", variable: false
|
|
83
|
-
)
|
|
84
|
-
end
|
|
61
|
+
resources.merge(static_resources)
|
|
62
|
+
end
|
|
85
63
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
64
|
+
def static_resources
|
|
65
|
+
static_resource("ttf_static", "ttf", ttf_files)
|
|
66
|
+
.merge(static_resource("woff2_static", "woff2", woff2_files))
|
|
67
|
+
end
|
|
93
68
|
|
|
94
|
-
|
|
69
|
+
def static_resource(key, format, files_by_family)
|
|
70
|
+
files = filter_static_files(files_by_family[family.family])
|
|
71
|
+
return {} if files.empty?
|
|
72
|
+
|
|
73
|
+
{ key => build_resource_entry(files, format: format,
|
|
74
|
+
variable: false) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def variable_source
|
|
78
|
+
github_family&.variable_source
|
|
95
79
|
end
|
|
96
80
|
|
|
97
81
|
def build_resource_entry(files, format:, variable:, axes: nil)
|
|
@@ -132,17 +116,13 @@ module Fontist
|
|
|
132
116
|
|
|
133
117
|
def is_variable_variant?(variant)
|
|
134
118
|
# Variable fonts often have axes in the filename like "VariableFont_wght"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return false unless family.variable_font?
|
|
138
|
-
|
|
139
|
-
family.axes.any? { |axis| variant.include?(axis.tag) }
|
|
119
|
+
variant.include?("Variable")
|
|
140
120
|
end
|
|
141
121
|
|
|
142
122
|
def build_fonts
|
|
143
123
|
parsed_fonts = []
|
|
144
124
|
|
|
145
|
-
# V5: Download and parse
|
|
125
|
+
# V5: Download and parse the API's static TTF files
|
|
146
126
|
ttf_files[family.family]&.each do |variant, url|
|
|
147
127
|
sleep(0.05) # Throttle API requests
|
|
148
128
|
|
|
@@ -150,7 +130,6 @@ module Fontist
|
|
|
150
130
|
downloaded = Fontist::Utils::Downloader.download(url)
|
|
151
131
|
metadata = Fontist::Import::FontMetadataExtractor.new(downloaded.path).extract
|
|
152
132
|
|
|
153
|
-
# V5: Include both static and variable fonts
|
|
154
133
|
filename = url.split("/").last
|
|
155
134
|
|
|
156
135
|
style_data = build_style_data(metadata, filename)
|
|
@@ -164,6 +143,7 @@ module Fontist
|
|
|
164
143
|
end
|
|
165
144
|
end
|
|
166
145
|
|
|
146
|
+
parsed_fonts.concat(variable_source.styles) if variable_source
|
|
167
147
|
return [] if parsed_fonts.empty?
|
|
168
148
|
|
|
169
149
|
group_fonts_by_subfamily(parsed_fonts)
|
|
@@ -173,17 +153,10 @@ module Fontist
|
|
|
173
153
|
# Add formats available for this style
|
|
174
154
|
style["formats"] = determine_formats_for_style(variant)
|
|
175
155
|
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if family.axes&.any?
|
|
181
|
-
style["variable_axes"] =
|
|
182
|
-
family.axes.map(&:tag)
|
|
183
|
-
end
|
|
184
|
-
else
|
|
185
|
-
style["variable_font"] = false
|
|
186
|
-
end
|
|
156
|
+
# Styles built from API files are static instances; the real
|
|
157
|
+
# variable styles come from variable_source.
|
|
158
|
+
style["variable_font"] = false
|
|
159
|
+
style["source_resource"] = "ttf_static" if variable_source
|
|
187
160
|
end
|
|
188
161
|
|
|
189
162
|
def determine_formats_for_style(variant)
|
|
@@ -24,6 +24,9 @@ module Fontist
|
|
|
24
24
|
attribute :homepage, :string
|
|
25
25
|
attribute :font_file_data, :string
|
|
26
26
|
|
|
27
|
+
# Real variable font files from google/fonts (VariableFontSource)
|
|
28
|
+
attr_accessor :variable_source
|
|
29
|
+
|
|
27
30
|
json do
|
|
28
31
|
map "family", to: :family
|
|
29
32
|
map "variants", to: :variants
|
|
@@ -56,15 +59,15 @@ module Fontist
|
|
|
56
59
|
|
|
57
60
|
# Try to parse as JSON first
|
|
58
61
|
begin
|
|
59
|
-
JSON.parse(files_data)
|
|
60
|
-
rescue JSON::
|
|
62
|
+
Fontist::JSON.parse(files_data)
|
|
63
|
+
rescue Fontist::JSON::ParseError, TypeError
|
|
61
64
|
# If it fails, convert Ruby hash syntax to JSON and parse
|
|
62
65
|
if files_data.is_a?(String)
|
|
63
66
|
# Convert Ruby hash syntax (=>) to JSON syntax (:)
|
|
64
67
|
json_str = files_data.gsub("=>", ":")
|
|
65
68
|
begin
|
|
66
|
-
return JSON.parse(json_str)
|
|
67
|
-
rescue JSON::
|
|
69
|
+
return Fontist::JSON.parse(json_str)
|
|
70
|
+
rescue Fontist::JSON::ParseError
|
|
68
71
|
{}
|
|
69
72
|
end
|
|
70
73
|
end
|