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,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "instance_generator"
|
|
4
|
+
require_relative "../converters/svg_generator"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Generates SVG fonts from variable fonts at specific coordinates
|
|
9
|
+
#
|
|
10
|
+
# [`VariableSvgGenerator`](lib/fontisan/variation/variable_svg_generator.rb)
|
|
11
|
+
# combines instance generation with SVG conversion to create static SVG
|
|
12
|
+
# fonts from variable fonts at any point in the design space.
|
|
13
|
+
#
|
|
14
|
+
# Process:
|
|
15
|
+
# 1. Accept variable font + axis coordinates
|
|
16
|
+
# 2. Generate static instance using InstanceGenerator
|
|
17
|
+
# 3. Build temporary font from instance tables
|
|
18
|
+
# 4. Delegate to SvgGenerator for SVG creation
|
|
19
|
+
# 5. Return SVG with variation metadata
|
|
20
|
+
#
|
|
21
|
+
# This enables generating SVG fonts at specific weights, widths, or other
|
|
22
|
+
# variation axes without creating intermediate font files.
|
|
23
|
+
#
|
|
24
|
+
# @example Generate SVG at Bold weight
|
|
25
|
+
# generator = VariableSvgGenerator.new(variable_font, { "wght" => 700.0 })
|
|
26
|
+
# svg_result = generator.generate
|
|
27
|
+
# File.write("bold.svg", svg_result[:svg_xml])
|
|
28
|
+
#
|
|
29
|
+
# @example Generate SVG at specific width and weight
|
|
30
|
+
# coords = { "wght" => 700.0, "wdth" => 75.0 }
|
|
31
|
+
# generator = VariableSvgGenerator.new(variable_font, coords)
|
|
32
|
+
# svg_result = generator.generate(pretty_print: true)
|
|
33
|
+
class VariableSvgGenerator
|
|
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
|
+
# Initialize generator
|
|
41
|
+
#
|
|
42
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
43
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates
|
|
44
|
+
# @raise [Error] If font is not a variable font
|
|
45
|
+
def initialize(font, coordinates = {})
|
|
46
|
+
@font = font
|
|
47
|
+
@coordinates = coordinates || {}
|
|
48
|
+
|
|
49
|
+
validate_variable_font!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Generate SVG font at specified coordinates
|
|
53
|
+
#
|
|
54
|
+
# Creates a static instance at the given coordinates and converts
|
|
55
|
+
# it to SVG format. Returns the same format as SvgGenerator for
|
|
56
|
+
# consistency.
|
|
57
|
+
#
|
|
58
|
+
# @param options [Hash] SVG generation options
|
|
59
|
+
# @option options [Boolean] :pretty_print Pretty print XML (default: true)
|
|
60
|
+
# @option options [Array<Integer>] :glyph_ids Specific glyphs (default: all)
|
|
61
|
+
# @option options [Integer] :max_glyphs Maximum glyphs (default: all)
|
|
62
|
+
# @option options [String] :font_id Font ID for SVG
|
|
63
|
+
# @option options [Integer] :default_advance Default advance width
|
|
64
|
+
# @return [Hash] Hash with :svg_xml key containing SVG XML string
|
|
65
|
+
# @raise [Error] If generation fails
|
|
66
|
+
def generate(options = {})
|
|
67
|
+
# Generate static instance tables
|
|
68
|
+
instance_tables = generate_static_instance
|
|
69
|
+
|
|
70
|
+
# Build temporary font from instance tables
|
|
71
|
+
static_font = build_font_from_tables(instance_tables)
|
|
72
|
+
|
|
73
|
+
# Generate SVG using standard generator
|
|
74
|
+
svg_generator = Converters::SvgGenerator.new
|
|
75
|
+
result = svg_generator.convert(static_font, options)
|
|
76
|
+
|
|
77
|
+
# Add variation metadata to result
|
|
78
|
+
result[:variation_metadata] = {
|
|
79
|
+
coordinates: @coordinates,
|
|
80
|
+
source_font: extract_font_name,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Generate SVG for a named instance
|
|
87
|
+
#
|
|
88
|
+
# @param instance_index [Integer] Index of named instance in fvar
|
|
89
|
+
# @param options [Hash] SVG generation options
|
|
90
|
+
# @return [Hash] Hash with :svg_xml key
|
|
91
|
+
def generate_named_instance(instance_index, options = {})
|
|
92
|
+
instance_generator = InstanceGenerator.new(@font)
|
|
93
|
+
instance_tables = instance_generator.generate_named_instance(instance_index)
|
|
94
|
+
|
|
95
|
+
static_font = build_font_from_tables(instance_tables)
|
|
96
|
+
svg_generator = Converters::SvgGenerator.new
|
|
97
|
+
result = svg_generator.convert(static_font, options)
|
|
98
|
+
|
|
99
|
+
# Add instance metadata
|
|
100
|
+
result[:variation_metadata] = {
|
|
101
|
+
instance_index: instance_index,
|
|
102
|
+
source_font: extract_font_name,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Get default coordinates for font
|
|
109
|
+
#
|
|
110
|
+
# Returns all axes at their default values.
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash<String, Float>] Default coordinates
|
|
113
|
+
def default_coordinates
|
|
114
|
+
return {} unless @font.has_table?("fvar")
|
|
115
|
+
|
|
116
|
+
fvar = @font.table("fvar")
|
|
117
|
+
return {} unless fvar
|
|
118
|
+
|
|
119
|
+
coords = {}
|
|
120
|
+
fvar.axes.each do |axis|
|
|
121
|
+
coords[axis.axis_tag] = axis.default_value
|
|
122
|
+
end
|
|
123
|
+
coords
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get list of named instances
|
|
127
|
+
#
|
|
128
|
+
# @return [Array<Hash>] Array of instance info
|
|
129
|
+
def named_instances
|
|
130
|
+
return [] unless @font.has_table?("fvar")
|
|
131
|
+
|
|
132
|
+
fvar = @font.table("fvar")
|
|
133
|
+
return [] unless fvar
|
|
134
|
+
|
|
135
|
+
fvar.instances.map.with_index do |instance, index|
|
|
136
|
+
{
|
|
137
|
+
index: index,
|
|
138
|
+
name: instance[:subfamily_name_id],
|
|
139
|
+
coordinates: build_instance_coordinates(instance, fvar.axes),
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Validate that font is a variable font
|
|
147
|
+
#
|
|
148
|
+
# @raise [Error] If not a variable font
|
|
149
|
+
def validate_variable_font!
|
|
150
|
+
unless @font.has_table?("fvar")
|
|
151
|
+
raise Fontisan::Error,
|
|
152
|
+
"Font must be a variable font (missing fvar table)"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check for variation data
|
|
156
|
+
has_gvar = @font.has_table?("gvar")
|
|
157
|
+
has_cff2 = @font.has_table?("CFF2")
|
|
158
|
+
|
|
159
|
+
unless has_gvar || has_cff2
|
|
160
|
+
raise Fontisan::Error,
|
|
161
|
+
"Variable font must have gvar (TrueType) or CFF2 (PostScript) table"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Generate static instance at current coordinates
|
|
166
|
+
#
|
|
167
|
+
# @return [Hash<String, String>] Instance tables
|
|
168
|
+
def generate_static_instance
|
|
169
|
+
# Use coordinates or defaults if none specified
|
|
170
|
+
coords = @coordinates.empty? ? default_coordinates : @coordinates
|
|
171
|
+
|
|
172
|
+
instance_generator = InstanceGenerator.new(@font, coords)
|
|
173
|
+
instance_generator.generate
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Build a font object from instance tables
|
|
177
|
+
#
|
|
178
|
+
# Creates a minimal font object that can be used by SvgGenerator.
|
|
179
|
+
# This is a lightweight wrapper around the table data.
|
|
180
|
+
#
|
|
181
|
+
# @param tables [Hash<String, String>] Font tables
|
|
182
|
+
# @return [Object] Font-like object
|
|
183
|
+
def build_font_from_tables(tables)
|
|
184
|
+
# Create a simple font wrapper that implements the minimal
|
|
185
|
+
# interface needed by SvgGenerator
|
|
186
|
+
InstanceFontWrapper.new(@font, tables)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Extract font name for metadata
|
|
190
|
+
#
|
|
191
|
+
# @return [String] Font name
|
|
192
|
+
def extract_font_name
|
|
193
|
+
name_table = @font.table("name")
|
|
194
|
+
return "Unknown" unless name_table
|
|
195
|
+
|
|
196
|
+
# Try font family name
|
|
197
|
+
family = name_table.font_family.first
|
|
198
|
+
return family if family && !family.empty?
|
|
199
|
+
|
|
200
|
+
"Unknown"
|
|
201
|
+
rescue StandardError
|
|
202
|
+
"Unknown"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Build coordinates from instance
|
|
206
|
+
#
|
|
207
|
+
# @param instance [Hash] Instance data
|
|
208
|
+
# @param axes [Array] Variation axes
|
|
209
|
+
# @return [Hash<String, Float>] Coordinates
|
|
210
|
+
def build_instance_coordinates(instance, axes)
|
|
211
|
+
coords = {}
|
|
212
|
+
instance[:coordinates].each_with_index do |value, index|
|
|
213
|
+
next if index >= axes.length
|
|
214
|
+
|
|
215
|
+
axis = axes[index]
|
|
216
|
+
coords[axis.axis_tag] = value
|
|
217
|
+
end
|
|
218
|
+
coords
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Wrapper class for instance font tables
|
|
222
|
+
#
|
|
223
|
+
# Provides minimal interface needed by SvgGenerator while using
|
|
224
|
+
# instance tables instead of original font tables.
|
|
225
|
+
class InstanceFontWrapper
|
|
226
|
+
# @return [Hash<String, String>] Font tables
|
|
227
|
+
attr_reader :table_data
|
|
228
|
+
|
|
229
|
+
# Initialize wrapper
|
|
230
|
+
#
|
|
231
|
+
# @param original_font [Object] Original variable font
|
|
232
|
+
# @param instance_tables [Hash<String, String>] Instance tables
|
|
233
|
+
def initialize(original_font, instance_tables)
|
|
234
|
+
@original_font = original_font
|
|
235
|
+
@table_data = instance_tables
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get table by tag
|
|
239
|
+
#
|
|
240
|
+
# @param tag [String] Table tag
|
|
241
|
+
# @return [Object, nil] Table or nil
|
|
242
|
+
def table(tag)
|
|
243
|
+
# Use instance table if available, otherwise fall back to original
|
|
244
|
+
if @table_data.key?(tag)
|
|
245
|
+
end
|
|
246
|
+
@original_font.table(tag)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Check if table exists
|
|
250
|
+
#
|
|
251
|
+
# @param tag [String] Table tag
|
|
252
|
+
# @return [Boolean] True if table exists
|
|
253
|
+
def has_table?(tag)
|
|
254
|
+
@table_data.key?(tag) || @original_font.has_table?(tag)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Forward other methods to original font
|
|
258
|
+
def method_missing(method, ...)
|
|
259
|
+
@original_font.send(method, ...)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def respond_to_missing?(method, include_private = false)
|
|
263
|
+
@original_font.respond_to?(method, include_private) || super
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "interpolator"
|
|
4
|
+
require_relative "region_matcher"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Provides shared context for variation operations
|
|
9
|
+
#
|
|
10
|
+
# This class centralizes the initialization of common variation components
|
|
11
|
+
# (axes, interpolator, region matcher) that are needed by most variation
|
|
12
|
+
# operations. It ensures consistent initialization and validation.
|
|
13
|
+
#
|
|
14
|
+
# @example Creating a variation context
|
|
15
|
+
# context = VariationContext.new(font)
|
|
16
|
+
# context.validate!
|
|
17
|
+
# puts "Axes: #{context.axes.map(&:axis_tag)}"
|
|
18
|
+
#
|
|
19
|
+
# @example Using in a variation class
|
|
20
|
+
# class MyGenerator
|
|
21
|
+
# def initialize(font)
|
|
22
|
+
# @context = VariationContext.new(font)
|
|
23
|
+
# @context.validate!
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def generate
|
|
27
|
+
# @context.interpolator.normalize_coordinate(value, "wght")
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
class VariationContext
|
|
31
|
+
# @return [TrueTypeFont, OpenTypeFont] Font instance
|
|
32
|
+
attr_reader :font
|
|
33
|
+
|
|
34
|
+
# @return [Fvar, nil] fvar table
|
|
35
|
+
attr_reader :fvar
|
|
36
|
+
|
|
37
|
+
# @return [Array<VariationAxisRecord>] Variation axes
|
|
38
|
+
attr_reader :axes
|
|
39
|
+
|
|
40
|
+
# @return [Interpolator] Coordinate interpolator
|
|
41
|
+
attr_reader :interpolator
|
|
42
|
+
|
|
43
|
+
# @return [RegionMatcher] Region matcher
|
|
44
|
+
attr_reader :region_matcher
|
|
45
|
+
|
|
46
|
+
# Initialize variation context
|
|
47
|
+
#
|
|
48
|
+
# Loads fvar table and initializes all common variation components.
|
|
49
|
+
# Does not validate - call validate! explicitly if needed.
|
|
50
|
+
#
|
|
51
|
+
# @param font [TrueTypeFont, OpenTypeFont] Variable font
|
|
52
|
+
def initialize(font)
|
|
53
|
+
@font = font
|
|
54
|
+
@fvar = font.has_table?("fvar") ? font.table("fvar") : nil
|
|
55
|
+
@axes = @fvar ? @fvar.axes : []
|
|
56
|
+
@interpolator = Interpolator.new(@axes)
|
|
57
|
+
@region_matcher = RegionMatcher.new(@axes)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Validate that font is a proper variable font
|
|
61
|
+
#
|
|
62
|
+
# Checks for fvar table and axes definition. Raises errors if
|
|
63
|
+
# font is not a valid variable font.
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @raise [MissingVariationTableError] If fvar table missing
|
|
67
|
+
# @raise [InvalidVariationDataError] If no axes defined
|
|
68
|
+
#
|
|
69
|
+
# @example Validate before processing
|
|
70
|
+
# context = VariationContext.new(font)
|
|
71
|
+
# context.validate!
|
|
72
|
+
# # Safe to proceed
|
|
73
|
+
def validate!
|
|
74
|
+
unless @fvar
|
|
75
|
+
raise MissingVariationTableError.new(
|
|
76
|
+
table: "fvar",
|
|
77
|
+
message: "Font is not a variable font (missing fvar table)",
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if @axes.empty?
|
|
82
|
+
raise InvalidVariationDataError.new(
|
|
83
|
+
message: "Variable font has no axes defined in fvar table",
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Check if font is a variable font
|
|
89
|
+
#
|
|
90
|
+
# @return [Boolean] True if fvar table exists
|
|
91
|
+
def variable_font?
|
|
92
|
+
!@fvar.nil?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get number of axes
|
|
96
|
+
#
|
|
97
|
+
# @return [Integer] Axis count
|
|
98
|
+
def axis_count
|
|
99
|
+
@axes.length
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Find axis by tag
|
|
103
|
+
#
|
|
104
|
+
# @param axis_tag [String] Axis tag (e.g., "wght", "wdth")
|
|
105
|
+
# @return [VariationAxisRecord, nil] Axis or nil if not found
|
|
106
|
+
#
|
|
107
|
+
# @example Find weight axis
|
|
108
|
+
# wght_axis = context.find_axis("wght")
|
|
109
|
+
# puts "Range: #{wght_axis.min_value} - #{wght_axis.max_value}"
|
|
110
|
+
def find_axis(axis_tag)
|
|
111
|
+
@axes.find { |axis| axis.axis_tag == axis_tag }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get axis tags
|
|
115
|
+
#
|
|
116
|
+
# @return [Array<String>] Array of axis tags
|
|
117
|
+
def axis_tags
|
|
118
|
+
@axes.map(&:axis_tag)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Validate coordinates against axes
|
|
122
|
+
#
|
|
123
|
+
# Checks that all coordinate values are within valid axis ranges.
|
|
124
|
+
#
|
|
125
|
+
# @param coordinates [Hash<String, Float>] Design space coordinates
|
|
126
|
+
# @return [void]
|
|
127
|
+
# @raise [InvalidCoordinatesError] If any coordinate out of range
|
|
128
|
+
#
|
|
129
|
+
# @example Validate coordinates
|
|
130
|
+
# context.validate_coordinates({ "wght" => 700 })
|
|
131
|
+
def validate_coordinates(coordinates)
|
|
132
|
+
coordinates.each do |axis_tag, value|
|
|
133
|
+
axis = find_axis(axis_tag)
|
|
134
|
+
|
|
135
|
+
unless axis
|
|
136
|
+
raise InvalidCoordinatesError.new(
|
|
137
|
+
axis: axis_tag,
|
|
138
|
+
value: value,
|
|
139
|
+
range: [],
|
|
140
|
+
message: "Unknown axis '#{axis_tag}'",
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if value < axis.min_value || value > axis.max_value
|
|
145
|
+
raise InvalidCoordinatesError.new(
|
|
146
|
+
axis: axis_tag,
|
|
147
|
+
value: value,
|
|
148
|
+
range: [axis.min_value, axis.max_value],
|
|
149
|
+
message: "Coordinate #{value} for axis '#{axis_tag}' outside valid range [#{axis.min_value}, #{axis.max_value}]",
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get default coordinates
|
|
156
|
+
#
|
|
157
|
+
# Returns coordinates at default values for all axes.
|
|
158
|
+
#
|
|
159
|
+
# @return [Hash<String, Float>] Default coordinates
|
|
160
|
+
def default_coordinates
|
|
161
|
+
coordinates = {}
|
|
162
|
+
@axes.each do |axis|
|
|
163
|
+
coordinates[axis.axis_tag] = axis.default_value
|
|
164
|
+
end
|
|
165
|
+
coordinates
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Normalize coordinates to [-1, 1] range
|
|
169
|
+
#
|
|
170
|
+
# Convenience method that delegates to interpolator.
|
|
171
|
+
#
|
|
172
|
+
# @param coordinates [Hash<String, Float>] User-space coordinates
|
|
173
|
+
# @return [Hash<String, Float>] Normalized coordinates
|
|
174
|
+
def normalize_coordinates(coordinates)
|
|
175
|
+
@interpolator.normalize_coordinates(coordinates)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Get variation type
|
|
179
|
+
#
|
|
180
|
+
# Determines whether font uses TrueType (gvar) or PostScript (CFF2)
|
|
181
|
+
# variation format.
|
|
182
|
+
#
|
|
183
|
+
# @return [Symbol] :truetype, :postscript, or :none
|
|
184
|
+
def variation_type
|
|
185
|
+
if @font.has_table?("CFF2")
|
|
186
|
+
:postscript
|
|
187
|
+
elsif @font.has_table?("gvar")
|
|
188
|
+
:truetype
|
|
189
|
+
else
|
|
190
|
+
:none
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check if font has glyph variations
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean] True if gvar or CFF2 present
|
|
197
|
+
def has_glyph_variations?
|
|
198
|
+
@font.has_table?("gvar") || @font.has_table?("CFF2")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Check if font has metrics variations
|
|
202
|
+
#
|
|
203
|
+
# @return [Boolean] True if HVAR, VVAR, or MVAR present
|
|
204
|
+
def has_metrics_variations?
|
|
205
|
+
@font.has_table?("HVAR") ||
|
|
206
|
+
@font.has_table?("VVAR") ||
|
|
207
|
+
@font.has_table?("MVAR")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|