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,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../font_writer"
|
|
4
|
+
require_relative "../converters/outline_converter"
|
|
5
|
+
require_relative "../converters/woff_writer"
|
|
6
|
+
require_relative "../error"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Variation
|
|
10
|
+
# Writes generated static font instances to files in various formats
|
|
11
|
+
#
|
|
12
|
+
# [`InstanceWriter`](lib/fontisan/variation/instance_writer.rb) takes
|
|
13
|
+
# instance tables generated by
|
|
14
|
+
# [`InstanceGenerator`](lib/fontisan/variation/instance_generator.rb) and
|
|
15
|
+
# writes them to files in the desired output format. It handles:
|
|
16
|
+
# - Format detection from file extension
|
|
17
|
+
# - Format conversion when needed (e.g., glyf → CFF for OTF)
|
|
18
|
+
# - SFNT version selection based on output format
|
|
19
|
+
# - Integration with FontWriter for binary output
|
|
20
|
+
# - Integration with OutlineConverter for format conversion
|
|
21
|
+
# - Integration with WoffWriter for WOFF packaging
|
|
22
|
+
#
|
|
23
|
+
# **Supported Output Formats:**
|
|
24
|
+
# - TTF (TrueType with glyf outlines)
|
|
25
|
+
# - OTF (OpenType with CFF outlines)
|
|
26
|
+
# - WOFF (Web Open Font Format)
|
|
27
|
+
# - WOFF2 (Web Open Font Format 2.0, future)
|
|
28
|
+
#
|
|
29
|
+
# @example Write instance to TTF
|
|
30
|
+
# tables = generator.generate
|
|
31
|
+
# InstanceWriter.write(tables, 'bold.ttf')
|
|
32
|
+
#
|
|
33
|
+
# @example Write instance to OTF with format conversion
|
|
34
|
+
# tables = generator.generate # from variable TTF
|
|
35
|
+
# InstanceWriter.write(tables, 'bold.otf', source_format: :ttf)
|
|
36
|
+
#
|
|
37
|
+
# @example Write instance to WOFF
|
|
38
|
+
# tables = generator.generate
|
|
39
|
+
# InstanceWriter.write(tables, 'bold.woff')
|
|
40
|
+
class InstanceWriter
|
|
41
|
+
# Supported output formats
|
|
42
|
+
SUPPORTED_FORMATS = %i[ttf otf woff woff2].freeze
|
|
43
|
+
|
|
44
|
+
# SFNT version constants
|
|
45
|
+
SFNT_VERSION_TRUETYPE = 0x00010000 # TrueType with glyf
|
|
46
|
+
SFNT_VERSION_CFF = 0x4F54544F # 'OTTO' for CFF
|
|
47
|
+
|
|
48
|
+
# Write instance tables to file
|
|
49
|
+
#
|
|
50
|
+
# @param tables [Hash<String, String>] Instance tables from
|
|
51
|
+
# InstanceGenerator
|
|
52
|
+
# @param output_path [String] Output file path
|
|
53
|
+
# @param options [Hash] Options
|
|
54
|
+
# @option options [Symbol] :format Output format (:ttf, :otf, :woff,
|
|
55
|
+
# :woff2)
|
|
56
|
+
# @option options [Symbol] :source_format Source format before instancing
|
|
57
|
+
# (:ttf or :otf)
|
|
58
|
+
# @option options [Boolean] :optimize Enable CFF optimization for OTF
|
|
59
|
+
# (default: false)
|
|
60
|
+
# @option options [Integer] :sfnt_version Override SFNT version
|
|
61
|
+
# @return [Integer] Number of bytes written
|
|
62
|
+
# @raise [ArgumentError] If parameters are invalid
|
|
63
|
+
# @raise [Error] If format conversion fails
|
|
64
|
+
def self.write(tables, output_path, options = {})
|
|
65
|
+
new(tables, options).write(output_path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Hash<String, String>] Instance tables
|
|
69
|
+
attr_reader :tables
|
|
70
|
+
|
|
71
|
+
# @return [Hash] Writer options
|
|
72
|
+
attr_reader :options
|
|
73
|
+
|
|
74
|
+
# Initialize writer with instance tables
|
|
75
|
+
#
|
|
76
|
+
# @param tables [Hash<String, String>] Instance tables from
|
|
77
|
+
# InstanceGenerator
|
|
78
|
+
# @param options [Hash] Writer options
|
|
79
|
+
# @option options [Symbol] :source_format Source format before instancing
|
|
80
|
+
# @option options [Boolean] :optimize Enable CFF optimization
|
|
81
|
+
def initialize(tables, options = {})
|
|
82
|
+
@tables = tables
|
|
83
|
+
@options = options
|
|
84
|
+
validate_tables!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Write instance to file
|
|
88
|
+
#
|
|
89
|
+
# @param output_path [String] Output file path
|
|
90
|
+
# @return [Integer] Number of bytes written
|
|
91
|
+
def write(output_path)
|
|
92
|
+
# Detect output format
|
|
93
|
+
format = detect_output_format(output_path)
|
|
94
|
+
validate_format!(format)
|
|
95
|
+
|
|
96
|
+
# Detect source format from tables
|
|
97
|
+
source_format = detect_source_format(@tables)
|
|
98
|
+
|
|
99
|
+
# Convert format if needed
|
|
100
|
+
output_tables = if format_conversion_needed?(source_format, format)
|
|
101
|
+
convert_format(source_format, format)
|
|
102
|
+
else
|
|
103
|
+
@tables
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Write to file based on format
|
|
107
|
+
case format
|
|
108
|
+
when :ttf, :otf
|
|
109
|
+
write_sfnt(output_tables, output_path, format)
|
|
110
|
+
when :woff
|
|
111
|
+
write_woff(output_tables, output_path, source_format)
|
|
112
|
+
when :woff2
|
|
113
|
+
raise Fontisan::Error,
|
|
114
|
+
"WOFF2 output not yet implemented (planned for Phase 6)"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Validate instance tables
|
|
121
|
+
#
|
|
122
|
+
# @raise [ArgumentError] If tables are invalid
|
|
123
|
+
def validate_tables!
|
|
124
|
+
raise ArgumentError, "Tables cannot be nil" if @tables.nil?
|
|
125
|
+
|
|
126
|
+
unless @tables.is_a?(Hash)
|
|
127
|
+
raise ArgumentError,
|
|
128
|
+
"Tables must be a Hash, got: #{@tables.class}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if @tables.empty?
|
|
132
|
+
raise ArgumentError, "Tables cannot be empty"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Check for required tables
|
|
136
|
+
required_tables = %w[head hhea maxp]
|
|
137
|
+
required_tables.each do |tag|
|
|
138
|
+
unless @tables.key?(tag)
|
|
139
|
+
raise ArgumentError, "Missing required table: #{tag}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Detect output format from file path
|
|
145
|
+
#
|
|
146
|
+
# @param path [String] Output file path
|
|
147
|
+
# @return [Symbol] Format (:ttf, :otf, :woff, :woff2)
|
|
148
|
+
def detect_output_format(path)
|
|
149
|
+
return @options[:format] if @options[:format]
|
|
150
|
+
|
|
151
|
+
ext = File.extname(path).downcase
|
|
152
|
+
case ext
|
|
153
|
+
when ".ttf" then :ttf
|
|
154
|
+
when ".otf" then :otf
|
|
155
|
+
when ".woff" then :woff
|
|
156
|
+
when ".woff2" then :woff2
|
|
157
|
+
else
|
|
158
|
+
raise ArgumentError,
|
|
159
|
+
"Cannot determine format from extension: #{ext}. " \
|
|
160
|
+
"Supported: .ttf, .otf, .woff, .woff2"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Validate output format
|
|
165
|
+
#
|
|
166
|
+
# @param format [Symbol] Format to validate
|
|
167
|
+
# @raise [ArgumentError] If format is not supported
|
|
168
|
+
def validate_format!(format)
|
|
169
|
+
unless SUPPORTED_FORMATS.include?(format)
|
|
170
|
+
raise ArgumentError,
|
|
171
|
+
"Unsupported format: #{format}. " \
|
|
172
|
+
"Supported: #{SUPPORTED_FORMATS.join(', ')}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Detect source format from instance tables
|
|
177
|
+
#
|
|
178
|
+
# @param tables [Hash<String, String>] Instance tables
|
|
179
|
+
# @return [Symbol] Source format (:ttf or :otf)
|
|
180
|
+
def detect_source_format(tables)
|
|
181
|
+
# Check for outline tables
|
|
182
|
+
if tables.key?("CFF ") || tables.key?("CFF2")
|
|
183
|
+
:otf
|
|
184
|
+
elsif tables.key?("glyf")
|
|
185
|
+
:ttf
|
|
186
|
+
else
|
|
187
|
+
# If no outline tables, use option or default to TTF
|
|
188
|
+
@options[:source_format] || :ttf
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Check if format conversion is needed
|
|
193
|
+
#
|
|
194
|
+
# @param source_format [Symbol] Source format
|
|
195
|
+
# @param target_format [Symbol] Target format
|
|
196
|
+
# @return [Boolean] True if conversion needed
|
|
197
|
+
def format_conversion_needed?(source_format, target_format)
|
|
198
|
+
# WOFF doesn't need outline conversion
|
|
199
|
+
return false if %i[woff woff2].include?(target_format)
|
|
200
|
+
|
|
201
|
+
# Check if outline formats differ
|
|
202
|
+
source_format != target_format
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Convert instance tables from source format to target format
|
|
206
|
+
#
|
|
207
|
+
# @param source_format [Symbol] Source format
|
|
208
|
+
# @param target_format [Symbol] Target format
|
|
209
|
+
# @return [Hash<String, String>] Converted tables
|
|
210
|
+
# @raise [Error] If conversion fails
|
|
211
|
+
def convert_format(source_format, target_format)
|
|
212
|
+
# Create temporary font object for conversion
|
|
213
|
+
temp_font = create_temp_font(@tables, source_format)
|
|
214
|
+
|
|
215
|
+
# Use OutlineConverter for format conversion
|
|
216
|
+
converter = Converters::OutlineConverter.new
|
|
217
|
+
converter.convert(
|
|
218
|
+
temp_font,
|
|
219
|
+
target_format: target_format,
|
|
220
|
+
optimize_cff: @options[:optimize] || false,
|
|
221
|
+
)
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
raise Fontisan::Error,
|
|
224
|
+
"Failed to convert instance from #{source_format} to " \
|
|
225
|
+
"#{target_format}: #{e.message}"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Create temporary font object from tables
|
|
229
|
+
#
|
|
230
|
+
# @param tables [Hash<String, String>] Font tables
|
|
231
|
+
# @param format [Symbol] Font format
|
|
232
|
+
# @return [Object] Font object
|
|
233
|
+
def create_temp_font(tables, format)
|
|
234
|
+
# Create minimal font object that responds to required methods
|
|
235
|
+
font_class = format == :otf ? OpenTypeFont : TrueTypeFont
|
|
236
|
+
font = font_class.new
|
|
237
|
+
|
|
238
|
+
# Set table data
|
|
239
|
+
font.instance_variable_set(:@table_data, tables)
|
|
240
|
+
|
|
241
|
+
# Define required methods
|
|
242
|
+
font.define_singleton_method(:table_data) { tables }
|
|
243
|
+
font.define_singleton_method(:table_names) { tables.keys }
|
|
244
|
+
font.define_singleton_method(:has_table?) { |tag| tables.key?(tag) }
|
|
245
|
+
font.define_singleton_method(:table) do |tag|
|
|
246
|
+
# Return nil if table doesn't exist
|
|
247
|
+
return nil unless tables.key?(tag)
|
|
248
|
+
|
|
249
|
+
# Parse and return table object
|
|
250
|
+
# For conversion, we need to lazy-load tables
|
|
251
|
+
parse_table(tag, tables[tag])
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
font
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Parse table data into table object
|
|
258
|
+
#
|
|
259
|
+
# @param tag [String] Table tag
|
|
260
|
+
# @param data [String] Table binary data
|
|
261
|
+
# @return [Object] Parsed table object
|
|
262
|
+
def parse_table(tag, data)
|
|
263
|
+
# For OutlineConverter, we need head, maxp, loca, glyf for TTF
|
|
264
|
+
# and CFF for OTF
|
|
265
|
+
case tag
|
|
266
|
+
when "head"
|
|
267
|
+
Tables::Head.new.tap { |t| t.parse(data) }
|
|
268
|
+
when "maxp"
|
|
269
|
+
Tables::Maxp.new.tap { |t| t.parse(data) }
|
|
270
|
+
when "loca"
|
|
271
|
+
Tables::Loca.new.tap { |t| t.data = data }
|
|
272
|
+
when "glyf"
|
|
273
|
+
Tables::Glyf.new.tap { |t| t.data = data }
|
|
274
|
+
when "CFF "
|
|
275
|
+
Tables::Cff.new.tap { |t| t.parse(data) }
|
|
276
|
+
when "CFF2"
|
|
277
|
+
Tables::Cff2.new.tap { |t| t.parse(data) }
|
|
278
|
+
else
|
|
279
|
+
# For other tables, return a simple object that just holds data
|
|
280
|
+
Object.new.tap do |obj|
|
|
281
|
+
obj.define_singleton_method(:data) { data }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
rescue StandardError => e
|
|
285
|
+
warn "Warning: Failed to parse #{tag} table: #{e.message}"
|
|
286
|
+
nil
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Write SFNT format (TTF or OTF)
|
|
290
|
+
#
|
|
291
|
+
# @param tables [Hash<String, String>] Output tables
|
|
292
|
+
# @param output_path [String] Output file path
|
|
293
|
+
# @param format [Symbol] Output format
|
|
294
|
+
# @return [Integer] Number of bytes written
|
|
295
|
+
def write_sfnt(tables, output_path, format)
|
|
296
|
+
# Determine SFNT version
|
|
297
|
+
sfnt_version = @options[:sfnt_version] || sfnt_version_for_format(
|
|
298
|
+
format,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Write using FontWriter
|
|
302
|
+
FontWriter.write_to_file(tables, output_path,
|
|
303
|
+
sfnt_version: sfnt_version)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Write WOFF format
|
|
307
|
+
#
|
|
308
|
+
# @param tables [Hash<String, String>] Output tables
|
|
309
|
+
# @param output_path [String] Output file path
|
|
310
|
+
# @param source_format [Symbol] Source format (for flavor detection)
|
|
311
|
+
# @return [Integer] Number of bytes written
|
|
312
|
+
def write_woff(tables, output_path, source_format)
|
|
313
|
+
# Create temporary font for WOFF writer
|
|
314
|
+
temp_font = create_temp_font(tables, source_format)
|
|
315
|
+
|
|
316
|
+
# Add cff? method for WoffWriter flavor detection
|
|
317
|
+
temp_font.define_singleton_method(:cff?) do
|
|
318
|
+
tables.key?("CFF ") || tables.key?("CFF2")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Use WoffWriter to create WOFF
|
|
322
|
+
writer = Converters::WoffWriter.new
|
|
323
|
+
woff_data = writer.convert(temp_font)
|
|
324
|
+
|
|
325
|
+
# Write to file
|
|
326
|
+
File.binwrite(output_path, woff_data)
|
|
327
|
+
rescue StandardError => e
|
|
328
|
+
raise Fontisan::Error,
|
|
329
|
+
"Failed to write WOFF output: #{e.message}"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Get SFNT version for output format
|
|
333
|
+
#
|
|
334
|
+
# @param format [Symbol] Output format
|
|
335
|
+
# @return [Integer] SFNT version constant
|
|
336
|
+
def sfnt_version_for_format(format)
|
|
337
|
+
format == :otf ? SFNT_VERSION_CFF : SFNT_VERSION_TRUETYPE
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -209,9 +209,12 @@ module Fontisan
|
|
|
209
209
|
coords2 = r2.region_axes[i]
|
|
210
210
|
|
|
211
211
|
# Compare start, peak, end coordinates
|
|
212
|
-
return false unless coords_similar?(coords1.start_coord,
|
|
213
|
-
|
|
214
|
-
return false unless coords_similar?(coords1.
|
|
212
|
+
return false unless coords_similar?(coords1.start_coord,
|
|
213
|
+
coords2.start_coord)
|
|
214
|
+
return false unless coords_similar?(coords1.peak_coord,
|
|
215
|
+
coords2.peak_coord)
|
|
216
|
+
return false unless coords_similar?(coords1.end_coord,
|
|
217
|
+
coords2.end_coord)
|
|
215
218
|
end
|
|
216
219
|
|
|
217
220
|
true
|
|
@@ -112,7 +112,10 @@ module Fontisan
|
|
|
112
112
|
validate_input if @options[:validate]
|
|
113
113
|
|
|
114
114
|
fvar = variation_table("fvar")
|
|
115
|
-
|
|
115
|
+
unless fvar
|
|
116
|
+
return { tables: @font.table_data.dup,
|
|
117
|
+
report: { error: "No fvar table" } }
|
|
118
|
+
end
|
|
116
119
|
|
|
117
120
|
# Find axes to keep
|
|
118
121
|
all_axes = fvar.axes
|
|
@@ -175,7 +178,8 @@ module Fontisan
|
|
|
175
178
|
optimizer = Optimizer.new(cff2, region_threshold: threshold)
|
|
176
179
|
optimizer.optimize
|
|
177
180
|
|
|
178
|
-
@report[:regions_deduplicated] =
|
|
181
|
+
@report[:regions_deduplicated] =
|
|
182
|
+
optimizer.stats[:regions_deduplicated]
|
|
179
183
|
@report[:cff2_optimized] = true
|
|
180
184
|
end
|
|
181
185
|
|
|
@@ -316,8 +320,14 @@ module Fontisan
|
|
|
316
320
|
# @param tables [Hash] Font tables
|
|
317
321
|
# @param glyph_ids [Array<Integer>] Glyph IDs to keep
|
|
318
322
|
def subset_metrics_variations(tables, glyph_ids)
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
if has_variation_table?("HVAR")
|
|
324
|
+
subset_metrics_table(tables, "HVAR",
|
|
325
|
+
glyph_ids)
|
|
326
|
+
end
|
|
327
|
+
if has_variation_table?("VVAR")
|
|
328
|
+
subset_metrics_table(tables, "VVAR",
|
|
329
|
+
glyph_ids)
|
|
330
|
+
end
|
|
321
331
|
# MVAR is font-wide, no glyph subsetting needed
|
|
322
332
|
end
|
|
323
333
|
|
|
@@ -333,7 +343,8 @@ module Fontisan
|
|
|
333
343
|
# 3. Remove unused ItemVariationData
|
|
334
344
|
# 4. Rebuild and serialize
|
|
335
345
|
|
|
336
|
-
@report[:"#{table_tag.downcase}_note"] =
|
|
346
|
+
@report[:"#{table_tag.downcase}_note"] =
|
|
347
|
+
"#{table_tag} subsetting not yet implemented"
|
|
337
348
|
end
|
|
338
349
|
|
|
339
350
|
# Update non-variation glyph tables
|
|
@@ -396,9 +407,18 @@ module Fontisan
|
|
|
396
407
|
# @param tables [Hash] Font tables
|
|
397
408
|
# @param keep_indices [Array<Integer>] Axis indices to keep
|
|
398
409
|
def subset_metrics_axes(tables, keep_indices)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
410
|
+
if has_variation_table?("HVAR")
|
|
411
|
+
subset_metrics_table_axes(tables, "HVAR",
|
|
412
|
+
keep_indices)
|
|
413
|
+
end
|
|
414
|
+
if has_variation_table?("VVAR")
|
|
415
|
+
subset_metrics_table_axes(tables, "VVAR",
|
|
416
|
+
keep_indices)
|
|
417
|
+
end
|
|
418
|
+
if has_variation_table?("MVAR")
|
|
419
|
+
subset_metrics_table_axes(tables, "MVAR",
|
|
420
|
+
keep_indices)
|
|
421
|
+
end
|
|
402
422
|
end
|
|
403
423
|
|
|
404
424
|
# Subset a single metrics table's axes
|
|
@@ -412,7 +432,8 @@ module Fontisan
|
|
|
412
432
|
# 2. Filter ItemVariationStore regions to keep axis indices
|
|
413
433
|
# 3. Rebuild and serialize
|
|
414
434
|
|
|
415
|
-
@report[:"#{table_tag.downcase}_axes_note"] =
|
|
435
|
+
@report[:"#{table_tag.downcase}_axes_note"] =
|
|
436
|
+
"#{table_tag} axis subsetting not yet implemented"
|
|
416
437
|
end
|
|
417
438
|
|
|
418
439
|
# Simplify metrics table regions
|
|
@@ -426,7 +447,8 @@ module Fontisan
|
|
|
426
447
|
# 3. Update delta set indices
|
|
427
448
|
# 4. Serialize back to binary
|
|
428
449
|
|
|
429
|
-
@report[:metrics_simplify_note] =
|
|
450
|
+
@report[:metrics_simplify_note] =
|
|
451
|
+
"Metrics region simplification not yet implemented"
|
|
430
452
|
end
|
|
431
453
|
|
|
432
454
|
# Create temporary font wrapper for validation
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../binary/base_record"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Variation
|
|
7
|
+
# Tuple variation header structure
|
|
8
|
+
#
|
|
9
|
+
# Used by both gvar and cvar tables to describe variation tuples.
|
|
10
|
+
# Each tuple header contains metadata about peak coordinates,
|
|
11
|
+
# intermediate regions, and point number handling.
|
|
12
|
+
class TupleVariationHeader < Binary::BaseRecord
|
|
13
|
+
uint16 :variation_data_size
|
|
14
|
+
uint16 :tuple_index
|
|
15
|
+
|
|
16
|
+
# Tuple index flags
|
|
17
|
+
EMBEDDED_PEAK_TUPLE = 0x8000
|
|
18
|
+
INTERMEDIATE_REGION = 0x4000
|
|
19
|
+
PRIVATE_POINT_NUMBERS = 0x2000
|
|
20
|
+
TUPLE_INDEX_MASK = 0x0FFF
|
|
21
|
+
|
|
22
|
+
# Check if tuple has embedded peak coordinates
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean] True if embedded
|
|
25
|
+
def embedded_peak_tuple?
|
|
26
|
+
(tuple_index & EMBEDDED_PEAK_TUPLE) != 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Check if tuple has intermediate region
|
|
30
|
+
#
|
|
31
|
+
# @return [Boolean] True if intermediate region
|
|
32
|
+
def intermediate_region?
|
|
33
|
+
(tuple_index & INTERMEDIATE_REGION) != 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if tuple has private point numbers
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] True if private points
|
|
39
|
+
def private_point_numbers?
|
|
40
|
+
(tuple_index & PRIVATE_POINT_NUMBERS) != 0
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get shared tuple index
|
|
44
|
+
#
|
|
45
|
+
# @return [Integer] Tuple index
|
|
46
|
+
def shared_tuple_index
|
|
47
|
+
tuple_index & TUPLE_INDEX_MASK
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|