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,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Models
|
|
7
|
+
# ValidationReport represents the result of font validation
|
|
8
|
+
#
|
|
9
|
+
# This model encapsulates all validation results including error, warning,
|
|
10
|
+
# and informational messages. It supports serialization to YAML, JSON, and
|
|
11
|
+
# plain text formats for different use cases.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating a validation report
|
|
14
|
+
# report = ValidationReport.new(
|
|
15
|
+
# font_path: "font.ttf",
|
|
16
|
+
# valid: false
|
|
17
|
+
# )
|
|
18
|
+
# report.add_error("tables", "Missing required table: glyf", nil)
|
|
19
|
+
# report.add_warning("checksum", "Table 'name' checksum mismatch", "name table")
|
|
20
|
+
#
|
|
21
|
+
# @example Serializing to YAML
|
|
22
|
+
# yaml_output = report.to_yaml
|
|
23
|
+
#
|
|
24
|
+
# @example Serializing to JSON
|
|
25
|
+
# json_output = report.to_json
|
|
26
|
+
class ValidationReport < Lutaml::Model::Serializable
|
|
27
|
+
# Individual validation issue
|
|
28
|
+
class Issue < Lutaml::Model::Serializable
|
|
29
|
+
attribute :severity, :string
|
|
30
|
+
attribute :category, :string
|
|
31
|
+
attribute :message, :string
|
|
32
|
+
attribute :location, :string, default: -> {}
|
|
33
|
+
|
|
34
|
+
yaml do
|
|
35
|
+
map "severity", to: :severity
|
|
36
|
+
map "category", to: :category
|
|
37
|
+
map "message", to: :message
|
|
38
|
+
map "location", to: :location
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
json do
|
|
42
|
+
map "severity", to: :severity
|
|
43
|
+
map "category", to: :category
|
|
44
|
+
map "message", to: :message
|
|
45
|
+
map "location", to: :location
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Validation summary counts
|
|
50
|
+
class Summary < Lutaml::Model::Serializable
|
|
51
|
+
attribute :errors, :integer, default: -> { 0 }
|
|
52
|
+
attribute :warnings, :integer, default: -> { 0 }
|
|
53
|
+
attribute :info, :integer, default: -> { 0 }
|
|
54
|
+
|
|
55
|
+
yaml do
|
|
56
|
+
map "errors", to: :errors
|
|
57
|
+
map "warnings", to: :warnings
|
|
58
|
+
map "info", to: :info
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
json do
|
|
62
|
+
map "errors", to: :errors
|
|
63
|
+
map "warnings", to: :warnings
|
|
64
|
+
map "info", to: :info
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
attribute :font_path, :string
|
|
69
|
+
attribute :valid, :boolean
|
|
70
|
+
attribute :issues, Issue, collection: true, default: -> { [] }
|
|
71
|
+
attribute :summary, Summary, default: -> { Summary.new }
|
|
72
|
+
|
|
73
|
+
yaml do
|
|
74
|
+
map "font_path", to: :font_path
|
|
75
|
+
map "valid", to: :valid
|
|
76
|
+
map "summary", to: :summary
|
|
77
|
+
map "issues", to: :issues
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
json do
|
|
81
|
+
map "font_path", to: :font_path
|
|
82
|
+
map "valid", to: :valid
|
|
83
|
+
map "summary", to: :summary
|
|
84
|
+
map "issues", to: :issues
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Add an error to the report
|
|
88
|
+
#
|
|
89
|
+
# @param category [String] The error category (e.g., "tables", "structure")
|
|
90
|
+
# @param message [String] The error message
|
|
91
|
+
# @param location [String, nil] The specific location of the error
|
|
92
|
+
# @return [void]
|
|
93
|
+
def add_error(category, message, location = nil)
|
|
94
|
+
issues << Issue.new(
|
|
95
|
+
severity: "error",
|
|
96
|
+
category: category,
|
|
97
|
+
message: message,
|
|
98
|
+
location: location,
|
|
99
|
+
)
|
|
100
|
+
summary.errors += 1
|
|
101
|
+
self.valid = false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Add a warning to the report
|
|
105
|
+
#
|
|
106
|
+
# @param category [String] The warning category
|
|
107
|
+
# @param message [String] The warning message
|
|
108
|
+
# @param location [String, nil] The specific location of the warning
|
|
109
|
+
# @return [void]
|
|
110
|
+
def add_warning(category, message, location = nil)
|
|
111
|
+
issues << Issue.new(
|
|
112
|
+
severity: "warning",
|
|
113
|
+
category: category,
|
|
114
|
+
message: message,
|
|
115
|
+
location: location,
|
|
116
|
+
)
|
|
117
|
+
summary.warnings += 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Add an info message to the report
|
|
121
|
+
#
|
|
122
|
+
# @param category [String] The info category
|
|
123
|
+
# @param message [String] The info message
|
|
124
|
+
# @param location [String, nil] The specific location
|
|
125
|
+
# @return [void]
|
|
126
|
+
def add_info(category, message, location = nil)
|
|
127
|
+
issues << Issue.new(
|
|
128
|
+
severity: "info",
|
|
129
|
+
category: category,
|
|
130
|
+
message: message,
|
|
131
|
+
location: location,
|
|
132
|
+
)
|
|
133
|
+
summary.info += 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get all error issues
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<Issue>] Array of error issues
|
|
139
|
+
def errors
|
|
140
|
+
issues.select { |issue| issue.severity == "error" }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Get all warning issues
|
|
144
|
+
#
|
|
145
|
+
# @return [Array<Issue>] Array of warning issues
|
|
146
|
+
def warnings
|
|
147
|
+
issues.select { |issue| issue.severity == "warning" }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get all info issues
|
|
151
|
+
#
|
|
152
|
+
# @return [Array<Issue>] Array of info issues
|
|
153
|
+
def info_issues
|
|
154
|
+
issues.select { |issue| issue.severity == "info" }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Check if report has errors
|
|
158
|
+
#
|
|
159
|
+
# @return [Boolean] true if errors exist
|
|
160
|
+
def has_errors?
|
|
161
|
+
summary.errors.positive?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check if report has warnings
|
|
165
|
+
#
|
|
166
|
+
# @return [Boolean] true if warnings exist
|
|
167
|
+
def has_warnings?
|
|
168
|
+
summary.warnings.positive?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Get a text summary of the validation
|
|
172
|
+
#
|
|
173
|
+
# @return [String] Human-readable summary
|
|
174
|
+
def text_summary
|
|
175
|
+
status = valid ? "VALID" : "INVALID"
|
|
176
|
+
lines = []
|
|
177
|
+
lines << "Font: #{font_path}"
|
|
178
|
+
lines << "Status: #{status}"
|
|
179
|
+
lines << ""
|
|
180
|
+
lines << "Summary:"
|
|
181
|
+
lines << " Errors: #{summary.errors}"
|
|
182
|
+
lines << " Warnings: #{summary.warnings}"
|
|
183
|
+
lines << " Info: #{summary.info}"
|
|
184
|
+
|
|
185
|
+
if issues.any?
|
|
186
|
+
lines << ""
|
|
187
|
+
lines << "Issues:"
|
|
188
|
+
issues.each do |issue|
|
|
189
|
+
severity_marker = case issue.severity
|
|
190
|
+
when "error" then "[ERROR]"
|
|
191
|
+
when "warning" then "[WARN]"
|
|
192
|
+
when "info" then "[INFO]"
|
|
193
|
+
end
|
|
194
|
+
location_info = issue.location ? " (#{issue.location})" : ""
|
|
195
|
+
lines << " #{severity_marker} #{issue.category}: #{issue.message}#{location_info}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
lines.join("\n")
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -63,12 +63,13 @@ module Fontisan
|
|
|
63
63
|
#
|
|
64
64
|
# @param index [Integer] Index of the font (0-based)
|
|
65
65
|
# @param io [IO] Open file handle
|
|
66
|
+
# @param mode [Symbol] Loading mode (:metadata or :full, default: :full)
|
|
66
67
|
# @return [OpenTypeFont, nil] Font object or nil if index out of range
|
|
67
|
-
def font(index, io)
|
|
68
|
+
def font(index, io, mode: LoadingModes::FULL)
|
|
68
69
|
return nil if index >= num_fonts
|
|
69
70
|
|
|
70
71
|
require_relative "open_type_font"
|
|
71
|
-
OpenTypeFont.from_collection(io, font_offsets[index])
|
|
72
|
+
OpenTypeFont.from_collection(io, font_offsets[index], mode: mode)
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
# Get font count
|
|
@@ -93,5 +94,158 @@ module Fontisan
|
|
|
93
94
|
def version
|
|
94
95
|
(major_version << 16) | minor_version
|
|
95
96
|
end
|
|
97
|
+
|
|
98
|
+
# List all fonts in the collection with basic metadata
|
|
99
|
+
#
|
|
100
|
+
# Returns a CollectionListInfo model containing summaries of all fonts.
|
|
101
|
+
# This is the API method used by the `ls` command for collections.
|
|
102
|
+
#
|
|
103
|
+
# @param io [IO] Open file handle to read fonts from
|
|
104
|
+
# @return [CollectionListInfo] List of fonts with metadata
|
|
105
|
+
#
|
|
106
|
+
# @example List fonts in collection
|
|
107
|
+
# File.open("fonts.otc", "rb") do |io|
|
|
108
|
+
# otc = OpenTypeCollection.read(io)
|
|
109
|
+
# list = otc.list_fonts(io)
|
|
110
|
+
# list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
|
|
111
|
+
# end
|
|
112
|
+
def list_fonts(io)
|
|
113
|
+
require_relative "models/collection_list_info"
|
|
114
|
+
require_relative "models/collection_font_summary"
|
|
115
|
+
require_relative "open_type_font"
|
|
116
|
+
require_relative "tables/name"
|
|
117
|
+
|
|
118
|
+
fonts = font_offsets.map.with_index do |offset, index|
|
|
119
|
+
font = OpenTypeFont.from_collection(io, offset)
|
|
120
|
+
|
|
121
|
+
# Extract basic font info
|
|
122
|
+
name_table = font.table("name")
|
|
123
|
+
post_table = font.table("post")
|
|
124
|
+
|
|
125
|
+
family_name = name_table&.english_name(Tables::Name::FAMILY) || "Unknown"
|
|
126
|
+
subfamily_name = name_table&.english_name(Tables::Name::SUBFAMILY) || "Regular"
|
|
127
|
+
postscript_name = name_table&.english_name(Tables::Name::POSTSCRIPT_NAME) || "Unknown"
|
|
128
|
+
|
|
129
|
+
# Determine font format
|
|
130
|
+
sfnt = font.header.sfnt_version
|
|
131
|
+
font_format = case sfnt
|
|
132
|
+
when 0x00010000, 0x74727565 # 0x74727565 = 'true'
|
|
133
|
+
"TrueType"
|
|
134
|
+
when 0x4F54544F # 'OTTO'
|
|
135
|
+
"OpenType"
|
|
136
|
+
else
|
|
137
|
+
"Unknown"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
num_glyphs = post_table&.glyph_names&.length || 0
|
|
141
|
+
num_tables = font.table_names.length
|
|
142
|
+
|
|
143
|
+
Models::CollectionFontSummary.new(
|
|
144
|
+
index: index,
|
|
145
|
+
family_name: family_name,
|
|
146
|
+
subfamily_name: subfamily_name,
|
|
147
|
+
postscript_name: postscript_name,
|
|
148
|
+
font_format: font_format,
|
|
149
|
+
num_glyphs: num_glyphs,
|
|
150
|
+
num_tables: num_tables,
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Models::CollectionListInfo.new(
|
|
155
|
+
collection_path: nil, # Will be set by command
|
|
156
|
+
num_fonts: num_fonts,
|
|
157
|
+
fonts: fonts,
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get comprehensive collection metadata
|
|
162
|
+
#
|
|
163
|
+
# Returns a CollectionInfo model with header information, offsets,
|
|
164
|
+
# and table sharing statistics.
|
|
165
|
+
# This is the API method used by the `info` command for collections.
|
|
166
|
+
#
|
|
167
|
+
# @param io [IO] Open file handle to read fonts from
|
|
168
|
+
# @param path [String] Collection file path (for file size)
|
|
169
|
+
# @return [CollectionInfo] Collection metadata
|
|
170
|
+
#
|
|
171
|
+
# @example Get collection info
|
|
172
|
+
# File.open("fonts.otc", "rb") do |io|
|
|
173
|
+
# otc = OpenTypeCollection.read(io)
|
|
174
|
+
# info = otc.collection_info(io, "fonts.otc")
|
|
175
|
+
# puts "Version: #{info.version_string}"
|
|
176
|
+
# end
|
|
177
|
+
def collection_info(io, path)
|
|
178
|
+
require_relative "models/collection_info"
|
|
179
|
+
require_relative "models/table_sharing_info"
|
|
180
|
+
|
|
181
|
+
# Calculate table sharing statistics
|
|
182
|
+
table_sharing = calculate_table_sharing(io)
|
|
183
|
+
|
|
184
|
+
# Get file size
|
|
185
|
+
file_size = path ? File.size(path) : 0
|
|
186
|
+
|
|
187
|
+
Models::CollectionInfo.new(
|
|
188
|
+
collection_path: path,
|
|
189
|
+
collection_format: "OTC",
|
|
190
|
+
ttc_tag: tag,
|
|
191
|
+
major_version: major_version,
|
|
192
|
+
minor_version: minor_version,
|
|
193
|
+
num_fonts: num_fonts,
|
|
194
|
+
font_offsets: font_offsets.to_a,
|
|
195
|
+
file_size_bytes: file_size,
|
|
196
|
+
table_sharing: table_sharing,
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# Calculate table sharing statistics
|
|
203
|
+
#
|
|
204
|
+
# Analyzes which tables are shared between fonts and calculates
|
|
205
|
+
# space savings from deduplication.
|
|
206
|
+
#
|
|
207
|
+
# @param io [IO] Open file handle
|
|
208
|
+
# @return [TableSharingInfo] Sharing statistics
|
|
209
|
+
def calculate_table_sharing(io)
|
|
210
|
+
require_relative "models/table_sharing_info"
|
|
211
|
+
require_relative "open_type_font"
|
|
212
|
+
|
|
213
|
+
# Extract all fonts
|
|
214
|
+
fonts = font_offsets.map do |offset|
|
|
215
|
+
OpenTypeFont.from_collection(io, offset)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Build table hash map (checksum -> size)
|
|
219
|
+
table_map = {}
|
|
220
|
+
total_table_size = 0
|
|
221
|
+
|
|
222
|
+
fonts.each do |font|
|
|
223
|
+
font.tables.each do |entry|
|
|
224
|
+
key = entry.checksum
|
|
225
|
+
size = entry.table_length
|
|
226
|
+
table_map[key] ||= size
|
|
227
|
+
total_table_size += size
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Count unique vs shared
|
|
232
|
+
unique_tables = table_map.size
|
|
233
|
+
total_tables = fonts.sum { |f| f.tables.length }
|
|
234
|
+
shared_tables = total_tables - unique_tables
|
|
235
|
+
|
|
236
|
+
# Calculate space saved
|
|
237
|
+
unique_size = table_map.values.sum
|
|
238
|
+
space_saved = total_table_size - unique_size
|
|
239
|
+
|
|
240
|
+
# Calculate sharing percentage
|
|
241
|
+
sharing_pct = total_tables.positive? ? (shared_tables.to_f / total_tables * 100).round(2) : 0.0
|
|
242
|
+
|
|
243
|
+
Models::TableSharingInfo.new(
|
|
244
|
+
shared_tables: shared_tables,
|
|
245
|
+
unique_tables: unique_tables,
|
|
246
|
+
sharing_percentage: sharing_pct,
|
|
247
|
+
space_saved_bytes: space_saved,
|
|
248
|
+
)
|
|
249
|
+
end
|
|
96
250
|
end
|
|
97
251
|
end
|