fontisan 0.4.32 → 0.4.34
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 +22 -213
- data/TODO.improvements/README.md +6 -6
- data/lib/fontisan/cli.rb +65 -0
- data/lib/fontisan/commands/audit_command.rb +257 -0
- data/lib/fontisan/commands.rb +1 -0
- data/lib/fontisan/models/audit_report.rb +143 -0
- data/lib/fontisan/models.rb +2 -0
- data/lib/fontisan/subset/table_strategy/cff2.rb +392 -0
- data/lib/fontisan/subset/table_strategy.rb +2 -0
- data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +5 -2
- data/lib/fontisan/tables/cff2/fd_select.rb +74 -1
- data/lib/fontisan/tables/cff2/index.rb +155 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +25 -26
- data/lib/fontisan/tables/cff2.rb +1 -0
- data/lib/fontisan/type1/charstrings.rb +34 -1
- data/lib/fontisan/type1/seac_expander.rb +68 -57
- data/lib/fontisan/ufo/compile/cff2.rb +161 -19
- data/lib/fontisan/ufo/compile/feature_compiler.rb +161 -0
- data/lib/fontisan/ufo/compile/gsub.rb +267 -0
- data/lib/fontisan/ufo/compile/variable_otf.rb +16 -7
- data/lib/fontisan/ufo/compile.rb +3 -1
- data/lib/fontisan/ufo/font.rb +3 -2
- data/lib/fontisan/ufo/image.rb +15 -2
- data/lib/fontisan/ufo/image_set.rb +82 -2
- data/lib/fontisan/ufo/reader.rb +8 -0
- data/lib/fontisan/version.rb +1 -1
- metadata +7 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Commands
|
|
8
|
+
# Produces a structured {Models::AuditReport} for a single font or
|
|
9
|
+
# an array of reports for every face in a collection.
|
|
10
|
+
#
|
|
11
|
+
# The audit report is MECE with ucode: fontisan owns the font-identity
|
|
12
|
+
# axis (name table, style metadata, OpenType layout); ucode owns the
|
|
13
|
+
# Unicode-coverage axis (UCD parsing, block/script aggregation). The
|
|
14
|
+
# audit command emits the raw codepoint list so a consumer can run
|
|
15
|
+
# ucode separately without re-reading the font.
|
|
16
|
+
#
|
|
17
|
+
# Delegates data extraction to existing sub-commands and table readers
|
|
18
|
+
# — no table re-parsing happens here. This keeps the audit command a
|
|
19
|
+
# thin orchestration layer (DRY, single source of truth).
|
|
20
|
+
#
|
|
21
|
+
# @example Single font
|
|
22
|
+
# cmd = AuditCommand.new("Inter.ttf", include_codepoints: true)
|
|
23
|
+
# cmd.run # => Models::AuditReport
|
|
24
|
+
#
|
|
25
|
+
# @example Collection
|
|
26
|
+
# cmd = AuditCommand.new("Inter.ttc")
|
|
27
|
+
# cmd.run # => Array<Models::AuditReport>
|
|
28
|
+
class AuditCommand < BaseCommand
|
|
29
|
+
# @return [Models::AuditReport, Array<Models::AuditReport>]
|
|
30
|
+
def run
|
|
31
|
+
if FontLoader.collection?(@font_path)
|
|
32
|
+
audit_collection
|
|
33
|
+
else
|
|
34
|
+
audit_single_font(font_index: nil, num_fonts: 1)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
# Collection dispatch
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def audit_collection
|
|
45
|
+
collection = FontLoader.load_collection(@font_path)
|
|
46
|
+
num = collection.num_fonts
|
|
47
|
+
Array.new(num) do |idx|
|
|
48
|
+
face = load_face(idx)
|
|
49
|
+
build_report(face, font_index: idx, num_fonts: num)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def audit_single_font(font_index:, num_fonts:)
|
|
54
|
+
build_report(font, font_index: font_index, num_fonts: num_fonts)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Reload a single face from a collection by index, with full tables
|
|
58
|
+
# so the audit is complete.
|
|
59
|
+
def load_face(idx)
|
|
60
|
+
FontLoader.load(@font_path, font_index: idx, mode: LoadingModes::FULL)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# Report assembly
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
# @param face [SfntFont] loaded font
|
|
68
|
+
# @param font_index [Integer, nil]
|
|
69
|
+
# @param num_fonts [Integer]
|
|
70
|
+
# @return [Models::AuditReport]
|
|
71
|
+
def build_report(face, font_index:, num_fonts:)
|
|
72
|
+
Models::AuditReport.new.tap do |r|
|
|
73
|
+
populate_provenance(r, font_index:, num_fonts:)
|
|
74
|
+
populate_identity(r, face)
|
|
75
|
+
populate_style(r, face)
|
|
76
|
+
populate_coverage(r, face)
|
|
77
|
+
populate_layout(r, face)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# Provenance
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def populate_provenance(report, font_index:, num_fonts:)
|
|
86
|
+
report.generated_at = Time.now.utc.iso8601
|
|
87
|
+
report.fontisan_version = VERSION
|
|
88
|
+
report.source_file = File.basename(@font_path)
|
|
89
|
+
report.source_sha256 = Digest::SHA256.file(@font_path).hexdigest
|
|
90
|
+
report.source_format = detect_source_format
|
|
91
|
+
report.font_index = font_index
|
|
92
|
+
report.num_fonts_in_source = num_fonts
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def detect_source_format
|
|
96
|
+
fmt = FontLoader.detect_format(@font_path)
|
|
97
|
+
fmt ? fmt.to_s : "unknown"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# Identity (name table)
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def populate_identity(report, face)
|
|
105
|
+
return unless face.has_table?(Constants::NAME_TAG)
|
|
106
|
+
|
|
107
|
+
name_table = face.table(Constants::NAME_TAG)
|
|
108
|
+
report.family_name = name_table.english_name(Tables::Name::FAMILY)
|
|
109
|
+
report.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
|
|
110
|
+
report.full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
111
|
+
report.postscript_name =
|
|
112
|
+
name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
113
|
+
report.version = name_table.english_name(Tables::Name::VERSION)
|
|
114
|
+
|
|
115
|
+
return unless face.has_table?(Constants::HEAD_TAG)
|
|
116
|
+
|
|
117
|
+
report.font_revision = face.table(Constants::HEAD_TAG).font_revision
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Style (OS/2, head, fvar)
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def populate_style(report, face)
|
|
125
|
+
report.is_variable = false
|
|
126
|
+
report.italic = false
|
|
127
|
+
report.bold = false
|
|
128
|
+
populate_os2_style(report, face) if face.has_table?(Constants::OS2_TAG)
|
|
129
|
+
populate_head_style(report, face) if face.has_table?(Constants::HEAD_TAG)
|
|
130
|
+
populate_fvar_style(report, face) if face.has_table?(Constants::FVAR_TAG)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def populate_os2_style(report, face)
|
|
134
|
+
os2 = face.table(Constants::OS2_TAG)
|
|
135
|
+
report.weight_class = os2.us_weight_class
|
|
136
|
+
report.width_class = os2.us_width_class
|
|
137
|
+
report.italic = os2.fs_selection && (os2.fs_selection & 0x01).positive?
|
|
138
|
+
report.bold = os2.fs_selection && (os2.fs_selection & 0x20).positive?
|
|
139
|
+
report.panose = format_panose(os2.panose)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def populate_head_style(report, face)
|
|
143
|
+
head = face.table(Constants::HEAD_TAG)
|
|
144
|
+
mac_style = head.mac_style || 0
|
|
145
|
+
# head macStyle overrides OS/2 fsSelection only when OS/2 didn't set it
|
|
146
|
+
report.italic = true if (mac_style & 0x02).positive?
|
|
147
|
+
report.bold = true if (mac_style & 0x01).positive?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def populate_fvar_style(report, face)
|
|
151
|
+
report.is_variable = true
|
|
152
|
+
fvar = face.table(Constants::FVAR_TAG)
|
|
153
|
+
report.axes = fvar.axes.map do |axis|
|
|
154
|
+
Models::AuditAxis.new(
|
|
155
|
+
tag: axis.axis_tag,
|
|
156
|
+
min_value: axis.min_value,
|
|
157
|
+
default_value: axis.default_value,
|
|
158
|
+
max_value: axis.max_value,
|
|
159
|
+
name_id: axis.axis_name_id,
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def format_panose(panose)
|
|
165
|
+
return nil unless panose
|
|
166
|
+
|
|
167
|
+
panose.to_a.join(" ")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
# Coverage (cmap + glyph count)
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def populate_coverage(report, face)
|
|
175
|
+
if face.has_table?(Constants::CMAP_TAG)
|
|
176
|
+
cmap = face.table(Constants::CMAP_TAG)
|
|
177
|
+
mappings = cmap.unicode_mappings || {}
|
|
178
|
+
report.total_codepoints = mappings.size
|
|
179
|
+
report.cmap_subtables = cmap.subtable_formats
|
|
180
|
+
report.codepoints = codepoint_list(mappings.keys) if include_codepoints?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
report.total_glyphs = total_glyphs(face)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def codepoint_list(codepoints)
|
|
187
|
+
codepoints.sort.map { |cp| format_codepoint(cp) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def format_codepoint(cp)
|
|
191
|
+
format("U+%04X", cp)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def total_glyphs(face)
|
|
195
|
+
return 0 unless face.has_table?(Constants::MAXP_TAG)
|
|
196
|
+
|
|
197
|
+
face.table(Constants::MAXP_TAG)&.num_glyphs || 0
|
|
198
|
+
rescue StandardError
|
|
199
|
+
0
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def include_codepoints?
|
|
203
|
+
@options.fetch(:include_codepoints, true)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# ------------------------------------------------------------------
|
|
207
|
+
# OpenType layout (GSUB/GPOS)
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
def populate_layout(report, face)
|
|
211
|
+
report.opentype_scripts = collect_scripts(face)
|
|
212
|
+
report.features = collect_features(face)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def collect_scripts(face)
|
|
216
|
+
scripts = Set.new
|
|
217
|
+
%w[GSUB GPOS].each do |tag|
|
|
218
|
+
next unless face.has_table?(tag)
|
|
219
|
+
|
|
220
|
+
table = face.table(tag)
|
|
221
|
+
scripts.merge(table.scripts) if table
|
|
222
|
+
end
|
|
223
|
+
scripts.sort
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def collect_features(face)
|
|
227
|
+
features = Set.new
|
|
228
|
+
%w[GSUB GPOS].each do |tag|
|
|
229
|
+
next unless face.has_table?(tag)
|
|
230
|
+
|
|
231
|
+
table = face.table(tag)
|
|
232
|
+
features.merge(collect_features_from_table(table)) if table
|
|
233
|
+
end
|
|
234
|
+
features.sort
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def collect_features_from_table(layout_table)
|
|
238
|
+
scripts = begin
|
|
239
|
+
layout_table.scripts
|
|
240
|
+
rescue StandardError
|
|
241
|
+
[]
|
|
242
|
+
end
|
|
243
|
+
scripts.flat_map { |script| features_for_script(layout_table, script) }
|
|
244
|
+
rescue StandardError
|
|
245
|
+
[]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def features_for_script(layout_table, script)
|
|
249
|
+
|
|
250
|
+
layout_table.features(script_tag: script)
|
|
251
|
+
rescue StandardError
|
|
252
|
+
[]
|
|
253
|
+
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
data/lib/fontisan/commands.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
module Fontisan
|
|
6
6
|
module Commands
|
|
7
7
|
autoload :BaseCommand, "fontisan/commands/base_command"
|
|
8
|
+
autoload :AuditCommand, "fontisan/commands/audit_command"
|
|
8
9
|
autoload :ConvertCommand, "fontisan/commands/convert_command"
|
|
9
10
|
autoload :DumpTableCommand, "fontisan/commands/dump_table_command"
|
|
10
11
|
autoload :ExportCommand, "fontisan/commands/export_command"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Models
|
|
7
|
+
# Variable-font axis descriptor for the audit report.
|
|
8
|
+
class AuditAxis < Lutaml::Model::Serializable
|
|
9
|
+
attribute :tag, :string
|
|
10
|
+
attribute :min_value, :float
|
|
11
|
+
attribute :default_value, :float
|
|
12
|
+
attribute :max_value, :float
|
|
13
|
+
attribute :name_id, :integer
|
|
14
|
+
|
|
15
|
+
json do
|
|
16
|
+
map "tag", to: :tag
|
|
17
|
+
map "min_value", to: :min_value
|
|
18
|
+
map "default_value", to: :default_value
|
|
19
|
+
map "max_value", to: :max_value
|
|
20
|
+
map "name_id", to: :name_id
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
yaml do
|
|
24
|
+
map "tag", to: :tag
|
|
25
|
+
map "min_value", to: :min_value
|
|
26
|
+
map "default_value", to: :default_value
|
|
27
|
+
map "max_value", to: :max_value
|
|
28
|
+
map "name_id", to: :name_id
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Structured font audit report covering identity, style, coverage,
|
|
33
|
+
# and OpenType layout facts. Designed for archival use — each report
|
|
34
|
+
# is self-describing with provenance (generated_at, fontisan_version,
|
|
35
|
+
# source_sha256).
|
|
36
|
+
#
|
|
37
|
+
# The report is a value object: it captures what the font FILE
|
|
38
|
+
# declares, not what any external index says about it.
|
|
39
|
+
class AuditReport < Lutaml::Model::Serializable
|
|
40
|
+
# Provenance
|
|
41
|
+
attribute :generated_at, :string
|
|
42
|
+
attribute :fontisan_version, :string
|
|
43
|
+
attribute :source_file, :string
|
|
44
|
+
attribute :source_sha256, :string
|
|
45
|
+
attribute :source_format, :string
|
|
46
|
+
attribute :font_index, :integer
|
|
47
|
+
attribute :num_fonts_in_source, :integer
|
|
48
|
+
|
|
49
|
+
# Identity
|
|
50
|
+
attribute :family_name, :string
|
|
51
|
+
attribute :subfamily_name, :string
|
|
52
|
+
attribute :full_name, :string
|
|
53
|
+
attribute :postscript_name, :string
|
|
54
|
+
attribute :version, :string
|
|
55
|
+
attribute :font_revision, :float
|
|
56
|
+
|
|
57
|
+
# Style
|
|
58
|
+
attribute :weight_class, :integer
|
|
59
|
+
attribute :width_class, :integer
|
|
60
|
+
attribute :italic, Lutaml::Model::Type::Boolean
|
|
61
|
+
attribute :bold, Lutaml::Model::Type::Boolean
|
|
62
|
+
attribute :panose, :string
|
|
63
|
+
attribute :is_variable, Lutaml::Model::Type::Boolean
|
|
64
|
+
attribute :axes, AuditAxis, collection: true
|
|
65
|
+
|
|
66
|
+
# Coverage
|
|
67
|
+
attribute :total_codepoints, :integer
|
|
68
|
+
attribute :total_glyphs, :integer
|
|
69
|
+
attribute :cmap_subtables, :integer, collection: true
|
|
70
|
+
attribute :codepoints, :string, collection: true
|
|
71
|
+
|
|
72
|
+
# OpenType layout
|
|
73
|
+
attribute :opentype_scripts, :string, collection: true
|
|
74
|
+
attribute :features, :string, collection: true
|
|
75
|
+
|
|
76
|
+
json do
|
|
77
|
+
map "generated_at", to: :generated_at
|
|
78
|
+
map "fontisan_version", to: :fontisan_version
|
|
79
|
+
map "source_file", to: :source_file
|
|
80
|
+
map "source_sha256", to: :source_sha256
|
|
81
|
+
map "source_format", to: :source_format
|
|
82
|
+
map "font_index", to: :font_index
|
|
83
|
+
map "num_fonts_in_source", to: :num_fonts_in_source
|
|
84
|
+
|
|
85
|
+
map "family_name", to: :family_name
|
|
86
|
+
map "subfamily_name", to: :subfamily_name
|
|
87
|
+
map "full_name", to: :full_name
|
|
88
|
+
map "postscript_name", to: :postscript_name
|
|
89
|
+
map "version", to: :version
|
|
90
|
+
map "font_revision", to: :font_revision
|
|
91
|
+
|
|
92
|
+
map "weight_class", to: :weight_class
|
|
93
|
+
map "width_class", to: :width_class
|
|
94
|
+
map "italic", to: :italic
|
|
95
|
+
map "bold", to: :bold
|
|
96
|
+
map "panose", to: :panose
|
|
97
|
+
map "is_variable", to: :is_variable
|
|
98
|
+
map "axes", to: :axes
|
|
99
|
+
|
|
100
|
+
map "total_codepoints", to: :total_codepoints
|
|
101
|
+
map "total_glyphs", to: :total_glyphs
|
|
102
|
+
map "cmap_subtables", to: :cmap_subtables
|
|
103
|
+
map "codepoints", to: :codepoints
|
|
104
|
+
|
|
105
|
+
map "opentype_scripts", to: :opentype_scripts
|
|
106
|
+
map "features", to: :features
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
yaml do
|
|
110
|
+
map "generated_at", to: :generated_at
|
|
111
|
+
map "fontisan_version", to: :fontisan_version
|
|
112
|
+
map "source_file", to: :source_file
|
|
113
|
+
map "source_sha256", to: :source_sha256
|
|
114
|
+
map "source_format", to: :source_format
|
|
115
|
+
map "font_index", to: :font_index
|
|
116
|
+
map "num_fonts_in_source", to: :num_fonts_in_source
|
|
117
|
+
|
|
118
|
+
map "family_name", to: :family_name
|
|
119
|
+
map "subfamily_name", to: :subfamily_name
|
|
120
|
+
map "full_name", to: :full_name
|
|
121
|
+
map "postscript_name", to: :postscript_name
|
|
122
|
+
map "version", to: :version
|
|
123
|
+
map "font_revision", to: :font_revision
|
|
124
|
+
|
|
125
|
+
map "weight_class", to: :weight_class
|
|
126
|
+
map "width_class", to: :width_class
|
|
127
|
+
map "italic", to: :italic
|
|
128
|
+
map "bold", to: :bold
|
|
129
|
+
map "panose", to: :panose
|
|
130
|
+
map "is_variable", to: :is_variable
|
|
131
|
+
map "axes", to: :axes
|
|
132
|
+
|
|
133
|
+
map "total_codepoints", to: :total_codepoints
|
|
134
|
+
map "total_glyphs", to: :total_glyphs
|
|
135
|
+
map "cmap_subtables", to: :cmap_subtables
|
|
136
|
+
map "codepoints", to: :codepoints
|
|
137
|
+
|
|
138
|
+
map "opentype_scripts", to: :opentype_scripts
|
|
139
|
+
map "features", to: :features
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/fontisan/models.rb
CHANGED
|
@@ -6,6 +6,8 @@ module Fontisan
|
|
|
6
6
|
module Models
|
|
7
7
|
autoload :AllScriptsFeaturesInfo,
|
|
8
8
|
"fontisan/models/all_scripts_features_info"
|
|
9
|
+
autoload :AuditAxis, "fontisan/models/audit_report"
|
|
10
|
+
autoload :AuditReport, "fontisan/models/audit_report"
|
|
9
11
|
autoload :AxisInfo, "fontisan/models/variable_font_info"
|
|
10
12
|
autoload :BitmapGlyph, "fontisan/models/bitmap_glyph"
|
|
11
13
|
autoload :BitmapStrike, "fontisan/models/bitmap_strike"
|