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,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "variation_context"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Variation
|
|
8
|
+
# Preserves variation data when converting between compatible font formats
|
|
9
|
+
#
|
|
10
|
+
# [`VariationPreserver`](lib/fontisan/variation/variation_preserver.rb)
|
|
11
|
+
# copies variation tables from source to target font during format
|
|
12
|
+
# conversion. It handles:
|
|
13
|
+
# - Common variation tables (fvar, avar, STAT) - shared by all variable fonts
|
|
14
|
+
# - Format-specific tables (gvar for TTF, CFF2 for OTF)
|
|
15
|
+
# - Metrics variation tables (HVAR, VVAR, MVAR)
|
|
16
|
+
# - Table checksum updates
|
|
17
|
+
# - Validation of table consistency
|
|
18
|
+
#
|
|
19
|
+
# **Use Cases:**
|
|
20
|
+
#
|
|
21
|
+
# 1. **Variable TTF → Variable WOFF**: Preserve all gvar-based variation
|
|
22
|
+
# 2. **Variable OTF → Variable WOFF**: Preserve all CFF2-based variation
|
|
23
|
+
# 3. **Variable TTF → Variable OTF**: Copy common tables (fvar, avar, STAT)
|
|
24
|
+
# but variation data conversion handled by Converter
|
|
25
|
+
# 4. **Variable OTF → Variable TTF**: Copy common tables (fvar, avar, STAT)
|
|
26
|
+
# but variation data conversion handled by Converter
|
|
27
|
+
#
|
|
28
|
+
# **Preserved Tables:**
|
|
29
|
+
#
|
|
30
|
+
# Common (all variable fonts):
|
|
31
|
+
# - fvar (Font Variations)
|
|
32
|
+
# - avar (Axis Variations, optional)
|
|
33
|
+
# - STAT (Style Attributes)
|
|
34
|
+
#
|
|
35
|
+
# TrueType-specific:
|
|
36
|
+
# - gvar (Glyph Variations)
|
|
37
|
+
# - cvar (CVT Variations, optional)
|
|
38
|
+
#
|
|
39
|
+
# CFF2-specific:
|
|
40
|
+
# - CFF2 (with blend operators)
|
|
41
|
+
#
|
|
42
|
+
# Metrics (optional):
|
|
43
|
+
# - HVAR (Horizontal Metrics Variations)
|
|
44
|
+
# - VVAR (Vertical Metrics Variations)
|
|
45
|
+
# - MVAR (Metrics Variations)
|
|
46
|
+
#
|
|
47
|
+
# @example Preserve variation when converting TTF to WOFF
|
|
48
|
+
# preserver = VariationPreserver.new(ttf_font, woff_tables)
|
|
49
|
+
# preserved_tables = preserver.preserve
|
|
50
|
+
#
|
|
51
|
+
# @example Preserve only common tables for outline conversion
|
|
52
|
+
# preserver = VariationPreserver.new(ttf_font, otf_tables,
|
|
53
|
+
# preserve_format_specific: false)
|
|
54
|
+
# preserved_tables = preserver.preserve
|
|
55
|
+
class VariationPreserver
|
|
56
|
+
# Common variation tables present in all variable fonts
|
|
57
|
+
COMMON_TABLES = %w[fvar avar STAT].freeze
|
|
58
|
+
|
|
59
|
+
# TrueType-specific variation tables
|
|
60
|
+
TRUETYPE_TABLES = %w[gvar cvar].freeze
|
|
61
|
+
|
|
62
|
+
# CFF2-specific variation tables
|
|
63
|
+
CFF2_TABLES = %w[CFF2].freeze
|
|
64
|
+
|
|
65
|
+
# Metrics variation tables
|
|
66
|
+
METRICS_TABLES = %w[HVAR VVAR MVAR].freeze
|
|
67
|
+
|
|
68
|
+
# All variation-related tables
|
|
69
|
+
ALL_VARIATION_TABLES = (COMMON_TABLES + TRUETYPE_TABLES +
|
|
70
|
+
CFF2_TABLES + METRICS_TABLES).freeze
|
|
71
|
+
|
|
72
|
+
# Preserve variation data from source to target
|
|
73
|
+
#
|
|
74
|
+
# @param source_font [TrueTypeFont, OpenTypeFont] Variable font
|
|
75
|
+
# @param target_tables [Hash<String, String>] Target font tables
|
|
76
|
+
# @param options [Hash] Preservation options
|
|
77
|
+
# @return [Hash<String, String>] Tables with variation data preserved
|
|
78
|
+
def self.preserve(source_font, target_tables, options = {})
|
|
79
|
+
new(source_font, target_tables, options).preserve
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Object] Source font
|
|
83
|
+
attr_reader :source_font
|
|
84
|
+
|
|
85
|
+
# @return [Hash<String, String>] Target tables
|
|
86
|
+
attr_reader :target_tables
|
|
87
|
+
|
|
88
|
+
# @return [Hash] Preservation options
|
|
89
|
+
attr_reader :options
|
|
90
|
+
|
|
91
|
+
# Initialize preserver
|
|
92
|
+
#
|
|
93
|
+
# @param source_font [TrueTypeFont, OpenTypeFont] Variable font
|
|
94
|
+
# @param target_tables [Hash<String, String>] Target font tables
|
|
95
|
+
# @param options [Hash] Preservation options
|
|
96
|
+
# @option options [Boolean] :preserve_format_specific Preserve format-
|
|
97
|
+
# specific variation tables (default: true)
|
|
98
|
+
# @option options [Boolean] :preserve_metrics Preserve metrics variation
|
|
99
|
+
# tables (default: true)
|
|
100
|
+
# @option options [Boolean] :validate Validate table consistency
|
|
101
|
+
# (default: true)
|
|
102
|
+
def initialize(source_font, target_tables, options = {})
|
|
103
|
+
@source_font = source_font
|
|
104
|
+
@target_tables = target_tables.dup
|
|
105
|
+
@options = options
|
|
106
|
+
|
|
107
|
+
validate_source!
|
|
108
|
+
@context = VariationContext.new(source_font)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Preserve variation tables
|
|
112
|
+
#
|
|
113
|
+
# @return [Hash<String, String>] Target tables with variation preserved
|
|
114
|
+
def preserve
|
|
115
|
+
# Copy common variation tables (fvar, avar, STAT)
|
|
116
|
+
copy_common_tables
|
|
117
|
+
|
|
118
|
+
# Copy format-specific variation tables if requested
|
|
119
|
+
if preserve_format_specific?
|
|
120
|
+
copy_format_specific_tables
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Copy metrics variation tables if requested
|
|
124
|
+
copy_metrics_tables if preserve_metrics?
|
|
125
|
+
|
|
126
|
+
# Validate consistency if requested
|
|
127
|
+
validate_consistency if validate?
|
|
128
|
+
|
|
129
|
+
@target_tables
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if source font is a variable font
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] True if source has fvar table
|
|
135
|
+
def variable_font?
|
|
136
|
+
@context.variable_font?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get variation type of source font
|
|
140
|
+
#
|
|
141
|
+
# @return [Symbol, nil] :truetype, :cff2, or nil
|
|
142
|
+
def variation_type
|
|
143
|
+
@context.variation_type
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Validate source font
|
|
149
|
+
#
|
|
150
|
+
# @raise [ArgumentError] If source is invalid
|
|
151
|
+
def validate_source!
|
|
152
|
+
raise ArgumentError, "Source font cannot be nil" if @source_font.nil?
|
|
153
|
+
|
|
154
|
+
unless @source_font.respond_to?(:has_table?) &&
|
|
155
|
+
@source_font.respond_to?(:table_data)
|
|
156
|
+
raise ArgumentError,
|
|
157
|
+
"Source font must respond to :has_table? and :table_data"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
raise ArgumentError, "Target tables cannot be nil" if @target_tables.nil?
|
|
161
|
+
|
|
162
|
+
unless @target_tables.is_a?(Hash)
|
|
163
|
+
raise ArgumentError,
|
|
164
|
+
"Target tables must be a Hash, got: #{@target_tables.class}"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Copy common variation tables (fvar, avar, STAT)
|
|
169
|
+
#
|
|
170
|
+
# These tables are independent of outline format and can always be copied
|
|
171
|
+
def copy_common_tables
|
|
172
|
+
COMMON_TABLES.each do |tag|
|
|
173
|
+
copy_table(tag) if @source_font.has_table?(tag)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Copy format-specific variation tables
|
|
178
|
+
#
|
|
179
|
+
# For TrueType: gvar, cvar
|
|
180
|
+
# For CFF2: CFF2 table
|
|
181
|
+
def copy_format_specific_tables
|
|
182
|
+
case variation_type
|
|
183
|
+
when :truetype
|
|
184
|
+
copy_truetype_variation_tables
|
|
185
|
+
when :postscript
|
|
186
|
+
copy_cff2_variation_tables
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Copy TrueType variation tables
|
|
191
|
+
def copy_truetype_variation_tables
|
|
192
|
+
TRUETYPE_TABLES.each do |tag|
|
|
193
|
+
copy_table(tag) if @source_font.has_table?(tag)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Copy CFF2 variation tables
|
|
198
|
+
def copy_cff2_variation_tables
|
|
199
|
+
# CFF2 table contains both outlines and variation data
|
|
200
|
+
# Only copy if target doesn't already have CFF2 and source has it
|
|
201
|
+
return unless @source_font.has_table?("CFF2")
|
|
202
|
+
return if @target_tables.key?("CFF2")
|
|
203
|
+
|
|
204
|
+
copy_table("CFF2")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Copy metrics variation tables (HVAR, VVAR, MVAR)
|
|
208
|
+
def copy_metrics_tables
|
|
209
|
+
METRICS_TABLES.each do |tag|
|
|
210
|
+
copy_table(tag) if @source_font.has_table?(tag)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Copy a single table from source to target
|
|
215
|
+
#
|
|
216
|
+
# @param tag [String] Table tag
|
|
217
|
+
def copy_table(tag)
|
|
218
|
+
return unless @source_font.has_table?(tag)
|
|
219
|
+
|
|
220
|
+
table_data = @source_font.table_data[tag]
|
|
221
|
+
return unless table_data
|
|
222
|
+
|
|
223
|
+
@target_tables[tag] = table_data.dup
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Validate table consistency
|
|
227
|
+
#
|
|
228
|
+
# Ensures that copied variation tables are consistent with target font
|
|
229
|
+
# @raise [Error] If validation fails
|
|
230
|
+
def validate_consistency
|
|
231
|
+
# Must have fvar if we're preserving variations
|
|
232
|
+
unless @target_tables.key?("fvar")
|
|
233
|
+
raise Fontisan::Error,
|
|
234
|
+
"Cannot preserve variations: fvar table missing"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# If we have gvar, we must have glyf (TrueType outlines)
|
|
238
|
+
if @target_tables.key?("gvar") && !@target_tables.key?("glyf")
|
|
239
|
+
raise Fontisan::Error,
|
|
240
|
+
"Invalid variation preservation: gvar present without glyf"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# If we have CFF2, we shouldn't have glyf (CFF2 has CFF outlines)
|
|
244
|
+
# Check both source and target to catch conflicts
|
|
245
|
+
has_cff2 = @target_tables.key?("CFF2") ||
|
|
246
|
+
(@source_font.has_table?("CFF2") && preserve_format_specific?)
|
|
247
|
+
if has_cff2 && @target_tables.key?("glyf")
|
|
248
|
+
raise Fontisan::Error,
|
|
249
|
+
"Invalid variation preservation: CFF2 and glyf both present"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Metrics variation tables require fvar
|
|
253
|
+
if metrics_tables_present? && !@target_tables.key?("fvar")
|
|
254
|
+
raise Fontisan::Error,
|
|
255
|
+
"Metrics variation tables require fvar table"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Check if any metrics variation tables are present
|
|
260
|
+
#
|
|
261
|
+
# @return [Boolean] True if HVAR, VVAR, or MVAR present
|
|
262
|
+
def metrics_tables_present?
|
|
263
|
+
METRICS_TABLES.any? { |tag| @target_tables.key?(tag) }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Get preserve_format_specific option
|
|
267
|
+
#
|
|
268
|
+
# @return [Boolean] True if format-specific tables should be preserved
|
|
269
|
+
def preserve_format_specific?
|
|
270
|
+
@options.fetch(:preserve_format_specific, true)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Get preserve_metrics option
|
|
274
|
+
#
|
|
275
|
+
# @return [Boolean] True if metrics tables should be preserved
|
|
276
|
+
def preserve_metrics?
|
|
277
|
+
@options.fetch(:preserve_metrics, true)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Get validate option
|
|
281
|
+
#
|
|
282
|
+
# @return [Boolean] True if consistency should be validated
|
|
283
|
+
def validate?
|
|
284
|
+
@options.fetch(:validate, true)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
data/lib/fontisan/version.rb
CHANGED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# WOFF2 Table Directory Entry
|
|
6
|
+
#
|
|
7
|
+
# [`Woff2::Directory`](lib/fontisan/woff2/directory.rb) represents
|
|
8
|
+
# a single table entry in the WOFF2 table directory. Unlike WOFF,
|
|
9
|
+
# WOFF2 uses variable-length encoding for sizes and supports table
|
|
10
|
+
# transformations for better compression.
|
|
11
|
+
#
|
|
12
|
+
# Each entry contains:
|
|
13
|
+
# - flags (1 byte): Contains tag index and transformation version
|
|
14
|
+
# - tag (0 or 4 bytes): Table tag (omitted if using known tag index)
|
|
15
|
+
# - origLength (UIntBase128): Original uncompressed table length
|
|
16
|
+
# - transformLength (UIntBase128, optional): Transformed data length
|
|
17
|
+
#
|
|
18
|
+
# Flags byte structure:
|
|
19
|
+
# - Bits 0-5: Table tag index (0-62 = known tags, 63 = custom tag)
|
|
20
|
+
# - Bits 6-7: Transformation version
|
|
21
|
+
#
|
|
22
|
+
# Reference: https://www.w3.org/TR/WOFF2/#table_dir_format
|
|
23
|
+
#
|
|
24
|
+
# @example Create entry for known table
|
|
25
|
+
# entry = Directory::Entry.new
|
|
26
|
+
# entry.tag = "glyf"
|
|
27
|
+
# entry.orig_length = 12000
|
|
28
|
+
# entry.flags = entry.calculate_flags
|
|
29
|
+
#
|
|
30
|
+
# @example Create entry for custom table
|
|
31
|
+
# entry = Directory::Entry.new
|
|
32
|
+
# entry.tag = "CUST"
|
|
33
|
+
# entry.orig_length = 5000
|
|
34
|
+
# entry.flags = 0x3F # Custom tag indicator
|
|
35
|
+
module Directory
|
|
36
|
+
# Known table tags with assigned indices (0-62)
|
|
37
|
+
# Index 63 (0x3F) indicates a custom tag follows
|
|
38
|
+
KNOWN_TAGS = [
|
|
39
|
+
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
|
|
40
|
+
"cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
|
|
41
|
+
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
|
|
42
|
+
"vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
|
|
43
|
+
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
|
|
44
|
+
"bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
|
|
45
|
+
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
|
|
46
|
+
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill"
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
# Transformation versions
|
|
50
|
+
TRANSFORM_NONE = 0
|
|
51
|
+
TRANSFORM_GLYF_LOCA = 0 # Applied to both glyf and loca
|
|
52
|
+
TRANSFORM_HMTX = 0 # Applied to hmtx
|
|
53
|
+
|
|
54
|
+
# Custom tag indicator
|
|
55
|
+
CUSTOM_TAG_INDEX = 0x3F
|
|
56
|
+
|
|
57
|
+
# WOFF2 Table Directory Entry
|
|
58
|
+
#
|
|
59
|
+
# Represents a single table in the WOFF2 font with all metadata
|
|
60
|
+
# needed for decompression and reconstruction.
|
|
61
|
+
class Entry
|
|
62
|
+
attr_accessor :tag, :flags, :orig_length, :transform_length, :offset # Calculated during encoding
|
|
63
|
+
|
|
64
|
+
def initialize
|
|
65
|
+
@tag = nil
|
|
66
|
+
@flags = 0
|
|
67
|
+
@orig_length = 0
|
|
68
|
+
@transform_length = nil
|
|
69
|
+
@offset = 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Calculate flags byte for this entry
|
|
73
|
+
#
|
|
74
|
+
# @return [Integer] Flags byte (0-255)
|
|
75
|
+
def calculate_flags
|
|
76
|
+
tag_index = KNOWN_TAGS.index(tag) || CUSTOM_TAG_INDEX
|
|
77
|
+
transform_version = determine_transform_version
|
|
78
|
+
|
|
79
|
+
# Combine tag index (bits 0-5) and transform version (bits 6-7)
|
|
80
|
+
(transform_version << 6) | tag_index
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Check if table uses a known tag
|
|
84
|
+
#
|
|
85
|
+
# @return [Boolean] True if known tag
|
|
86
|
+
def known_tag?
|
|
87
|
+
KNOWN_TAGS.include?(tag)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if table is transformed
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] True if transformed
|
|
93
|
+
def transformed?
|
|
94
|
+
transform_version != TRANSFORM_NONE && transform_length
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get transformation version from flags
|
|
98
|
+
#
|
|
99
|
+
# @return [Integer] Transform version (0-3)
|
|
100
|
+
def transform_version
|
|
101
|
+
(flags >> 6) & 0x03
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get tag index from flags
|
|
105
|
+
#
|
|
106
|
+
# @return [Integer] Tag index (0-63)
|
|
107
|
+
def tag_index
|
|
108
|
+
flags & 0x3F
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Determine if this table should be transformed
|
|
112
|
+
#
|
|
113
|
+
# For Phase 2 Milestone 2.1, we support transformation flags
|
|
114
|
+
# but don't implement the actual transformations yet.
|
|
115
|
+
#
|
|
116
|
+
# @return [Integer] Transform version
|
|
117
|
+
def determine_transform_version
|
|
118
|
+
# For this milestone, we don't apply transformations
|
|
119
|
+
# but we recognize which tables could be transformed
|
|
120
|
+
TRANSFORM_NONE
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check if table can be transformed (glyf, loca, hmtx)
|
|
124
|
+
#
|
|
125
|
+
# @return [Boolean] True if transformable
|
|
126
|
+
def transformable?
|
|
127
|
+
%w[glyf loca hmtx].include?(tag)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Calculate size of this entry when serialized
|
|
131
|
+
#
|
|
132
|
+
# @return [Integer] Size in bytes
|
|
133
|
+
def serialized_size
|
|
134
|
+
size = 1 # flags byte
|
|
135
|
+
size += 4 unless known_tag? # custom tag
|
|
136
|
+
size += uint_base128_size(orig_length)
|
|
137
|
+
size += uint_base128_size(transform_length) if transformed?
|
|
138
|
+
size
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
# Estimate size of UIntBase128 encoded value
|
|
144
|
+
#
|
|
145
|
+
# @param value [Integer] Value to encode
|
|
146
|
+
# @return [Integer] Size in bytes (1-5)
|
|
147
|
+
def uint_base128_size(value)
|
|
148
|
+
return 1 if value.nil? || value < 128
|
|
149
|
+
|
|
150
|
+
bytes = 0
|
|
151
|
+
v = value
|
|
152
|
+
while v.positive?
|
|
153
|
+
bytes += 1
|
|
154
|
+
v >>= 7
|
|
155
|
+
end
|
|
156
|
+
[bytes, 5].min # Max 5 bytes
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Encode an integer as UIntBase128
|
|
161
|
+
#
|
|
162
|
+
# Variable-length encoding where:
|
|
163
|
+
# - If value < 128, use 1 byte
|
|
164
|
+
# - Otherwise, use high bit to indicate continuation
|
|
165
|
+
#
|
|
166
|
+
# @param value [Integer] Value to encode
|
|
167
|
+
# @return [String] Binary encoded data
|
|
168
|
+
def self.encode_uint_base128(value)
|
|
169
|
+
return [value].pack("C") if value < 128
|
|
170
|
+
|
|
171
|
+
bytes = []
|
|
172
|
+
v = value
|
|
173
|
+
|
|
174
|
+
# Build bytes from least to most significant
|
|
175
|
+
loop do
|
|
176
|
+
bytes.unshift(v & 0x7F)
|
|
177
|
+
v >>= 7
|
|
178
|
+
break if v.zero?
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Set high bit on all but last byte
|
|
182
|
+
(0...bytes.length - 1).each do |i|
|
|
183
|
+
bytes[i] |= 0x80
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
bytes.pack("C*")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Decode UIntBase128 from IO
|
|
190
|
+
#
|
|
191
|
+
# @param io [IO] Input stream
|
|
192
|
+
# @return [Integer] Decoded value
|
|
193
|
+
# @raise [Error] If encoding is invalid
|
|
194
|
+
def self.decode_uint_base128(io)
|
|
195
|
+
result = 0
|
|
196
|
+
5.times do
|
|
197
|
+
byte = io.read(1)&.unpack1("C")
|
|
198
|
+
return nil unless byte
|
|
199
|
+
|
|
200
|
+
# Check if high bit is set (continuation)
|
|
201
|
+
if (byte & 0x80).zero?
|
|
202
|
+
return (result << 7) | byte
|
|
203
|
+
else
|
|
204
|
+
result = (result << 7) | (byte & 0x7F)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# If we're here, encoding is invalid (> 5 bytes)
|
|
209
|
+
raise Fontisan::Error, "Invalid UIntBase128 encoding"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Encode 255UInt16 value
|
|
213
|
+
#
|
|
214
|
+
# Used in transformed glyf table:
|
|
215
|
+
# - 0-252: value itself (1 byte)
|
|
216
|
+
# - 253: next byte + 253 (2 bytes)
|
|
217
|
+
# - 254: next 2 bytes as big-endian (3 bytes)
|
|
218
|
+
# - 255: next 2 bytes + 506 (3 bytes)
|
|
219
|
+
#
|
|
220
|
+
# @param value [Integer] Value to encode (0-65535)
|
|
221
|
+
# @return [String] Binary encoded data
|
|
222
|
+
def self.encode_255_uint16(value)
|
|
223
|
+
if value < 253
|
|
224
|
+
[value].pack("C")
|
|
225
|
+
elsif value < 506
|
|
226
|
+
[253, value - 253].pack("C*")
|
|
227
|
+
elsif value < 65536
|
|
228
|
+
[254].pack("C") + [value].pack("n")
|
|
229
|
+
else
|
|
230
|
+
[255].pack("C") + [value - 506].pack("n")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Decode 255UInt16 from IO
|
|
235
|
+
#
|
|
236
|
+
# @param io [IO] Input stream
|
|
237
|
+
# @return [Integer] Decoded value
|
|
238
|
+
def self.decode_255_uint16(io)
|
|
239
|
+
first = io.read(1)&.unpack1("C")
|
|
240
|
+
return nil unless first
|
|
241
|
+
|
|
242
|
+
case first
|
|
243
|
+
when 0..252
|
|
244
|
+
first
|
|
245
|
+
when 253
|
|
246
|
+
second = io.read(1)&.unpack1("C")
|
|
247
|
+
253 + second
|
|
248
|
+
when 254
|
|
249
|
+
io.read(2)&.unpack1("n")
|
|
250
|
+
when 255
|
|
251
|
+
value = io.read(2)&.unpack1("n")
|
|
252
|
+
value + 506
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|