fontisan 0.1.0 → 0.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/.rubocop_todo.yml +672 -69
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1477 -297
- data/Rakefile +63 -41
- 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 +364 -4
- data/lib/fontisan/collection/builder.rb +341 -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 +317 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +24 -1
- data/lib/fontisan/commands/convert_command.rb +218 -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 +286 -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 +203 -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 +79 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +408 -0
- data/lib/fontisan/converters/outline_converter.rb +998 -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 +122 -15
- data/lib/fontisan/font_writer.rb +302 -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 +310 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +266 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +354 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +117 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +289 -0
- data/lib/fontisan/loading_modes.rb +115 -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 +405 -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 +321 -19
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- 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/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +154 -0
- data/lib/fontisan/pipeline/strategies/base_strategy.rb +75 -0
- data/lib/fontisan/pipeline/strategies/instance_strategy.rb +93 -0
- data/lib/fontisan/pipeline/strategies/named_strategy.rb +118 -0
- data/lib/fontisan/pipeline/strategies/preserve_strategy.rb +56 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +411 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -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 +934 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +356 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +237 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -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 +257 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -0
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +207 -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/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +125 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +489 -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/private_dict_blend_handler.rb +246 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +574 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +419 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +346 -0
- data/lib/fontisan/tables/cvar.rb +203 -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 +231 -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 +321 -20
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +60 -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/validation/variable_font_validator.rb +218 -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 +375 -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/instance_writer.rb +341 -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/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/validator.rb +345 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/variation/variation_preserver.rb +288 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +666 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +717 -0
- data/lib/fontisan/woff_font.rb +488 -0
- data/lib/fontisan.rb +132 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +234 -4
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "variation_context"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Variation
|
|
9
|
+
# Inspects and analyzes variable font structure
|
|
10
|
+
#
|
|
11
|
+
# This class provides comprehensive analysis of variable font structure,
|
|
12
|
+
# including axes, instances, regions, and variation statistics. Results
|
|
13
|
+
# can be exported to JSON or YAML formats.
|
|
14
|
+
#
|
|
15
|
+
# @example Inspecting a variable font
|
|
16
|
+
# inspector = Fontisan::Variation::Inspector.new(font)
|
|
17
|
+
# info = inspector.inspect_variation
|
|
18
|
+
# # => { axes: [...], instances: [...], regions: {...}, statistics: {...} }
|
|
19
|
+
#
|
|
20
|
+
# @example Exporting to JSON
|
|
21
|
+
# inspector.export_json
|
|
22
|
+
# # => "{ \"axes\": [...], ... }"
|
|
23
|
+
#
|
|
24
|
+
# @example Exporting to YAML
|
|
25
|
+
# inspector.export_yaml
|
|
26
|
+
# # => "---\naxes:\n - ..."
|
|
27
|
+
class Inspector
|
|
28
|
+
# @return [TrueTypeFont, OpenTypeFont] Font to inspect
|
|
29
|
+
attr_reader :font
|
|
30
|
+
|
|
31
|
+
# @return [VariationContext] Variation context
|
|
32
|
+
attr_reader :context
|
|
33
|
+
|
|
34
|
+
# Initialize inspector
|
|
35
|
+
#
|
|
36
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
37
|
+
def initialize(font)
|
|
38
|
+
@font = font
|
|
39
|
+
@context = VariationContext.new(font)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Inspect complete variation structure
|
|
43
|
+
#
|
|
44
|
+
# Returns comprehensive information about font variation capabilities.
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash] Complete variation information
|
|
47
|
+
def inspect_variation
|
|
48
|
+
{
|
|
49
|
+
axes: inspect_axes,
|
|
50
|
+
instances: inspect_instances,
|
|
51
|
+
regions: inspect_regions,
|
|
52
|
+
statistics: calculate_statistics,
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Export inspection results as JSON
|
|
57
|
+
#
|
|
58
|
+
# @return [String] JSON formatted output
|
|
59
|
+
def export_json
|
|
60
|
+
JSON.pretty_generate(inspect_variation)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Export inspection results as YAML
|
|
64
|
+
#
|
|
65
|
+
# @return [String] YAML formatted output
|
|
66
|
+
def export_yaml
|
|
67
|
+
YAML.dump(inspect_variation)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Check if font is a variable font
|
|
71
|
+
#
|
|
72
|
+
# @return [Boolean] True if font has variation tables
|
|
73
|
+
def variable_font?
|
|
74
|
+
@context.variable_font?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Inspect variation axes
|
|
80
|
+
#
|
|
81
|
+
# @return [Array<Hash>] Array of axis information
|
|
82
|
+
def inspect_axes
|
|
83
|
+
return [] unless variable_font?
|
|
84
|
+
return [] unless @context.fvar
|
|
85
|
+
|
|
86
|
+
@context.axes.map do |axis|
|
|
87
|
+
{
|
|
88
|
+
tag: axis.axis_tag,
|
|
89
|
+
name: axis_name(axis.axis_name_id),
|
|
90
|
+
min: axis.min_value,
|
|
91
|
+
default: axis.default_value,
|
|
92
|
+
max: axis.max_value,
|
|
93
|
+
hidden: axis.flags & 0x0001 != 0,
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Inspect named instances
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<Hash>] Array of instance information
|
|
101
|
+
def inspect_instances
|
|
102
|
+
return [] unless variable_font?
|
|
103
|
+
return [] unless @context.fvar
|
|
104
|
+
|
|
105
|
+
@context.fvar.instances.map.with_index do |instance, index|
|
|
106
|
+
{
|
|
107
|
+
index: index,
|
|
108
|
+
name: instance_name(instance[:subfamily_name_id]),
|
|
109
|
+
postscript_name: instance_name(instance[:postscript_name_id]),
|
|
110
|
+
coordinates: instance_coordinates(instance[:coordinates], @context.axes),
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Inspect variation regions
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash] Region statistics and information
|
|
118
|
+
def inspect_regions
|
|
119
|
+
regions = {
|
|
120
|
+
gvar: nil,
|
|
121
|
+
hvar: nil,
|
|
122
|
+
vvar: nil,
|
|
123
|
+
mvar: nil,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if @font.has_table?("gvar")
|
|
127
|
+
regions[:gvar] = inspect_gvar_regions
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if @font.has_table?("HVAR")
|
|
131
|
+
regions[:hvar] = inspect_hvar_regions
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if @font.has_table?("VVAR")
|
|
135
|
+
regions[:vvar] = inspect_vvar_regions
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if @font.has_table?("MVAR")
|
|
139
|
+
regions[:mvar] = inspect_mvar_regions
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
regions.compact
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Inspect gvar table regions
|
|
146
|
+
#
|
|
147
|
+
# @return [Hash] Gvar region information
|
|
148
|
+
def inspect_gvar_regions
|
|
149
|
+
gvar = @font.table("gvar")
|
|
150
|
+
return nil unless gvar
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
glyph_count: gvar.glyph_count,
|
|
154
|
+
axis_count: gvar.axis_count,
|
|
155
|
+
shared_tuples: gvar.shared_tuple_count || 0,
|
|
156
|
+
glyph_variation_data_present: gvar.glyph_count.positive?,
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Inspect HVAR table regions
|
|
161
|
+
#
|
|
162
|
+
# @return [Hash] HVAR region information
|
|
163
|
+
def inspect_hvar_regions
|
|
164
|
+
hvar = @font.table("HVAR")
|
|
165
|
+
return nil unless hvar
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
advance_width_mapping: hvar.advance_width_mapping ? true : false,
|
|
169
|
+
lsb_mapping: hvar.lsb_mapping ? true : false,
|
|
170
|
+
rsb_mapping: hvar.rsb_mapping ? true : false,
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Inspect VVAR table regions
|
|
175
|
+
#
|
|
176
|
+
# @return [Hash] VVAR region information
|
|
177
|
+
def inspect_vvar_regions
|
|
178
|
+
vvar = @font.table("VVAR")
|
|
179
|
+
return nil unless vvar
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
advance_height_mapping: vvar.advance_height_mapping ? true : false,
|
|
183
|
+
tsb_mapping: vvar.tsb_mapping ? true : false,
|
|
184
|
+
bsb_mapping: vvar.bsb_mapping ? true : false,
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Inspect MVAR table regions
|
|
189
|
+
#
|
|
190
|
+
# @return [Hash] MVAR region information
|
|
191
|
+
def inspect_mvar_regions
|
|
192
|
+
mvar = @font.table("MVAR")
|
|
193
|
+
return nil unless mvar
|
|
194
|
+
|
|
195
|
+
{
|
|
196
|
+
value_record_count: mvar.value_record_count || 0,
|
|
197
|
+
metrics_varied: mvar.value_records&.map { |r| r[:value_tag] } || [],
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Calculate variation statistics
|
|
202
|
+
#
|
|
203
|
+
# @return [Hash] Statistical information
|
|
204
|
+
def calculate_statistics
|
|
205
|
+
stats = {
|
|
206
|
+
is_variable: variable_font?,
|
|
207
|
+
axis_count: 0,
|
|
208
|
+
instance_count: 0,
|
|
209
|
+
has_glyph_variations: @context.has_glyph_variations?,
|
|
210
|
+
has_metrics_variations: @context.has_metrics_variations?,
|
|
211
|
+
variation_tables: [],
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if variable_font?
|
|
215
|
+
stats[:axis_count] = @context.axis_count
|
|
216
|
+
stats[:instance_count] = @context.fvar.instance_count if @context.fvar
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# List variation tables present
|
|
220
|
+
variation_table_tags = %w[fvar gvar cvar HVAR VVAR MVAR avar STAT]
|
|
221
|
+
stats[:variation_tables] = variation_table_tags.select do |tag|
|
|
222
|
+
@font.has_table?(tag)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Calculate design space size
|
|
226
|
+
if stats[:axis_count].positive?
|
|
227
|
+
stats[:design_space_dimensions] = stats[:axis_count]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
stats
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Get axis name from name table
|
|
234
|
+
#
|
|
235
|
+
# @param name_id [Integer] Name ID
|
|
236
|
+
# @return [String] Axis name
|
|
237
|
+
def axis_name(name_id)
|
|
238
|
+
return "Unknown" unless @font.has_table?("name")
|
|
239
|
+
|
|
240
|
+
name_table = @font.table("name")
|
|
241
|
+
record = name_table.names.find { |n| n[:name_id] == name_id }
|
|
242
|
+
record ? record[:string] : "Axis #{name_id}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Get instance name from name table
|
|
246
|
+
#
|
|
247
|
+
# @param name_id [Integer] Name ID
|
|
248
|
+
# @return [String, nil] Instance name
|
|
249
|
+
def instance_name(name_id)
|
|
250
|
+
return nil unless name_id
|
|
251
|
+
return nil unless @font.has_table?("name")
|
|
252
|
+
|
|
253
|
+
name_table = @font.table("name")
|
|
254
|
+
record = name_table.names.find { |n| n[:name_id] == name_id }
|
|
255
|
+
record ? record[:string] : "Instance #{name_id}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Build coordinates hash from instance
|
|
259
|
+
#
|
|
260
|
+
# @param coordinates [Array<Float>] Coordinate values
|
|
261
|
+
# @param axes [Array] Variation axes
|
|
262
|
+
# @return [Hash<String, Float>] Coordinates by axis tag
|
|
263
|
+
def instance_coordinates(coordinates, axes)
|
|
264
|
+
coords = {}
|
|
265
|
+
coordinates.each_with_index do |value, index|
|
|
266
|
+
break if index >= axes.length
|
|
267
|
+
|
|
268
|
+
axis = axes[index]
|
|
269
|
+
coords[axis.axis_tag] = value
|
|
270
|
+
end
|
|
271
|
+
coords
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "interpolator"
|
|
4
|
+
require_relative "region_matcher"
|
|
5
|
+
require_relative "metrics_adjuster"
|
|
6
|
+
require_relative "variation_context"
|
|
7
|
+
require_relative "table_accessor"
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Variation
|
|
11
|
+
# Generates static font instances from variable fonts
|
|
12
|
+
#
|
|
13
|
+
# This class creates static font instances by applying variation deltas
|
|
14
|
+
# at specific design space coordinates. It supports both TrueType (gvar)
|
|
15
|
+
# and PostScript (CFF2) variable fonts.
|
|
16
|
+
#
|
|
17
|
+
# Process:
|
|
18
|
+
# 1. Extract variation data (axes, deltas, regions)
|
|
19
|
+
# 2. Calculate interpolation scalars for given coordinates
|
|
20
|
+
# 3. Apply deltas to outlines (gvar or CFF2)
|
|
21
|
+
# 4. Apply deltas to metrics (HVAR, VVAR, MVAR)
|
|
22
|
+
# 5. Remove variation tables to create static font
|
|
23
|
+
#
|
|
24
|
+
# @example Generating an instance at specific coordinates
|
|
25
|
+
# generator = Fontisan::Variation::InstanceGenerator.new(font, { "wght" => 700.0 })
|
|
26
|
+
# instance_tables = generator.generate
|
|
27
|
+
#
|
|
28
|
+
# @example Generating a named instance
|
|
29
|
+
# generator = Fontisan::Variation::InstanceGenerator.new(font)
|
|
30
|
+
# instance_tables = generator.generate_named_instance(0)
|
|
31
|
+
class InstanceGenerator
|
|
32
|
+
include TableAccessor
|
|
33
|
+
|
|
34
|
+
# @return [TrueTypeFont, OpenTypeFont] Variable font
|
|
35
|
+
attr_reader :font
|
|
36
|
+
|
|
37
|
+
# @return [Hash<String, Float>] Design space coordinates
|
|
38
|
+
attr_reader :coordinates
|
|
39
|
+
|
|
40
|
+
# @return [VariationContext] Variation context
|
|
41
|
+
attr_reader :context
|
|
42
|
+
|
|
43
|
+
# Initialize generator with font and optional coordinates
|
|
44
|
+
#
|
|
45
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
46
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates (axis tag => value)
|
|
47
|
+
# @param options [Hash] Options
|
|
48
|
+
# @option options [Boolean] :skip_validation Skip context validation (default: false)
|
|
49
|
+
def initialize(font, coordinates = {}, options = {})
|
|
50
|
+
@font = font
|
|
51
|
+
@coordinates = coordinates
|
|
52
|
+
|
|
53
|
+
# Initialize variation context
|
|
54
|
+
@context = VariationContext.new(@font)
|
|
55
|
+
@context.validate! unless options[:skip_validation]
|
|
56
|
+
|
|
57
|
+
# Initialize table cache for lazy loading
|
|
58
|
+
@variation_tables = {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generate static font instance
|
|
62
|
+
#
|
|
63
|
+
# Applies variation deltas and returns static font tables.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
66
|
+
def generate
|
|
67
|
+
# Start with base font tables
|
|
68
|
+
tables = @font.table_data.dup
|
|
69
|
+
|
|
70
|
+
# Determine variation type
|
|
71
|
+
if has_variation_table?("gvar")
|
|
72
|
+
# TrueType outlines with gvar
|
|
73
|
+
apply_gvar_deltas(tables)
|
|
74
|
+
elsif has_variation_table?("CFF2")
|
|
75
|
+
# PostScript outlines with CFF2 blend
|
|
76
|
+
apply_cff2_blend(tables)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Apply metrics variations if present
|
|
80
|
+
apply_metrics_deltas(tables) if @context.has_metrics_variations?
|
|
81
|
+
|
|
82
|
+
# Remove variation-specific tables to create static font
|
|
83
|
+
remove_variation_tables(tables)
|
|
84
|
+
|
|
85
|
+
tables
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Generate a named instance
|
|
89
|
+
#
|
|
90
|
+
# @param instance_index [Integer] Index of named instance in fvar table
|
|
91
|
+
# @return [Hash<String, String>] Map of table tags to binary data
|
|
92
|
+
def generate_named_instance(instance_index)
|
|
93
|
+
# Extract instance coordinates from fvar
|
|
94
|
+
return generate if instance_index.nil? || !@context.fvar
|
|
95
|
+
|
|
96
|
+
instances = @context.fvar.instances
|
|
97
|
+
return generate if instance_index >= instances.length
|
|
98
|
+
|
|
99
|
+
instance = instances[instance_index]
|
|
100
|
+
@coordinates = build_coordinates_from_instance(instance, @context.axes)
|
|
101
|
+
|
|
102
|
+
generate
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Apply gvar deltas to TrueType outlines
|
|
106
|
+
#
|
|
107
|
+
# @param tables [Hash<String, String>] Font tables
|
|
108
|
+
def apply_gvar_deltas(_tables)
|
|
109
|
+
gvar = variation_table("gvar")
|
|
110
|
+
glyf = @font.table("glyf")
|
|
111
|
+
return unless gvar && glyf
|
|
112
|
+
|
|
113
|
+
# Get glyph count
|
|
114
|
+
maxp = @font.table("maxp")
|
|
115
|
+
glyph_count = maxp ? maxp.num_glyphs : gvar.glyph_count
|
|
116
|
+
|
|
117
|
+
# Process each glyph
|
|
118
|
+
glyph_count.times do |glyph_id|
|
|
119
|
+
apply_glyph_deltas(glyph_id, gvar, glyf)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Rebuild glyf and loca tables with adjusted outlines
|
|
123
|
+
# This is a placeholder - full implementation would reconstruct tables
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Apply deltas to a specific glyph
|
|
127
|
+
#
|
|
128
|
+
# @param glyph_id [Integer] Glyph ID
|
|
129
|
+
# @param gvar [Gvar] Gvar table
|
|
130
|
+
# @param glyf [Glyf] Glyf table
|
|
131
|
+
def apply_glyph_deltas(glyph_id, gvar, _glyf)
|
|
132
|
+
# Get tuple variations for this glyph
|
|
133
|
+
tuple_data = gvar.glyph_tuple_variations(glyph_id)
|
|
134
|
+
return unless tuple_data
|
|
135
|
+
|
|
136
|
+
# Match tuples to current coordinates
|
|
137
|
+
matches = @context.region_matcher.match_tuples(
|
|
138
|
+
coordinates: @coordinates,
|
|
139
|
+
tuples: tuple_data[:tuples],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
nil if matches.empty?
|
|
143
|
+
|
|
144
|
+
# Get base glyph outline
|
|
145
|
+
# Apply matched deltas with their scalars
|
|
146
|
+
# This is a placeholder - full implementation would:
|
|
147
|
+
# 1. Parse glyph outline points
|
|
148
|
+
# 2. Parse delta data for each tuple
|
|
149
|
+
# 3. Apply: new_point = base_point + Σ(delta * scalar)
|
|
150
|
+
# 4. Update glyph outline
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Apply CFF2 blend operators
|
|
154
|
+
#
|
|
155
|
+
# @param tables [Hash<String, String>] Font tables
|
|
156
|
+
def apply_cff2_blend(_tables)
|
|
157
|
+
cff2 = variation_table("CFF2")
|
|
158
|
+
return unless cff2
|
|
159
|
+
|
|
160
|
+
# Set number of axes for CFF2
|
|
161
|
+
cff2.num_axes = @context.axis_count
|
|
162
|
+
|
|
163
|
+
# Process each glyph's CharString
|
|
164
|
+
glyph_count = cff2.glyph_count
|
|
165
|
+
return if glyph_count.zero?
|
|
166
|
+
|
|
167
|
+
# Calculate variation scalars once
|
|
168
|
+
calculate_variation_scalars
|
|
169
|
+
|
|
170
|
+
# Apply blend to each glyph
|
|
171
|
+
# This is a placeholder - full implementation would:
|
|
172
|
+
# 1. Parse CharString with blend operators
|
|
173
|
+
# 2. Apply scalars to blend operands
|
|
174
|
+
# 3. Rebuild CharStrings without blend operators
|
|
175
|
+
# 4. Update CFF2 table
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Calculate variation scalars for current coordinates
|
|
179
|
+
#
|
|
180
|
+
# @return [Array<Float>] Scalars for each axis
|
|
181
|
+
def calculate_variation_scalars
|
|
182
|
+
@context.axes.map do |axis|
|
|
183
|
+
coord = @coordinates[axis.axis_tag] || axis.default_value
|
|
184
|
+
@context.interpolator.normalize_coordinate(coord, axis.axis_tag)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Apply metrics variations
|
|
189
|
+
#
|
|
190
|
+
# @param tables [Hash<String, String>] Font tables
|
|
191
|
+
def apply_metrics_deltas(tables)
|
|
192
|
+
# Apply HVAR (horizontal metrics)
|
|
193
|
+
apply_hvar_deltas(tables) if has_variation_table?("HVAR")
|
|
194
|
+
|
|
195
|
+
# Apply VVAR (vertical metrics)
|
|
196
|
+
apply_vvar_deltas(tables) if has_variation_table?("VVAR")
|
|
197
|
+
|
|
198
|
+
# Apply MVAR (font-wide metrics)
|
|
199
|
+
apply_mvar_deltas(tables) if has_variation_table?("MVAR")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Apply HVAR deltas to horizontal metrics
|
|
203
|
+
#
|
|
204
|
+
# @param tables [Hash<String, String>] Font tables
|
|
205
|
+
def apply_hvar_deltas(_tables)
|
|
206
|
+
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
207
|
+
adjuster.apply_hvar_deltas(@coordinates)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Apply VVAR deltas to vertical metrics
|
|
211
|
+
#
|
|
212
|
+
# @param tables [Hash<String, String>] Font tables
|
|
213
|
+
def apply_vvar_deltas(_tables)
|
|
214
|
+
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
215
|
+
adjuster.apply_vvar_deltas(@coordinates)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Apply MVAR deltas to font-wide metrics
|
|
219
|
+
#
|
|
220
|
+
# @param tables [Hash<String, String>] Font tables
|
|
221
|
+
def apply_mvar_deltas(_tables)
|
|
222
|
+
adjuster = MetricsAdjuster.new(@font, @context.interpolator)
|
|
223
|
+
adjuster.apply_mvar_deltas(@coordinates)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Remove variation tables from static font
|
|
227
|
+
#
|
|
228
|
+
# @param tables [Hash<String, String>] Font tables
|
|
229
|
+
def remove_variation_tables(tables)
|
|
230
|
+
variation_tables = %w[fvar gvar cvar HVAR VVAR MVAR avar STAT]
|
|
231
|
+
variation_tables.each { |tag| tables.delete(tag) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Interpolate a single value
|
|
235
|
+
#
|
|
236
|
+
# @param base_value [Numeric] Base value
|
|
237
|
+
# @param deltas [Array<Numeric>] Delta values
|
|
238
|
+
# @param scalars [Array<Float>] Region scalars
|
|
239
|
+
# @return [Float] Interpolated value
|
|
240
|
+
def interpolate_value(base_value, deltas, scalars)
|
|
241
|
+
@context.interpolator.interpolate_value(base_value, deltas, scalars)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Interpolate a point
|
|
245
|
+
#
|
|
246
|
+
# @param base_point [Hash] Base point with :x and :y
|
|
247
|
+
# @param delta_points [Array<Hash>] Delta points
|
|
248
|
+
# @param scalars [Array<Float>] Region scalars
|
|
249
|
+
# @return [Hash] Interpolated point
|
|
250
|
+
def interpolate_point(base_point, delta_points, scalars)
|
|
251
|
+
@context.interpolator.interpolate_point(base_point, delta_points, scalars)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
# Build coordinates hash from instance
|
|
257
|
+
#
|
|
258
|
+
# @param instance [Hash] Instance data from fvar
|
|
259
|
+
# @param axes [Array<VariationAxisRecord>] Variation axes
|
|
260
|
+
# @return [Hash<String, Float>] Coordinates hash
|
|
261
|
+
def build_coordinates_from_instance(instance, axes)
|
|
262
|
+
coordinates = {}
|
|
263
|
+
instance[:coordinates].each_with_index do |value, index|
|
|
264
|
+
next if index >= axes.length
|
|
265
|
+
|
|
266
|
+
axis = axes[index]
|
|
267
|
+
coordinates[axis.axis_tag] = value
|
|
268
|
+
end
|
|
269
|
+
coordinates
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|