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,346 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../binary/base_record"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# Parser for the 'CFF2' (Compact Font Format 2) table
|
|
9
|
+
#
|
|
10
|
+
# CFF2 is used primarily in variable fonts with PostScript outlines.
|
|
11
|
+
# Key differences from CFF:
|
|
12
|
+
# - No Name INDEX (font names come from name table)
|
|
13
|
+
# - No Encoding or Charset (use cmap table instead)
|
|
14
|
+
# - Support for blend operators in CharStrings for variations
|
|
15
|
+
# - Different default values in DICTs
|
|
16
|
+
#
|
|
17
|
+
# Reference: Adobe Technical Note #5177
|
|
18
|
+
#
|
|
19
|
+
# @example Reading a CFF2 table
|
|
20
|
+
# data = font.table_data("CFF2")
|
|
21
|
+
# cff2 = Fontisan::Tables::Cff2.read(data)
|
|
22
|
+
# num_glyphs = cff2.glyph_count
|
|
23
|
+
class Cff2 < Binary::BaseRecord
|
|
24
|
+
# CFF2 header structure
|
|
25
|
+
class Cff2Header < Binary::BaseRecord
|
|
26
|
+
uint8 :major_version
|
|
27
|
+
uint8 :minor_version
|
|
28
|
+
uint8 :header_size
|
|
29
|
+
uint16 :top_dict_length
|
|
30
|
+
|
|
31
|
+
# Check if version is valid
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] True if version is 2.0
|
|
34
|
+
def valid?
|
|
35
|
+
major_version == 2 && minor_version.zero?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Parse the CFF2 table
|
|
40
|
+
#
|
|
41
|
+
# @return [self]
|
|
42
|
+
def parse
|
|
43
|
+
return self if @parsed
|
|
44
|
+
|
|
45
|
+
@header = parse_header
|
|
46
|
+
@global_subr_index = parse_global_subr_index
|
|
47
|
+
@top_dict = parse_top_dict
|
|
48
|
+
@charstrings_index = parse_charstrings_index
|
|
49
|
+
|
|
50
|
+
@parsed = true
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get the CFF2 header
|
|
55
|
+
#
|
|
56
|
+
# @return [Cff2Header] Header structure
|
|
57
|
+
def header
|
|
58
|
+
parse unless @parsed
|
|
59
|
+
@header
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get glyph count from font's maxp table
|
|
63
|
+
#
|
|
64
|
+
# CFF2 doesn't store glyph count internally - it relies on the maxp table
|
|
65
|
+
#
|
|
66
|
+
# @return [Integer] Number of glyphs (requires access to font's maxp)
|
|
67
|
+
def glyph_count
|
|
68
|
+
# This needs to be set externally or retrieved from maxp table
|
|
69
|
+
# For now, return a default that indicates it needs to be set
|
|
70
|
+
@glyph_count || 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Set glyph count (from maxp table)
|
|
74
|
+
#
|
|
75
|
+
# @param count [Integer] Number of glyphs
|
|
76
|
+
def glyph_count=(count)
|
|
77
|
+
@glyph_count = count
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Set number of variation axes (from fvar table)
|
|
81
|
+
#
|
|
82
|
+
# @param count [Integer] Number of axes
|
|
83
|
+
def num_axes=(count)
|
|
84
|
+
@num_axes = count
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get number of variation axes
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer] Number of axes
|
|
90
|
+
def num_axes
|
|
91
|
+
@num_axes || 0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get CharString for a specific glyph
|
|
95
|
+
#
|
|
96
|
+
# @param glyph_id [Integer] Glyph ID
|
|
97
|
+
# @return [CharstringParser, nil] CharString object or nil
|
|
98
|
+
def charstring_for_glyph(glyph_id)
|
|
99
|
+
parse unless @parsed
|
|
100
|
+
return nil if @charstrings_index.nil?
|
|
101
|
+
return nil if glyph_id >= @charstrings_index.count
|
|
102
|
+
|
|
103
|
+
# Get CharString data from INDEX
|
|
104
|
+
charstring_data = @charstrings_index[glyph_id]
|
|
105
|
+
return nil if charstring_data.nil?
|
|
106
|
+
|
|
107
|
+
# Parse with CFF2 CharString parser
|
|
108
|
+
require_relative "cff2/charstring_parser"
|
|
109
|
+
CharstringParser.new(
|
|
110
|
+
charstring_data,
|
|
111
|
+
@num_axes,
|
|
112
|
+
@global_subr_index,
|
|
113
|
+
nil, # local subrs (CFF2 may not have them)
|
|
114
|
+
0 # vsindex
|
|
115
|
+
).parse
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get all CharStrings
|
|
119
|
+
#
|
|
120
|
+
# @return [Array<CharstringParser>] Array of parsed CharStrings
|
|
121
|
+
def charstrings
|
|
122
|
+
return [] unless @charstrings_index
|
|
123
|
+
|
|
124
|
+
@charstrings_index.count.times.map do |glyph_id|
|
|
125
|
+
charstring_for_glyph(glyph_id)
|
|
126
|
+
end.compact
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if table is valid
|
|
130
|
+
#
|
|
131
|
+
# @return [Boolean] True if valid CFF2 table
|
|
132
|
+
def valid?
|
|
133
|
+
header.valid?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# Parse CFF2 header
|
|
139
|
+
#
|
|
140
|
+
# @return [Cff2Header] Parsed header
|
|
141
|
+
def parse_header
|
|
142
|
+
data = raw_data
|
|
143
|
+
return nil if data.nil? || data.bytesize < 5
|
|
144
|
+
|
|
145
|
+
Cff2Header.read(data.byteslice(0, 5))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Parse Global Subr INDEX
|
|
149
|
+
#
|
|
150
|
+
# @return [Cff::Index] Global subroutines INDEX
|
|
151
|
+
def parse_global_subr_index
|
|
152
|
+
# CFF2 has a Global Subr INDEX after the header
|
|
153
|
+
data = raw_data
|
|
154
|
+
return nil unless @header
|
|
155
|
+
|
|
156
|
+
offset = @header.header_size
|
|
157
|
+
|
|
158
|
+
# Global Subr INDEX follows header
|
|
159
|
+
io = StringIO.new(data)
|
|
160
|
+
io.seek(offset)
|
|
161
|
+
|
|
162
|
+
require_relative "cff/index"
|
|
163
|
+
Cff::Index.new(io, start_offset: offset)
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
warn "Failed to parse Global Subr INDEX: #{e.message}"
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Parse Top DICT
|
|
170
|
+
#
|
|
171
|
+
# @return [Hash] Top DICT data
|
|
172
|
+
def parse_top_dict
|
|
173
|
+
# CFF2 Top DICT follows the header (length specified in header)
|
|
174
|
+
data = raw_data
|
|
175
|
+
return {} unless @header
|
|
176
|
+
|
|
177
|
+
offset = @header.header_size
|
|
178
|
+
length = @header.top_dict_length
|
|
179
|
+
|
|
180
|
+
return {} if offset + length > data.bytesize
|
|
181
|
+
|
|
182
|
+
top_dict_data = data.byteslice(offset, length)
|
|
183
|
+
|
|
184
|
+
# Parse Top DICT (simplified for now)
|
|
185
|
+
# Full implementation would parse DICT operators
|
|
186
|
+
parse_dict(top_dict_data)
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
warn "Failed to parse Top DICT: #{e.message}"
|
|
189
|
+
{}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Parse CharStrings INDEX
|
|
193
|
+
#
|
|
194
|
+
# @return [Cff::Index, nil] CharStrings INDEX
|
|
195
|
+
def parse_charstrings_index
|
|
196
|
+
# CharStrings INDEX location is specified in Top DICT
|
|
197
|
+
# For now, we'll try to find it after Global Subr INDEX
|
|
198
|
+
data = raw_data
|
|
199
|
+
return nil unless @header
|
|
200
|
+
|
|
201
|
+
# Calculate offset after header + global subr
|
|
202
|
+
offset = @header.header_size
|
|
203
|
+
|
|
204
|
+
# Skip Global Subr INDEX
|
|
205
|
+
if @global_subr_index
|
|
206
|
+
offset += calculate_index_size(@global_subr_index)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Skip Top DICT
|
|
210
|
+
offset += @header.top_dict_length
|
|
211
|
+
|
|
212
|
+
io = StringIO.new(data)
|
|
213
|
+
io.seek(offset)
|
|
214
|
+
|
|
215
|
+
require_relative "cff/index"
|
|
216
|
+
Cff::Index.new(io, start_offset: offset)
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
warn "Failed to parse CharStrings INDEX: #{e.message}"
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Parse a DICT structure
|
|
223
|
+
#
|
|
224
|
+
# @param data [String] DICT data
|
|
225
|
+
# @return [Hash] Parsed operators and values
|
|
226
|
+
def parse_dict(data)
|
|
227
|
+
dict = {}
|
|
228
|
+
io = StringIO.new(data)
|
|
229
|
+
io.set_encoding(Encoding::BINARY)
|
|
230
|
+
|
|
231
|
+
operands = []
|
|
232
|
+
|
|
233
|
+
until io.eof?
|
|
234
|
+
byte = io.getbyte
|
|
235
|
+
|
|
236
|
+
if byte <= 21 && ![12, 28, 29, 30, 31].include?(byte)
|
|
237
|
+
# Operator
|
|
238
|
+
operator = byte
|
|
239
|
+
if operator == 12
|
|
240
|
+
operator = [12, io.getbyte]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
dict[operator] = operands.dup
|
|
244
|
+
operands.clear
|
|
245
|
+
else
|
|
246
|
+
# Operand (number)
|
|
247
|
+
io.pos -= 1
|
|
248
|
+
operands << read_dict_number(io)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
dict
|
|
253
|
+
rescue StandardError
|
|
254
|
+
{}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Read a number from DICT data
|
|
258
|
+
#
|
|
259
|
+
# @param io [StringIO] Input stream
|
|
260
|
+
# @return [Integer, Float] Number value
|
|
261
|
+
def read_dict_number(io)
|
|
262
|
+
byte = io.getbyte
|
|
263
|
+
|
|
264
|
+
case byte
|
|
265
|
+
when 28
|
|
266
|
+
# 3-byte signed integer
|
|
267
|
+
b1 = io.getbyte
|
|
268
|
+
b2 = io.getbyte
|
|
269
|
+
value = (b1 << 8) | b2
|
|
270
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
271
|
+
when 29
|
|
272
|
+
# 5-byte signed integer
|
|
273
|
+
bytes = io.read(4)
|
|
274
|
+
bytes.unpack1("l>")
|
|
275
|
+
when 30
|
|
276
|
+
# Real number (nibble-based)
|
|
277
|
+
read_real_number(io)
|
|
278
|
+
when 32..246
|
|
279
|
+
byte - 139
|
|
280
|
+
when 247..250
|
|
281
|
+
b2 = io.getbyte
|
|
282
|
+
(byte - 247) * 256 + b2 + 108
|
|
283
|
+
when 251..254
|
|
284
|
+
b2 = io.getbyte
|
|
285
|
+
-(byte - 251) * 256 - b2 - 108
|
|
286
|
+
else
|
|
287
|
+
0
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Read a real number from DICT
|
|
292
|
+
#
|
|
293
|
+
# @param io [StringIO] Input stream
|
|
294
|
+
# @return [Float] Real number
|
|
295
|
+
def read_real_number(io)
|
|
296
|
+
nibbles = []
|
|
297
|
+
loop do
|
|
298
|
+
byte = io.getbyte
|
|
299
|
+
nibbles << ((byte >> 4) & 0x0F)
|
|
300
|
+
nibbles << (byte & 0x0F)
|
|
301
|
+
break if (byte & 0x0F) == 0x0F
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Convert nibbles to string
|
|
305
|
+
str = ""
|
|
306
|
+
nibbles.each do |nibble|
|
|
307
|
+
case nibble
|
|
308
|
+
when 0..9 then str << nibble.to_s
|
|
309
|
+
when 0x0A then str << "."
|
|
310
|
+
when 0x0B then str << "E"
|
|
311
|
+
when 0x0C then str << "E-"
|
|
312
|
+
when 0x0E then str << "-"
|
|
313
|
+
when 0x0F then break
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
str.to_f
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Calculate size of an INDEX structure
|
|
321
|
+
#
|
|
322
|
+
# @param index [Cff::Index] INDEX structure
|
|
323
|
+
# @return [Integer] Size in bytes
|
|
324
|
+
def calculate_index_size(index)
|
|
325
|
+
return 2 if index.count.zero? # Just count field
|
|
326
|
+
|
|
327
|
+
# count (2) + offSize (1) + offsets + data
|
|
328
|
+
count = index.count
|
|
329
|
+
data_size = index.instance_variable_get(:@data_size) || 0
|
|
330
|
+
off_size = index.instance_variable_get(:@off_size) || 4
|
|
331
|
+
|
|
332
|
+
2 + 1 + ((count + 1) * off_size) + data_size
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Load CFF2 subcomponents
|
|
339
|
+
require_relative "cff2/charstring_parser"
|
|
340
|
+
require_relative "cff2/blend_operator"
|
|
341
|
+
require_relative "cff2/operand_stack"
|
|
342
|
+
require_relative "cff2/table_reader"
|
|
343
|
+
require_relative "cff2/variation_data_extractor"
|
|
344
|
+
require_relative "cff2/region_matcher"
|
|
345
|
+
require_relative "cff2/private_dict_blend_handler"
|
|
346
|
+
require_relative "cff2/table_builder"
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../binary/base_record"
|
|
5
|
+
require_relative "../variation/tuple_variation_header"
|
|
6
|
+
|
|
7
|
+
module Fontisan
|
|
8
|
+
module Tables
|
|
9
|
+
# Parser for the 'cvar' (CVT Variations) table
|
|
10
|
+
#
|
|
11
|
+
# The cvar table provides variation data for the Control Value Table (CVT)
|
|
12
|
+
# in TrueType variable fonts with hinting. The CVT contains scalar values
|
|
13
|
+
# that are referenced by TrueType instructions for grid-fitting.
|
|
14
|
+
#
|
|
15
|
+
# Like gvar, this table uses a TupleVariationStore structure with packed
|
|
16
|
+
# delta values rather than ItemVariationStore.
|
|
17
|
+
#
|
|
18
|
+
# Reference: OpenType specification, cvar table
|
|
19
|
+
#
|
|
20
|
+
# @example Reading a cvar table
|
|
21
|
+
# data = font.table_data("cvar")
|
|
22
|
+
# cvar = Fontisan::Tables::Cvar.read(data)
|
|
23
|
+
# cvt_deltas = cvar.cvt_variations
|
|
24
|
+
class Cvar < Binary::BaseRecord
|
|
25
|
+
uint16 :major_version
|
|
26
|
+
uint16 :minor_version
|
|
27
|
+
uint16 :tuple_variation_count
|
|
28
|
+
uint16 :data_offset
|
|
29
|
+
|
|
30
|
+
# Get version as a float
|
|
31
|
+
#
|
|
32
|
+
# @return [Float] Version number (e.g., 1.0)
|
|
33
|
+
def version
|
|
34
|
+
major_version + (minor_version / 10.0)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get tuple count
|
|
38
|
+
#
|
|
39
|
+
# @return [Integer] Number of tuple variations
|
|
40
|
+
def tuple_count
|
|
41
|
+
tuple_variation_count & 0x0FFF
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check if using shared point numbers
|
|
45
|
+
#
|
|
46
|
+
# @return [Boolean] True if shared points
|
|
47
|
+
def shared_point_numbers?
|
|
48
|
+
(tuple_variation_count & 0x8000) != 0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get axis count from fvar table (needs to be provided externally)
|
|
52
|
+
# This is a placeholder that should be set by the caller
|
|
53
|
+
attr_accessor :axis_count
|
|
54
|
+
|
|
55
|
+
# Parse tuple variation headers
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Hash>] Array of tuple information
|
|
58
|
+
def tuple_variations
|
|
59
|
+
return @tuple_variations if @tuple_variations
|
|
60
|
+
return @tuple_variations = [] if tuple_count.zero?
|
|
61
|
+
|
|
62
|
+
data = raw_data
|
|
63
|
+
# Tuple records start after header (8 bytes)
|
|
64
|
+
offset = 8
|
|
65
|
+
|
|
66
|
+
count = tuple_count
|
|
67
|
+
tuples = []
|
|
68
|
+
|
|
69
|
+
count.times do |_i|
|
|
70
|
+
break if offset + 4 > data.bytesize
|
|
71
|
+
|
|
72
|
+
header_data = data.byteslice(offset, 4)
|
|
73
|
+
header = Variation::TupleVariationHeader.read(header_data)
|
|
74
|
+
offset += 4
|
|
75
|
+
|
|
76
|
+
tuple_info = {
|
|
77
|
+
data_size: header.variation_data_size,
|
|
78
|
+
embedded_peak: header.embedded_peak_tuple?,
|
|
79
|
+
intermediate: header.intermediate_region?,
|
|
80
|
+
private_points: header.private_point_numbers?,
|
|
81
|
+
shared_index: header.shared_tuple_index,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Read peak tuple if embedded
|
|
85
|
+
if header.embedded_peak_tuple? && axis_count
|
|
86
|
+
peak = Array.new(axis_count) do
|
|
87
|
+
next nil if offset + 2 > data.bytesize
|
|
88
|
+
|
|
89
|
+
coord_data = data.byteslice(offset, 2)
|
|
90
|
+
offset += 2
|
|
91
|
+
|
|
92
|
+
value = coord_data.unpack1("n")
|
|
93
|
+
signed = value > 0x7FFF ? value - 0x10000 : value
|
|
94
|
+
signed / 16384.0
|
|
95
|
+
end.compact
|
|
96
|
+
tuple_info[:peak] = peak
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Read intermediate region if present
|
|
100
|
+
if header.intermediate_region? && axis_count
|
|
101
|
+
start_tuple = Array.new(axis_count) do
|
|
102
|
+
next nil if offset + 2 > data.bytesize
|
|
103
|
+
|
|
104
|
+
coord_data = data.byteslice(offset, 2)
|
|
105
|
+
offset += 2
|
|
106
|
+
|
|
107
|
+
value = coord_data.unpack1("n")
|
|
108
|
+
signed = value > 0x7FFF ? value - 0x10000 : value
|
|
109
|
+
signed / 16384.0
|
|
110
|
+
end.compact
|
|
111
|
+
|
|
112
|
+
end_tuple = Array.new(axis_count) do
|
|
113
|
+
next nil if offset + 2 > data.bytesize
|
|
114
|
+
|
|
115
|
+
coord_data = data.byteslice(offset, 2)
|
|
116
|
+
offset += 2
|
|
117
|
+
|
|
118
|
+
value = coord_data.unpack1("n")
|
|
119
|
+
signed = value > 0x7FFF ? value - 0x10000 : value
|
|
120
|
+
signed / 16384.0
|
|
121
|
+
end.compact
|
|
122
|
+
|
|
123
|
+
tuple_info[:start] = start_tuple
|
|
124
|
+
tuple_info[:end] = end_tuple
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
tuples << tuple_info
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
@tuple_variations = tuples
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
warn "Failed to parse cvar tuple variations: #{e.message}"
|
|
133
|
+
@tuple_variations = []
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get variation data section
|
|
137
|
+
#
|
|
138
|
+
# @return [String, nil] Raw variation data
|
|
139
|
+
def variation_data
|
|
140
|
+
return @variation_data if defined?(@variation_data)
|
|
141
|
+
|
|
142
|
+
data = raw_data
|
|
143
|
+
offset = data_offset
|
|
144
|
+
|
|
145
|
+
return @variation_data = nil if offset >= data.bytesize
|
|
146
|
+
|
|
147
|
+
@variation_data = data.byteslice(offset..-1)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Parse CVT deltas for a specific tuple
|
|
151
|
+
#
|
|
152
|
+
# This is a simplified parser that returns the raw delta data.
|
|
153
|
+
# Full delta unpacking would require knowing point counts and
|
|
154
|
+
# delta formats.
|
|
155
|
+
#
|
|
156
|
+
# @param tuple_index [Integer] Tuple index
|
|
157
|
+
# @return [Hash, nil] Tuple info with data offset
|
|
158
|
+
def tuple_variation_data(tuple_index)
|
|
159
|
+
return nil if tuple_index >= tuple_count
|
|
160
|
+
|
|
161
|
+
tuples = tuple_variations
|
|
162
|
+
return nil if tuple_index >= tuples.length
|
|
163
|
+
|
|
164
|
+
tuple = tuples[tuple_index]
|
|
165
|
+
|
|
166
|
+
# Calculate data offset for this tuple
|
|
167
|
+
# This is complex and requires walking through all previous tuples
|
|
168
|
+
# For now, return tuple metadata
|
|
169
|
+
{
|
|
170
|
+
tuple: tuple,
|
|
171
|
+
data_size: tuple[:data_size],
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get summary of CVT variations
|
|
176
|
+
#
|
|
177
|
+
# @return [Hash] Summary information
|
|
178
|
+
def summary
|
|
179
|
+
{
|
|
180
|
+
version: version,
|
|
181
|
+
tuple_count: tuple_count,
|
|
182
|
+
shared_points: shared_point_numbers?,
|
|
183
|
+
data_offset: data_offset,
|
|
184
|
+
tuples: tuple_variations.map do |t|
|
|
185
|
+
{
|
|
186
|
+
embedded_peak: t[:embedded_peak],
|
|
187
|
+
intermediate: t[:intermediate],
|
|
188
|
+
private_points: t[:private_points],
|
|
189
|
+
peak: t[:peak],
|
|
190
|
+
}
|
|
191
|
+
end,
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Check if table is valid
|
|
196
|
+
#
|
|
197
|
+
# @return [Boolean] True if valid
|
|
198
|
+
def valid?
|
|
199
|
+
major_version == 1 && minor_version.zero?
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
data/lib/fontisan/tables/fvar.rb
CHANGED
|
@@ -89,7 +89,7 @@ module Fontisan
|
|
|
89
89
|
return @axes = [] if axis_count.zero?
|
|
90
90
|
|
|
91
91
|
# Get the full data buffer as binary string
|
|
92
|
-
data =
|
|
92
|
+
data = raw_data
|
|
93
93
|
|
|
94
94
|
@axes = Array.new(axis_count) do |i|
|
|
95
95
|
offset = axes_array_offset + (i * axis_size)
|
|
@@ -106,7 +106,7 @@ module Fontisan
|
|
|
106
106
|
return @instances = [] if instance_count.zero?
|
|
107
107
|
|
|
108
108
|
# Get the full data buffer as binary string
|
|
109
|
-
data =
|
|
109
|
+
data = raw_data
|
|
110
110
|
|
|
111
111
|
# Calculate instance data offset (after all axes)
|
|
112
112
|
instance_offset = axes_array_offset + (axis_count * axis_size)
|