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,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Woff2
|
|
7
|
+
# WOFF2 Header structure
|
|
8
|
+
#
|
|
9
|
+
# [`Woff2::Header`](lib/fontisan/woff2/header.rb) represents the main
|
|
10
|
+
# header of a WOFF2 file according to W3C WOFF2 specification.
|
|
11
|
+
#
|
|
12
|
+
# The header is more compact than WOFF, using 48 bytes.
|
|
13
|
+
#
|
|
14
|
+
# Structure (all big-endian):
|
|
15
|
+
# - uint32: signature (0x774F4632 'wOF2')
|
|
16
|
+
# - uint32: flavor (0x00010000 for TTF, 0x4F54544F for CFF)
|
|
17
|
+
# - uint32: file_length (total WOFF2 file size)
|
|
18
|
+
# - uint16: numTables (number of font tables)
|
|
19
|
+
# - uint16: reserved (must be 0)
|
|
20
|
+
# - uint32: totalSfntSize (uncompressed font size)
|
|
21
|
+
# - uint32: totalCompressedSize (size of compressed data block)
|
|
22
|
+
# - uint16: majorVersion (major version of WOFF file)
|
|
23
|
+
# - uint16: minorVersion (minor version of WOFF file)
|
|
24
|
+
# - uint32: metaOffset (offset to metadata, 0 if none)
|
|
25
|
+
# - uint32: metaLength (compressed metadata length)
|
|
26
|
+
# - uint32: metaOrigLength (uncompressed metadata length)
|
|
27
|
+
# - uint32: privOffset (offset to private data, 0 if none)
|
|
28
|
+
# - uint32: privLength (length of private data)
|
|
29
|
+
#
|
|
30
|
+
# Reference: https://www.w3.org/TR/WOFF2/#woff20Header
|
|
31
|
+
#
|
|
32
|
+
# @example Create a header
|
|
33
|
+
# header = Woff2::Header.new
|
|
34
|
+
# header.signature = 0x774F4632
|
|
35
|
+
# header.flavor = 0x00010000
|
|
36
|
+
# header.num_tables = 10
|
|
37
|
+
class Woff2Header < BinData::Record
|
|
38
|
+
endian :big
|
|
39
|
+
|
|
40
|
+
uint32 :signature # 'wOF2' magic number
|
|
41
|
+
uint32 :flavor # Font format (TTF or CFF)
|
|
42
|
+
uint32 :file_length # Total WOFF2 file size
|
|
43
|
+
uint16 :num_tables # Number of font tables
|
|
44
|
+
uint16 :reserved # Reserved, must be 0
|
|
45
|
+
uint32 :total_sfnt_size # Uncompressed font size
|
|
46
|
+
uint32 :total_compressed_size # Compressed data block size
|
|
47
|
+
uint16 :major_version # Major version number
|
|
48
|
+
uint16 :minor_version # Minor version number
|
|
49
|
+
uint32 :meta_offset # Metadata block offset (0 if none)
|
|
50
|
+
uint32 :meta_length # Compressed metadata length
|
|
51
|
+
uint32 :meta_orig_length # Uncompressed metadata length
|
|
52
|
+
uint32 :priv_offset # Private data offset (0 if none)
|
|
53
|
+
uint32 :priv_length # Private data length
|
|
54
|
+
|
|
55
|
+
# WOFF2 signature constant
|
|
56
|
+
SIGNATURE = 0x774F4632 # 'wOF2'
|
|
57
|
+
|
|
58
|
+
# Check if signature is valid
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] True if signature is valid
|
|
61
|
+
def valid_signature?
|
|
62
|
+
signature == SIGNATURE
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if font is TrueType flavored
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean] True if TrueType
|
|
68
|
+
def truetype?
|
|
69
|
+
[0x00010000, 0x74727565].include?(flavor) # 'true'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if font is CFF flavored
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean] True if CFF/OpenType
|
|
75
|
+
def cff?
|
|
76
|
+
flavor == 0x4F54544F # 'OTTO'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if metadata is present
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean] True if metadata exists
|
|
82
|
+
def has_metadata?
|
|
83
|
+
meta_offset.positive? && meta_length.positive?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if private data is present
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean] True if private data exists
|
|
89
|
+
def has_private_data?
|
|
90
|
+
priv_offset.positive? && priv_length.positive?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get header size in bytes
|
|
94
|
+
#
|
|
95
|
+
# @return [Integer] Header size (always 48 bytes)
|
|
96
|
+
def self.header_size
|
|
97
|
+
48
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Woff2
|
|
7
|
+
# Reconstructs hmtx table from WOFF2 transformed format
|
|
8
|
+
#
|
|
9
|
+
# WOFF2 hmtx transformation optimizes horizontal metrics by:
|
|
10
|
+
# - Using variable-length encoding for advance widths
|
|
11
|
+
# - Optionally deriving LSB from glyf bounding boxes
|
|
12
|
+
# - Omitting redundant trailing advance widths
|
|
13
|
+
#
|
|
14
|
+
# See: https://www.w3.org/TR/WOFF2/#hmtx_table_format
|
|
15
|
+
#
|
|
16
|
+
# @example Reconstructing hmtx table
|
|
17
|
+
# hmtx_data = HmtxTransformer.reconstruct(
|
|
18
|
+
# transformed_data,
|
|
19
|
+
# num_glyphs,
|
|
20
|
+
# number_of_h_metrics
|
|
21
|
+
# )
|
|
22
|
+
class HmtxTransformer
|
|
23
|
+
# Flags for hmtx transformation
|
|
24
|
+
HMTX_FLAG_EXPLICIT_ADVANCE_WIDTHS = 0x01
|
|
25
|
+
HMTX_FLAG_EXPLICIT_LSB_VALUES = 0x02
|
|
26
|
+
HMTX_FLAG_SYMMETRIC = 0x04
|
|
27
|
+
|
|
28
|
+
# Reconstruct hmtx table from transformed data
|
|
29
|
+
#
|
|
30
|
+
# @param transformed_data [String] The transformed hmtx table data
|
|
31
|
+
# @param num_glyphs [Integer] Number of glyphs
|
|
32
|
+
# @param num_h_metrics [Integer] From hhea.numberOfHMetrics
|
|
33
|
+
# @param glyf_lsbs [Array<Integer>, nil] LSB values from glyf bboxes (optional)
|
|
34
|
+
# @return [String] Standard hmtx table data
|
|
35
|
+
# @raise [InvalidFontError] If data is corrupted or invalid
|
|
36
|
+
def self.reconstruct(transformed_data, num_glyphs, num_h_metrics, glyf_lsbs = nil)
|
|
37
|
+
io = StringIO.new(transformed_data)
|
|
38
|
+
|
|
39
|
+
# Read transformation flags
|
|
40
|
+
flags = read_uint8(io)
|
|
41
|
+
|
|
42
|
+
# Read advance widths
|
|
43
|
+
advance_widths = []
|
|
44
|
+
|
|
45
|
+
if (flags & HMTX_FLAG_EXPLICIT_ADVANCE_WIDTHS).zero?
|
|
46
|
+
# Proportional encoding - read deltas
|
|
47
|
+
# First advance width is explicit
|
|
48
|
+
first_advance = read_255_uint16(io)
|
|
49
|
+
advance_widths << first_advance
|
|
50
|
+
|
|
51
|
+
# Remaining are deltas from previous
|
|
52
|
+
(num_h_metrics - 1).times do
|
|
53
|
+
delta = read_int16(io)
|
|
54
|
+
advance_widths << (advance_widths.last + delta)
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
# Explicit advance widths in transformed format
|
|
58
|
+
num_h_metrics.times do
|
|
59
|
+
advance_widths << read_255_uint16(io)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Read LSB values
|
|
64
|
+
lsbs = []
|
|
65
|
+
|
|
66
|
+
if (flags & HMTX_FLAG_EXPLICIT_LSB_VALUES) != 0
|
|
67
|
+
# Explicit LSB values
|
|
68
|
+
num_glyphs.times do
|
|
69
|
+
lsbs << read_int16(io)
|
|
70
|
+
end
|
|
71
|
+
elsif glyf_lsbs
|
|
72
|
+
# Use LSB values from glyf bounding boxes
|
|
73
|
+
lsbs = glyf_lsbs
|
|
74
|
+
else
|
|
75
|
+
# Need to read LSB values for long metrics
|
|
76
|
+
num_h_metrics.times do
|
|
77
|
+
lsbs << read_int16(io)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Remaining LSBs for glyphs that share the last advance width
|
|
81
|
+
(num_glyphs - num_h_metrics).times do
|
|
82
|
+
lsbs << read_int16(io)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Build standard hmtx table
|
|
87
|
+
build_hmtx_table(advance_widths, lsbs, num_h_metrics, num_glyphs)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Read variable-length 255UInt16 integer
|
|
91
|
+
#
|
|
92
|
+
# Format from WOFF2 spec:
|
|
93
|
+
# - value < 253: one byte
|
|
94
|
+
# - value == 253: 253 + next uint16
|
|
95
|
+
# - value == 254: 253 * 2 + next uint16
|
|
96
|
+
# - value == 255: 253 * 3 + next uint16
|
|
97
|
+
#
|
|
98
|
+
# @param io [StringIO] Input stream
|
|
99
|
+
# @return [Integer] Decoded value
|
|
100
|
+
def self.read_255_uint16(io)
|
|
101
|
+
code = read_uint8(io)
|
|
102
|
+
|
|
103
|
+
case code
|
|
104
|
+
when 255
|
|
105
|
+
759 + read_uint16(io) # 253 * 3 + value
|
|
106
|
+
when 254
|
|
107
|
+
506 + read_uint16(io) # 253 * 2 + value
|
|
108
|
+
when 253
|
|
109
|
+
253 + read_uint16(io)
|
|
110
|
+
else
|
|
111
|
+
code
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Build standard hmtx table format
|
|
116
|
+
#
|
|
117
|
+
# Standard hmtx format:
|
|
118
|
+
# - longHorMetric[numberOfHMetrics] (advanceWidth, lsb pairs)
|
|
119
|
+
# - int16[numGlyphs - numberOfHMetrics] (additional LSBs)
|
|
120
|
+
#
|
|
121
|
+
# @param advance_widths [Array<Integer>] Advance widths
|
|
122
|
+
# @param lsbs [Array<Integer>] Left side bearings
|
|
123
|
+
# @param num_h_metrics [Integer] Number of entries with full hMetrics
|
|
124
|
+
# @param num_glyphs [Integer] Total number of glyphs
|
|
125
|
+
# @return [String] Standard hmtx table data
|
|
126
|
+
def self.build_hmtx_table(advance_widths, lsbs, num_h_metrics, num_glyphs)
|
|
127
|
+
data = +""
|
|
128
|
+
|
|
129
|
+
# Write longHorMetric array (advanceWidth + lsb pairs)
|
|
130
|
+
num_h_metrics.times do |i|
|
|
131
|
+
advance_width = advance_widths[i] || advance_widths.last
|
|
132
|
+
lsb = lsbs[i] || 0
|
|
133
|
+
|
|
134
|
+
data << [advance_width].pack("n") # uint16 advanceWidth
|
|
135
|
+
data << [lsb].pack("n") # int16 lsb
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Write remaining LSB values
|
|
139
|
+
# These glyphs all share the last advance width from the array
|
|
140
|
+
(num_h_metrics...num_glyphs).each do |i|
|
|
141
|
+
lsb = lsbs[i] || 0
|
|
142
|
+
data << [lsb].pack("n") # int16 lsb
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
data
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Helper methods for reading binary data
|
|
149
|
+
|
|
150
|
+
def self.read_uint8(io)
|
|
151
|
+
io.read(1)&.unpack1("C") || raise(EOFError, "Unexpected end of stream")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.read_uint16(io)
|
|
155
|
+
io.read(2)&.unpack1("n") || raise(EOFError, "Unexpected end of stream")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.read_int16(io)
|
|
159
|
+
value = read_uint16(io)
|
|
160
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Woff2
|
|
5
|
+
# Table transformer for WOFF2 encoding
|
|
6
|
+
#
|
|
7
|
+
# [`Woff2::TableTransformer`](lib/fontisan/woff2/table_transformer.rb)
|
|
8
|
+
# handles table transformations that improve compression in WOFF2.
|
|
9
|
+
# The WOFF2 spec defines transformations for glyf/loca and hmtx tables.
|
|
10
|
+
#
|
|
11
|
+
# For Phase 2 Milestone 2.1:
|
|
12
|
+
# - Architecture is in place for transformations
|
|
13
|
+
# - Actual transformation implementations are marked as TODO
|
|
14
|
+
# - Tables are copied as-is without transformation
|
|
15
|
+
# - This allows valid WOFF2 generation while leaving room for optimization
|
|
16
|
+
#
|
|
17
|
+
# Future milestones will implement:
|
|
18
|
+
# - glyf/loca transformation (combined stream, delta encoding)
|
|
19
|
+
# - hmtx transformation (compact representation)
|
|
20
|
+
#
|
|
21
|
+
# Reference: https://www.w3.org/TR/WOFF2/#table_tranforms
|
|
22
|
+
#
|
|
23
|
+
# @example Transform tables for WOFF2
|
|
24
|
+
# transformer = TableTransformer.new(font)
|
|
25
|
+
# glyf_data = transformer.transform_table("glyf")
|
|
26
|
+
class TableTransformer
|
|
27
|
+
# @return [Object] Font object with table access
|
|
28
|
+
attr_reader :font
|
|
29
|
+
|
|
30
|
+
# Initialize transformer with font
|
|
31
|
+
#
|
|
32
|
+
# @param font [TrueTypeFont, OpenTypeFont] Source font
|
|
33
|
+
def initialize(font)
|
|
34
|
+
@font = font
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Transform a table for WOFF2 encoding
|
|
38
|
+
#
|
|
39
|
+
# For Milestone 2.1, this returns the original table data
|
|
40
|
+
# without transformation. The architecture supports future
|
|
41
|
+
# implementation of actual transformations.
|
|
42
|
+
#
|
|
43
|
+
# @param tag [String] Table tag
|
|
44
|
+
# @return [String, nil] Transformed (or original) table data
|
|
45
|
+
def transform_table(tag)
|
|
46
|
+
case tag
|
|
47
|
+
when "glyf"
|
|
48
|
+
transform_glyf
|
|
49
|
+
when "loca"
|
|
50
|
+
transform_loca
|
|
51
|
+
when "hmtx"
|
|
52
|
+
transform_hmtx
|
|
53
|
+
else
|
|
54
|
+
# No transformation, return original data
|
|
55
|
+
get_table_data(tag)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if a table can be transformed
|
|
60
|
+
#
|
|
61
|
+
# @param tag [String] Table tag
|
|
62
|
+
# @return [Boolean] True if table supports transformation
|
|
63
|
+
def transformable?(tag)
|
|
64
|
+
%w[glyf loca hmtx].include?(tag)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Determine transformation version for a table
|
|
68
|
+
#
|
|
69
|
+
# For Milestone 2.1, always returns TRANSFORM_NONE since
|
|
70
|
+
# we don't implement transformations yet.
|
|
71
|
+
#
|
|
72
|
+
# @param tag [String] Table tag
|
|
73
|
+
# @return [Integer] Transformation version (0 = none)
|
|
74
|
+
def transformation_version(_tag)
|
|
75
|
+
# For this milestone, no transformations are applied
|
|
76
|
+
Directory::TRANSFORM_NONE
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Transform glyf table
|
|
82
|
+
#
|
|
83
|
+
# The WOFF2 glyf transformation combines glyf and loca into a
|
|
84
|
+
# single stream with delta-encoded coordinates and flags.
|
|
85
|
+
#
|
|
86
|
+
# TODO: Implement full glyf transformation for better compression.
|
|
87
|
+
# For now, returns original table data.
|
|
88
|
+
#
|
|
89
|
+
# @return [String, nil] Transformed glyf data
|
|
90
|
+
def transform_glyf
|
|
91
|
+
# TODO: Implement glyf transformation
|
|
92
|
+
# This would involve:
|
|
93
|
+
# 1. Parse all glyphs from glyf table
|
|
94
|
+
# 2. Combine with loca offsets
|
|
95
|
+
# 3. Create transformed stream with:
|
|
96
|
+
# - nContour values
|
|
97
|
+
# - nPoints values
|
|
98
|
+
# - Flag bytes (with run-length encoding)
|
|
99
|
+
# - x-coordinates (delta-encoded)
|
|
100
|
+
# - y-coordinates (delta-encoded)
|
|
101
|
+
# - Instruction bytes
|
|
102
|
+
# 4. Use 255UInt16 encoding for variable-length integers
|
|
103
|
+
#
|
|
104
|
+
# Reference: https://www.w3.org/TR/WOFF2/#glyf_table_format
|
|
105
|
+
|
|
106
|
+
get_table_data("glyf")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Transform loca table
|
|
110
|
+
#
|
|
111
|
+
# In WOFF2, loca is combined with glyf during transformation.
|
|
112
|
+
# When glyf is transformed, loca table is omitted from output.
|
|
113
|
+
#
|
|
114
|
+
# TODO: Implement loca transformation (combined with glyf).
|
|
115
|
+
# For now, returns original table data.
|
|
116
|
+
#
|
|
117
|
+
# @return [String, nil] Transformed loca data
|
|
118
|
+
def transform_loca
|
|
119
|
+
# TODO: Implement loca transformation
|
|
120
|
+
# When glyf transformation is implemented, loca will be:
|
|
121
|
+
# 1. Combined into the transformed glyf stream
|
|
122
|
+
# 2. Reconstructed during decompression
|
|
123
|
+
# 3. Not present as separate table in WOFF2
|
|
124
|
+
|
|
125
|
+
get_table_data("loca")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Transform hmtx table
|
|
129
|
+
#
|
|
130
|
+
# The WOFF2 hmtx transformation stores advance widths more efficiently
|
|
131
|
+
# by exploiting redundancy (many glyphs have same advance width).
|
|
132
|
+
#
|
|
133
|
+
# TODO: Implement hmtx transformation for better compression.
|
|
134
|
+
# For now, returns original table data.
|
|
135
|
+
#
|
|
136
|
+
# @return [String, nil] Transformed hmtx data
|
|
137
|
+
def transform_hmtx
|
|
138
|
+
# TODO: Implement hmtx transformation
|
|
139
|
+
# This would involve:
|
|
140
|
+
# 1. Parse hmtx table
|
|
141
|
+
# 2. Extract common advance widths
|
|
142
|
+
# 3. Identify proportional vs monospace sections
|
|
143
|
+
# 4. Use flags to indicate structure
|
|
144
|
+
# 5. Store only unique advance widths
|
|
145
|
+
# 6. Store LSB array separately
|
|
146
|
+
#
|
|
147
|
+
# Reference: https://www.w3.org/TR/WOFF2/#hmtx_table_format
|
|
148
|
+
|
|
149
|
+
get_table_data("hmtx")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get raw table data from font
|
|
153
|
+
#
|
|
154
|
+
# @param tag [String] Table tag
|
|
155
|
+
# @return [String, nil] Table data or nil if not found
|
|
156
|
+
def get_table_data(tag)
|
|
157
|
+
return nil unless font.respond_to?(:table_data)
|
|
158
|
+
|
|
159
|
+
font.table_data(tag)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|