fontisan 0.2.0 → 0.2.2
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 +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -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 +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
class Cff2
|
|
6
|
+
# Region matcher for calculating variation scalars
|
|
7
|
+
#
|
|
8
|
+
# Maps design space coordinates to region scalars based on
|
|
9
|
+
# the Variable Store region definitions. Each region defines
|
|
10
|
+
# a range (start, peak, end) for each variation axis.
|
|
11
|
+
#
|
|
12
|
+
# Scalar Calculation:
|
|
13
|
+
# - If coordinate is at peak: scalar = 1.0
|
|
14
|
+
# - If coordinate is between start and peak: linear interpolation
|
|
15
|
+
# - If coordinate is between peak and end: linear interpolation
|
|
16
|
+
# - If coordinate is outside [start, end]: scalar = 0.0
|
|
17
|
+
#
|
|
18
|
+
# Reference: OpenType Font Variations Overview
|
|
19
|
+
# Reference: Adobe Technical Note #5177 (CFF2)
|
|
20
|
+
#
|
|
21
|
+
# @example Calculating scalars
|
|
22
|
+
# matcher = RegionMatcher.new(regions)
|
|
23
|
+
# scalars = matcher.calculate_scalars({ "wght" => 0.5, "wdth" => 0.3 })
|
|
24
|
+
class RegionMatcher
|
|
25
|
+
# @return [Array<Hash>] Regions from Variable Store
|
|
26
|
+
attr_reader :regions
|
|
27
|
+
|
|
28
|
+
# Initialize matcher with regions
|
|
29
|
+
#
|
|
30
|
+
# @param regions [Array<Hash>] Region definitions from Variable Store
|
|
31
|
+
def initialize(regions)
|
|
32
|
+
@regions = regions
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Calculate scalars for all regions at given coordinates
|
|
36
|
+
#
|
|
37
|
+
# Coordinates are normalized values in the range [-1.0, 1.0]
|
|
38
|
+
# where 0.0 represents the default/regular style.
|
|
39
|
+
#
|
|
40
|
+
# @param coordinates [Array<Float>] Normalized coordinates per axis
|
|
41
|
+
# @return [Array<Float>] Scalars for each region
|
|
42
|
+
def calculate_scalars(coordinates)
|
|
43
|
+
@regions.map do |region|
|
|
44
|
+
calculate_region_scalar(region, coordinates)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Calculate scalar for a single region
|
|
49
|
+
#
|
|
50
|
+
# The scalar is the product of scalars for all axes in the region.
|
|
51
|
+
# If any axis has scalar 0.0, the entire region scalar is 0.0.
|
|
52
|
+
#
|
|
53
|
+
# @param region [Hash] Region definition
|
|
54
|
+
# @param coordinates [Array<Float>] Normalized coordinates per axis
|
|
55
|
+
# @return [Float] Scalar for the region (0.0 to 1.0)
|
|
56
|
+
def calculate_region_scalar(region, coordinates)
|
|
57
|
+
axes = region[:axes]
|
|
58
|
+
|
|
59
|
+
# Multiply scalars for all axes
|
|
60
|
+
scalar = 1.0
|
|
61
|
+
axes.each_with_index do |axis, i|
|
|
62
|
+
coord = coordinates[i] || 0.0
|
|
63
|
+
axis_scalar = calculate_axis_scalar(axis, coord)
|
|
64
|
+
scalar *= axis_scalar
|
|
65
|
+
|
|
66
|
+
# Early exit if any axis is out of range
|
|
67
|
+
return 0.0 if axis_scalar.zero?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
scalar
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Calculate scalar for a single axis
|
|
74
|
+
#
|
|
75
|
+
# @param axis [Hash] Axis definition with :start_coord, :peak_coord, :end_coord
|
|
76
|
+
# @param coordinate [Float] Normalized coordinate for this axis
|
|
77
|
+
# @return [Float] Scalar for this axis (0.0 to 1.0)
|
|
78
|
+
def calculate_axis_scalar(axis, coordinate)
|
|
79
|
+
start_coord = axis[:start_coord]
|
|
80
|
+
peak_coord = axis[:peak_coord]
|
|
81
|
+
end_coord = axis[:end_coord]
|
|
82
|
+
|
|
83
|
+
# Outside the region
|
|
84
|
+
return 0.0 if coordinate < start_coord || coordinate > end_coord
|
|
85
|
+
|
|
86
|
+
# At or beyond peak
|
|
87
|
+
return 1.0 if coordinate == peak_coord
|
|
88
|
+
|
|
89
|
+
# Between start and peak
|
|
90
|
+
if coordinate < peak_coord
|
|
91
|
+
# Linear interpolation: (coord - start) / (peak - start)
|
|
92
|
+
range = peak_coord - start_coord
|
|
93
|
+
return 1.0 if range.zero? # Avoid division by zero
|
|
94
|
+
|
|
95
|
+
(coordinate - start_coord) / range
|
|
96
|
+
else
|
|
97
|
+
# Between peak and end
|
|
98
|
+
# Linear interpolation: (end - coord) / (end - peak)
|
|
99
|
+
range = end_coord - peak_coord
|
|
100
|
+
return 1.0 if range.zero? # Avoid division by zero
|
|
101
|
+
|
|
102
|
+
(end_coord - coordinate) / range
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if coordinates are within any region
|
|
107
|
+
#
|
|
108
|
+
# @param coordinates [Array<Float>] Normalized coordinates
|
|
109
|
+
# @return [Boolean] True if coordinates activate any region
|
|
110
|
+
def coordinates_active?(coordinates)
|
|
111
|
+
scalars = calculate_scalars(coordinates)
|
|
112
|
+
scalars.any?(&:positive?)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get active regions for coordinates
|
|
116
|
+
#
|
|
117
|
+
# Returns indices of regions that have non-zero scalars
|
|
118
|
+
#
|
|
119
|
+
# @param coordinates [Array<Float>] Normalized coordinates
|
|
120
|
+
# @return [Array<Integer>] Indices of active regions
|
|
121
|
+
def active_regions(coordinates)
|
|
122
|
+
scalars = calculate_scalars(coordinates)
|
|
123
|
+
scalars.each_with_index.select { |scalar, _| scalar.positive? }
|
|
124
|
+
.map(&:last)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get scalar for specific region index
|
|
128
|
+
#
|
|
129
|
+
# @param region_index [Integer] Region index
|
|
130
|
+
# @param coordinates [Array<Float>] Normalized coordinates
|
|
131
|
+
# @return [Float, nil] Scalar for the region, or nil if index invalid
|
|
132
|
+
def scalar_for_region(region_index, coordinates)
|
|
133
|
+
return nil if region_index >= @regions.size
|
|
134
|
+
|
|
135
|
+
region = @regions[region_index]
|
|
136
|
+
calculate_region_scalar(region, coordinates)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Validate region structure
|
|
140
|
+
#
|
|
141
|
+
# @return [Array<String>] Array of validation errors (empty if valid)
|
|
142
|
+
def validate
|
|
143
|
+
errors = []
|
|
144
|
+
|
|
145
|
+
@regions.each_with_index do |region, i|
|
|
146
|
+
axes = region[:axes]
|
|
147
|
+
unless axes.is_a?(Array)
|
|
148
|
+
errors << "Region #{i} has invalid axes (not an array)"
|
|
149
|
+
next
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
axes.each_with_index do |axis, j|
|
|
153
|
+
unless axis.is_a?(Hash)
|
|
154
|
+
errors << "Region #{i}, axis #{j} is not a hash"
|
|
155
|
+
next
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check required keys
|
|
159
|
+
%i[start_coord peak_coord end_coord].each do |key|
|
|
160
|
+
unless axis.key?(key)
|
|
161
|
+
errors << "Region #{i}, axis #{j} missing #{key}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Validate coordinate ordering
|
|
166
|
+
if axis[:start_coord] && axis[:peak_coord] && axis[:end_coord]
|
|
167
|
+
start = axis[:start_coord]
|
|
168
|
+
peak = axis[:peak_coord]
|
|
169
|
+
ending = axis[:end_coord]
|
|
170
|
+
|
|
171
|
+
unless start <= peak && peak <= ending
|
|
172
|
+
errors << "Region #{i}, axis #{j} has invalid ordering: " \
|
|
173
|
+
"#{start} > #{peak} > #{ending}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
errors
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get number of axes from first region
|
|
183
|
+
#
|
|
184
|
+
# @return [Integer] Number of axes
|
|
185
|
+
def axis_count
|
|
186
|
+
return 0 if @regions.empty?
|
|
187
|
+
|
|
188
|
+
@regions.first[:axis_count] || @regions.first[:axes]&.size || 0
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Check if matcher has regions
|
|
192
|
+
#
|
|
193
|
+
# @return [Boolean] True if regions are present
|
|
194
|
+
def has_regions?
|
|
195
|
+
!@regions.empty?
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|