fontisan 0.1.0 → 0.2.0
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 +529 -65
- data/Gemfile +1 -0
- data/LICENSE +5 -1
- data/README.adoc +1301 -275
- data/Rakefile +27 -2
- 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 +309 -0
- data/lib/fontisan/collection/builder.rb +260 -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 +241 -0
- data/lib/fontisan/collection/writer.rb +306 -0
- data/lib/fontisan/commands/base_command.rb +8 -1
- data/lib/fontisan/commands/convert_command.rb +291 -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 +295 -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 +178 -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 +69 -0
- data/lib/fontisan/converters/conversion_strategy.rb +96 -0
- data/lib/fontisan/converters/format_converter.rb +259 -0
- data/lib/fontisan/converters/outline_converter.rb +936 -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 +121 -12
- data/lib/fontisan/font_writer.rb +301 -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 +177 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +185 -0
- data/lib/fontisan/hints/postscript_hint_extractor.rb +254 -0
- data/lib/fontisan/hints/truetype_hint_applier.rb +71 -0
- data/lib/fontisan/hints/truetype_hint_extractor.rb +162 -0
- data/lib/fontisan/loading_modes.rb +113 -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 +233 -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 +296 -10
- 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/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 +905 -0
- data/lib/fontisan/tables/cff/charstring_builder.rb +322 -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 +242 -0
- data/lib/fontisan/tables/cff/encoding.rb +274 -0
- data/lib/fontisan/tables/cff/header.rb +102 -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/private_dict.rb +284 -0
- data/lib/fontisan/tables/cff/top_dict.rb +236 -0
- data/lib/fontisan/tables/cff.rb +487 -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.rb +341 -0
- data/lib/fontisan/tables/cvar.rb +242 -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 +270 -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 +297 -11
- data/lib/fontisan/utilities/brotli_wrapper.rb +159 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +18 -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/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 +268 -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/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/validator.rb +345 -0
- data/lib/fontisan/variation/variation_context.rb +211 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +257 -0
- data/lib/fontisan/woff2/header.rb +101 -0
- data/lib/fontisan/woff2/table_transformer.rb +163 -0
- data/lib/fontisan/woff2_font.rb +712 -0
- data/lib/fontisan/woff_font.rb +483 -0
- data/lib/fontisan.rb +120 -0
- data/scripts/compare_stack_aware.rb +187 -0
- data/scripts/measure_optimization.rb +141 -0
- metadata +205 -4
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
# Represents a simple TrueType glyph with contours
|
|
6
|
+
#
|
|
7
|
+
# A simple glyph is defined by one or more contours, where each contour
|
|
8
|
+
# is a closed path made up of on-curve and off-curve points. The points
|
|
9
|
+
# are stored using delta encoding to save space.
|
|
10
|
+
#
|
|
11
|
+
# The glyph structure consists of:
|
|
12
|
+
# - Header: numberOfContours, xMin, yMin, xMax, yMax (10 bytes)
|
|
13
|
+
# - endPtsOfContours: array marking the last point of each contour
|
|
14
|
+
# - instructions: TrueType hinting instructions (optional)
|
|
15
|
+
# - flags: array of point flags (compressed with repeat counts)
|
|
16
|
+
# - xCoordinates: x-coordinates (delta-encoded, variable byte length)
|
|
17
|
+
# - yCoordinates: y-coordinates (delta-encoded, variable byte length)
|
|
18
|
+
#
|
|
19
|
+
# Point flags (8-bit) indicate:
|
|
20
|
+
# - Bit 0 (0x01): ON_CURVE_POINT - point is on the curve
|
|
21
|
+
# - Bit 1 (0x02): X_SHORT_VECTOR - x-coordinate is 1 byte
|
|
22
|
+
# - Bit 2 (0x04): Y_SHORT_VECTOR - y-coordinate is 1 byte
|
|
23
|
+
# - Bit 3 (0x08): REPEAT_FLAG - repeat this flag n times
|
|
24
|
+
# - Bit 4 (0x10): X_IS_SAME_OR_POSITIVE_X_SHORT - x value interpretation
|
|
25
|
+
# - Bit 5 (0x20): Y_IS_SAME_OR_POSITIVE_Y_SHORT - y value interpretation
|
|
26
|
+
#
|
|
27
|
+
# Reference: OpenType specification, glyf table - Simple Glyph Description
|
|
28
|
+
# https://docs.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description
|
|
29
|
+
class SimpleGlyph
|
|
30
|
+
# Flag constants
|
|
31
|
+
ON_CURVE_POINT = 0x01
|
|
32
|
+
X_SHORT_VECTOR = 0x02
|
|
33
|
+
Y_SHORT_VECTOR = 0x04
|
|
34
|
+
REPEAT_FLAG = 0x08
|
|
35
|
+
X_IS_SAME_OR_POSITIVE_X_SHORT = 0x10
|
|
36
|
+
Y_IS_SAME_OR_POSITIVE_Y_SHORT = 0x20
|
|
37
|
+
|
|
38
|
+
# Glyph header fields
|
|
39
|
+
attr_reader :glyph_id
|
|
40
|
+
attr_reader :num_contours, :x_min, :y_min, :x_max, :y_max,
|
|
41
|
+
:instruction_length, :instructions, :flags, :x_coordinates, :y_coordinates
|
|
42
|
+
|
|
43
|
+
# Glyph data fields
|
|
44
|
+
attr_reader :end_pts_of_contours
|
|
45
|
+
|
|
46
|
+
# Parse simple glyph data
|
|
47
|
+
#
|
|
48
|
+
# @param data [String] Binary glyph data
|
|
49
|
+
# @param glyph_id [Integer] Glyph ID for error reporting
|
|
50
|
+
# @return [SimpleGlyph] Parsed simple glyph
|
|
51
|
+
# @raise [Fontisan::CorruptedTableError] If data is insufficient or invalid
|
|
52
|
+
def self.parse(data, glyph_id)
|
|
53
|
+
glyph = new(glyph_id)
|
|
54
|
+
glyph.parse_data(data)
|
|
55
|
+
glyph
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Initialize a new simple glyph
|
|
59
|
+
#
|
|
60
|
+
# @param glyph_id [Integer] Glyph ID
|
|
61
|
+
def initialize(glyph_id)
|
|
62
|
+
@glyph_id = glyph_id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parse glyph data
|
|
66
|
+
#
|
|
67
|
+
# @param data [String] Binary glyph data
|
|
68
|
+
# @raise [Fontisan::CorruptedTableError] If parsing fails
|
|
69
|
+
def parse_data(data)
|
|
70
|
+
io = StringIO.new(data)
|
|
71
|
+
io.set_encoding(Encoding::BINARY)
|
|
72
|
+
|
|
73
|
+
parse_header(io)
|
|
74
|
+
parse_contour_ends(io)
|
|
75
|
+
parse_instructions(io)
|
|
76
|
+
parse_flags(io)
|
|
77
|
+
parse_coordinates(io)
|
|
78
|
+
|
|
79
|
+
validate_parsed_data!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if this is a simple glyph
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] Always true for SimpleGlyph
|
|
85
|
+
def simple?
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check if this is a compound glyph
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean] Always false for SimpleGlyph
|
|
92
|
+
def compound?
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if glyph has no outline data
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean] True if no contours
|
|
99
|
+
def empty?
|
|
100
|
+
num_contours.zero?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get bounding box as array
|
|
104
|
+
#
|
|
105
|
+
# @return [Array<Integer>] Bounding box [xMin, yMin, xMax, yMax]
|
|
106
|
+
def bounding_box
|
|
107
|
+
[x_min, y_min, x_max, y_max]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get total number of points
|
|
111
|
+
#
|
|
112
|
+
# @return [Integer] Total points in all contours
|
|
113
|
+
def num_points
|
|
114
|
+
return 0 if empty?
|
|
115
|
+
|
|
116
|
+
end_pts_of_contours.last + 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check if a specific point is on the curve
|
|
120
|
+
#
|
|
121
|
+
# @param index [Integer] Point index (0-based)
|
|
122
|
+
# @return [Boolean, nil] True if on curve, false if off curve, nil if invalid
|
|
123
|
+
def on_curve?(index)
|
|
124
|
+
return nil if index.negative? || index >= num_points
|
|
125
|
+
|
|
126
|
+
(flags[index] & ON_CURVE_POINT) != 0
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get contour for a specific point
|
|
130
|
+
#
|
|
131
|
+
# @param point_index [Integer] Point index (0-based)
|
|
132
|
+
# @return [Integer, nil] Contour index (0-based) or nil if invalid
|
|
133
|
+
def contour_for_point(point_index)
|
|
134
|
+
return nil if point_index.negative? || point_index >= num_points
|
|
135
|
+
|
|
136
|
+
end_pts_of_contours.index { |end_pt| point_index <= end_pt }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get all points for a specific contour
|
|
140
|
+
#
|
|
141
|
+
# @param contour_index [Integer] Contour index (0-based)
|
|
142
|
+
# @return [Array<Hash>, nil] Array of point hashes or nil if invalid
|
|
143
|
+
def points_for_contour(contour_index)
|
|
144
|
+
return nil if contour_index.negative? || contour_index >= num_contours
|
|
145
|
+
|
|
146
|
+
start_pt = contour_index.zero? ? 0 : end_pts_of_contours[contour_index - 1] + 1
|
|
147
|
+
end_pt = end_pts_of_contours[contour_index]
|
|
148
|
+
|
|
149
|
+
(start_pt..end_pt).map do |i|
|
|
150
|
+
{
|
|
151
|
+
x: x_coordinates[i],
|
|
152
|
+
y: y_coordinates[i],
|
|
153
|
+
on_curve: on_curve?(i),
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# Parse glyph header (10 bytes)
|
|
161
|
+
#
|
|
162
|
+
# @param io [StringIO] Input stream
|
|
163
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
164
|
+
def parse_header(io)
|
|
165
|
+
header = io.read(10)
|
|
166
|
+
if header.nil? || header.length < 10
|
|
167
|
+
raise Fontisan::CorruptedTableError,
|
|
168
|
+
"Insufficient header data for simple glyph #{glyph_id}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
values = header.unpack("n5")
|
|
172
|
+
@num_contours = to_signed_16(values[0])
|
|
173
|
+
@x_min = to_signed_16(values[1])
|
|
174
|
+
@y_min = to_signed_16(values[2])
|
|
175
|
+
@x_max = to_signed_16(values[3])
|
|
176
|
+
@y_max = to_signed_16(values[4])
|
|
177
|
+
|
|
178
|
+
if @num_contours.negative?
|
|
179
|
+
raise Fontisan::CorruptedTableError,
|
|
180
|
+
"Simple glyph #{glyph_id} has negative contour count: #{@num_contours}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Parse contour end points
|
|
185
|
+
#
|
|
186
|
+
# @param io [StringIO] Input stream
|
|
187
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
188
|
+
def parse_contour_ends(io)
|
|
189
|
+
return if num_contours.zero?
|
|
190
|
+
|
|
191
|
+
data = io.read(num_contours * 2)
|
|
192
|
+
if data.nil? || data.length < num_contours * 2
|
|
193
|
+
raise Fontisan::CorruptedTableError,
|
|
194
|
+
"Insufficient contour end data for simple glyph #{glyph_id}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
@end_pts_of_contours = data.unpack("n*")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Parse TrueType instructions
|
|
201
|
+
#
|
|
202
|
+
# @param io [StringIO] Input stream
|
|
203
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
204
|
+
def parse_instructions(io)
|
|
205
|
+
length_data = io.read(2)
|
|
206
|
+
if length_data.nil? || length_data.length < 2
|
|
207
|
+
raise Fontisan::CorruptedTableError,
|
|
208
|
+
"Insufficient instruction length data for simple glyph #{glyph_id}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
@instruction_length = length_data.unpack1("n")
|
|
212
|
+
|
|
213
|
+
if @instruction_length.positive?
|
|
214
|
+
@instructions = io.read(@instruction_length)
|
|
215
|
+
if @instructions.nil? || @instructions.length < @instruction_length
|
|
216
|
+
raise Fontisan::CorruptedTableError,
|
|
217
|
+
"Insufficient instruction data for simple glyph #{glyph_id}"
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
@instructions = "".b
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Parse flags with repeat compression
|
|
225
|
+
#
|
|
226
|
+
# Flags use run-length encoding: when REPEAT_FLAG is set,
|
|
227
|
+
# the next byte indicates how many times to repeat the flag.
|
|
228
|
+
#
|
|
229
|
+
# @param io [StringIO] Input stream
|
|
230
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
231
|
+
def parse_flags(io)
|
|
232
|
+
return if num_contours.zero?
|
|
233
|
+
|
|
234
|
+
total_points = num_points
|
|
235
|
+
@flags = []
|
|
236
|
+
|
|
237
|
+
while @flags.length < total_points
|
|
238
|
+
flag_byte = io.read(1)
|
|
239
|
+
if flag_byte.nil? || flag_byte.empty?
|
|
240
|
+
raise Fontisan::CorruptedTableError,
|
|
241
|
+
"Insufficient flag data for simple glyph #{glyph_id}"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
flag = flag_byte.unpack1("C")
|
|
245
|
+
@flags << flag
|
|
246
|
+
|
|
247
|
+
# Check for repeat flag
|
|
248
|
+
if (flag & REPEAT_FLAG) != 0
|
|
249
|
+
repeat_count = io.read(1)
|
|
250
|
+
if repeat_count.nil? || repeat_count.empty?
|
|
251
|
+
raise Fontisan::CorruptedTableError,
|
|
252
|
+
"Missing repeat count for simple glyph #{glyph_id}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
count = repeat_count.unpack1("C")
|
|
256
|
+
count.times { @flags << flag }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if @flags.length != total_points
|
|
261
|
+
raise Fontisan::CorruptedTableError,
|
|
262
|
+
"Flag count mismatch for simple glyph #{glyph_id}: " \
|
|
263
|
+
"expected #{total_points}, got #{@flags.length}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Parse x and y coordinates with delta encoding
|
|
268
|
+
#
|
|
269
|
+
# Coordinates are delta-encoded from the previous point.
|
|
270
|
+
# The flag indicates whether coordinates are 1 byte (short) or 2 bytes.
|
|
271
|
+
# For short coordinates, another flag bit indicates sign.
|
|
272
|
+
# For long coordinates, a flag bit indicates if the value is same as previous (delta=0).
|
|
273
|
+
#
|
|
274
|
+
# @param io [StringIO] Input stream
|
|
275
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
276
|
+
def parse_coordinates(io)
|
|
277
|
+
return if num_contours.zero?
|
|
278
|
+
|
|
279
|
+
@x_coordinates = parse_coordinate_array(io, :x)
|
|
280
|
+
@y_coordinates = parse_coordinate_array(io, :y)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Parse a coordinate array (x or y)
|
|
284
|
+
#
|
|
285
|
+
# @param io [StringIO] Input stream
|
|
286
|
+
# @param axis [:x, :y] Which axis to parse
|
|
287
|
+
# @return [Array<Integer>] Absolute coordinates
|
|
288
|
+
# @raise [Fontisan::CorruptedTableError] If insufficient data
|
|
289
|
+
def parse_coordinate_array(io, axis)
|
|
290
|
+
short_flag = axis == :x ? X_SHORT_VECTOR : Y_SHORT_VECTOR
|
|
291
|
+
same_or_positive_flag = axis == :x ? X_IS_SAME_OR_POSITIVE_X_SHORT : Y_IS_SAME_OR_POSITIVE_Y_SHORT
|
|
292
|
+
|
|
293
|
+
coordinates = []
|
|
294
|
+
current = 0
|
|
295
|
+
|
|
296
|
+
flags.each_with_index do |flag, i|
|
|
297
|
+
if (flag & short_flag) != 0
|
|
298
|
+
# Short coordinate (1 byte, unsigned)
|
|
299
|
+
byte = io.read(1)
|
|
300
|
+
if byte.nil? || byte.empty?
|
|
301
|
+
raise Fontisan::CorruptedTableError,
|
|
302
|
+
"Insufficient #{axis} coordinate data for simple glyph #{glyph_id} at point #{i}"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
value = byte.unpack1("C")
|
|
306
|
+
# Sign determination: if same_or_positive_flag is set, value is positive; otherwise negative
|
|
307
|
+
delta = (flag & same_or_positive_flag).zero? ? -value : value
|
|
308
|
+
elsif (flag & same_or_positive_flag) != 0
|
|
309
|
+
# Same as previous (delta = 0)
|
|
310
|
+
delta = 0
|
|
311
|
+
else
|
|
312
|
+
# Long coordinate (2 bytes, signed)
|
|
313
|
+
bytes = io.read(2)
|
|
314
|
+
if bytes.nil? || bytes.length < 2
|
|
315
|
+
raise Fontisan::CorruptedTableError,
|
|
316
|
+
"Insufficient #{axis} coordinate data for simple glyph #{glyph_id} at point #{i}"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
delta = to_signed_16(bytes.unpack1("n"))
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
current += delta
|
|
323
|
+
coordinates << current
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
coordinates
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Validate parsed data consistency
|
|
330
|
+
#
|
|
331
|
+
# @raise [Fontisan::CorruptedTableError] If validation fails
|
|
332
|
+
def validate_parsed_data!
|
|
333
|
+
return if num_contours.zero?
|
|
334
|
+
|
|
335
|
+
# Check that we have correct number of points
|
|
336
|
+
expected_points = num_points
|
|
337
|
+
if flags.length != expected_points
|
|
338
|
+
raise Fontisan::CorruptedTableError,
|
|
339
|
+
"Point count mismatch for simple glyph #{glyph_id}: " \
|
|
340
|
+
"expected #{expected_points} points, got #{flags.length} flags"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
if x_coordinates.length != expected_points
|
|
344
|
+
raise Fontisan::CorruptedTableError,
|
|
345
|
+
"X coordinate count mismatch for simple glyph #{glyph_id}: " \
|
|
346
|
+
"expected #{expected_points}, got #{x_coordinates.length}"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
if y_coordinates.length != expected_points
|
|
350
|
+
raise Fontisan::CorruptedTableError,
|
|
351
|
+
"Y coordinate count mismatch for simple glyph #{glyph_id}: " \
|
|
352
|
+
"expected #{expected_points}, got #{y_coordinates.length}"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Check that contour end points are monotonically increasing
|
|
356
|
+
end_pts_of_contours.each_cons(2) do |prev, curr|
|
|
357
|
+
if curr <= prev
|
|
358
|
+
raise Fontisan::CorruptedTableError,
|
|
359
|
+
"Invalid contour end points for simple glyph #{glyph_id}: " \
|
|
360
|
+
"not monotonically increasing"
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Check that last contour end point matches total points
|
|
365
|
+
last_end_pt = end_pts_of_contours.last
|
|
366
|
+
if last_end_pt != expected_points - 1
|
|
367
|
+
raise Fontisan::CorruptedTableError,
|
|
368
|
+
"Last contour end point mismatch for simple glyph #{glyph_id}: " \
|
|
369
|
+
"expected #{expected_points - 1}, got #{last_end_pt}"
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Convert unsigned 16-bit value to signed
|
|
374
|
+
#
|
|
375
|
+
# @param value [Integer] Unsigned 16-bit value
|
|
376
|
+
# @return [Integer] Signed 16-bit value
|
|
377
|
+
def to_signed_16(value)
|
|
378
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../binary/base_record"
|
|
4
|
+
require_relative "glyf/simple_glyph"
|
|
5
|
+
require_relative "glyf/compound_glyph"
|
|
6
|
+
require_relative "glyf/curve_converter"
|
|
7
|
+
require_relative "glyf/glyph_builder"
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Tables
|
|
11
|
+
# Parser for the 'glyf' (Glyph Data) table
|
|
12
|
+
#
|
|
13
|
+
# The glyf table contains TrueType glyph outline data. Each glyph is
|
|
14
|
+
# described by either a simple glyph (with contours and points) or a
|
|
15
|
+
# compound glyph (composed of other glyphs with transformations).
|
|
16
|
+
#
|
|
17
|
+
# The glyf table is accessed via offsets from the loca table, which
|
|
18
|
+
# provides the byte offset and size for each glyph. Empty glyphs
|
|
19
|
+
# (e.g., space characters) have zero size in loca.
|
|
20
|
+
#
|
|
21
|
+
# Glyph types are determined by numberOfContours:
|
|
22
|
+
# - numberOfContours >= 0: Simple glyph with that many contours
|
|
23
|
+
# - numberOfContours == -1: Compound glyph composed of other glyphs
|
|
24
|
+
#
|
|
25
|
+
# The glyf table is context-dependent and requires:
|
|
26
|
+
# - loca table (for glyph offsets and sizes)
|
|
27
|
+
# - head table (for coordinate interpretation and flags)
|
|
28
|
+
#
|
|
29
|
+
# Reference: OpenType specification, glyf table
|
|
30
|
+
# https://docs.microsoft.com/en-us/typography/opentype/spec/glyf
|
|
31
|
+
#
|
|
32
|
+
# @example Accessing a glyph
|
|
33
|
+
# # Get required tables first
|
|
34
|
+
# head = font.table('head')
|
|
35
|
+
# loca = font.table('loca')
|
|
36
|
+
# loca.parse_with_context(head.index_to_loc_format, maxp.num_glyphs)
|
|
37
|
+
#
|
|
38
|
+
# # Parse glyf table
|
|
39
|
+
# data = font.read_table_data('glyf')
|
|
40
|
+
# glyf = Fontisan::Tables::Glyf.read(data)
|
|
41
|
+
#
|
|
42
|
+
# # Get a specific glyph
|
|
43
|
+
# glyph = glyf.glyph_for(42, loca, head)
|
|
44
|
+
# puts glyph.simple? ? "Simple glyph" : "Compound glyph"
|
|
45
|
+
# puts glyph.bounding_box # => [xMin, yMin, xMax, yMax]
|
|
46
|
+
class Glyf < Binary::BaseRecord
|
|
47
|
+
# Store the raw data for deferred parsing
|
|
48
|
+
attr_accessor :raw_data
|
|
49
|
+
|
|
50
|
+
# Cache for parsed glyphs
|
|
51
|
+
# @return [Hash<Integer, SimpleGlyph|CompoundGlyph>]
|
|
52
|
+
attr_reader :glyphs_cache
|
|
53
|
+
|
|
54
|
+
# Override read to capture raw data
|
|
55
|
+
#
|
|
56
|
+
# @param io [IO, String] Input data
|
|
57
|
+
# @return [Glyf] Parsed table instance
|
|
58
|
+
def self.read(io)
|
|
59
|
+
instance = new
|
|
60
|
+
|
|
61
|
+
# Initialize cache
|
|
62
|
+
instance.instance_variable_set(:@glyphs_cache, {})
|
|
63
|
+
|
|
64
|
+
# Handle nil or empty data gracefully
|
|
65
|
+
instance.raw_data = if io.nil?
|
|
66
|
+
"".b
|
|
67
|
+
elsif io.is_a?(String)
|
|
68
|
+
io
|
|
69
|
+
else
|
|
70
|
+
io.read || "".b
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
instance
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get glyph data for a specific glyph ID
|
|
77
|
+
#
|
|
78
|
+
# This method retrieves and parses the glyph at the specified ID.
|
|
79
|
+
# It uses the loca table to determine the offset and size, then
|
|
80
|
+
# parses the glyph data to create either a SimpleGlyph or CompoundGlyph.
|
|
81
|
+
#
|
|
82
|
+
# Results are cached to avoid re-parsing the same glyph multiple times.
|
|
83
|
+
#
|
|
84
|
+
# @param glyph_id [Integer] Glyph ID (0-based, 0 is .notdef)
|
|
85
|
+
# @param loca [Loca] Parsed loca table with offsets
|
|
86
|
+
# @param head [Head] Parsed head table for coordinate interpretation
|
|
87
|
+
# @return [SimpleGlyph, CompoundGlyph, nil] Parsed glyph or nil if empty/invalid
|
|
88
|
+
# @raise [ArgumentError] If loca is not parsed or tables are invalid
|
|
89
|
+
# @raise [Fontisan::CorruptedTableError] If glyph data is corrupted
|
|
90
|
+
#
|
|
91
|
+
# @example Getting a simple glyph
|
|
92
|
+
# glyph = glyf.glyph_for(65, loca, head) # 'A' character
|
|
93
|
+
# if glyph.simple?
|
|
94
|
+
# puts "Contours: #{glyph.num_contours}"
|
|
95
|
+
# puts "Points: #{glyph.x_coordinates.length}"
|
|
96
|
+
# end
|
|
97
|
+
def glyph_for(glyph_id, loca, head)
|
|
98
|
+
# Return cached glyph if available
|
|
99
|
+
return glyphs_cache[glyph_id] if glyphs_cache.key?(glyph_id)
|
|
100
|
+
|
|
101
|
+
# Validate inputs
|
|
102
|
+
validate_context!(loca, head, glyph_id)
|
|
103
|
+
|
|
104
|
+
# Get offset and size from loca table
|
|
105
|
+
offset = loca.offset_for(glyph_id)
|
|
106
|
+
size = loca.size_of(glyph_id)
|
|
107
|
+
|
|
108
|
+
# Empty glyph (e.g., space character)
|
|
109
|
+
if size.nil? || size.zero?
|
|
110
|
+
glyphs_cache[glyph_id] = nil
|
|
111
|
+
return nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Validate offset and size
|
|
115
|
+
if offset + size > raw_data.length
|
|
116
|
+
raise Fontisan::CorruptedTableError,
|
|
117
|
+
"Glyph #{glyph_id} extends beyond glyf table: " \
|
|
118
|
+
"offset=#{offset}, size=#{size}, table_size=#{raw_data.length}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Extract glyph data
|
|
122
|
+
glyph_data = raw_data[offset, size]
|
|
123
|
+
|
|
124
|
+
# Parse glyph
|
|
125
|
+
glyph = parse_glyph_data(glyph_data, glyph_id)
|
|
126
|
+
glyphs_cache[glyph_id] = glyph
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Clear the glyph cache to free memory
|
|
130
|
+
#
|
|
131
|
+
# This is useful for long-running processes that parse many glyphs
|
|
132
|
+
# but don't need to keep them all in memory.
|
|
133
|
+
#
|
|
134
|
+
# @return [void]
|
|
135
|
+
def clear_cache
|
|
136
|
+
glyphs_cache.clear
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get the number of cached glyphs
|
|
140
|
+
#
|
|
141
|
+
# @return [Integer] Number of glyphs in cache
|
|
142
|
+
def cache_size
|
|
143
|
+
glyphs_cache.size
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Check if a glyph is cached
|
|
147
|
+
#
|
|
148
|
+
# @param glyph_id [Integer] Glyph ID to check
|
|
149
|
+
# @return [Boolean] True if glyph is cached
|
|
150
|
+
def cached?(glyph_id)
|
|
151
|
+
glyphs_cache.key?(glyph_id)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Lazy initialization of glyphs cache
|
|
155
|
+
#
|
|
156
|
+
# @return [Hash] The glyphs cache
|
|
157
|
+
def glyphs_cache
|
|
158
|
+
@glyphs_cache ||= {}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# Validate context and glyph ID
|
|
164
|
+
#
|
|
165
|
+
# @param loca [Loca] Loca table
|
|
166
|
+
# @param head [Head] Head table
|
|
167
|
+
# @param glyph_id [Integer] Glyph ID
|
|
168
|
+
# @raise [ArgumentError] If validation fails
|
|
169
|
+
def validate_context!(loca, head, glyph_id)
|
|
170
|
+
unless loca.respond_to?(:offset_for) && loca.respond_to?(:size_of)
|
|
171
|
+
raise ArgumentError,
|
|
172
|
+
"loca must be a parsed Loca table with offset_for and size_of methods"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
unless loca.parsed?
|
|
176
|
+
raise ArgumentError,
|
|
177
|
+
"loca table must be parsed with parse_with_context before use"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
unless head.respond_to?(:units_per_em)
|
|
181
|
+
raise ArgumentError,
|
|
182
|
+
"head must be a parsed Head table"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if glyph_id.nil? || glyph_id.negative?
|
|
186
|
+
raise ArgumentError,
|
|
187
|
+
"glyph_id must be >= 0, got: #{glyph_id.inspect}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
if glyph_id >= loca.num_glyphs
|
|
191
|
+
raise ArgumentError,
|
|
192
|
+
"glyph_id #{glyph_id} exceeds number of glyphs (#{loca.num_glyphs})"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Parse glyph data into SimpleGlyph or CompoundGlyph
|
|
197
|
+
#
|
|
198
|
+
# @param data [String] Binary glyph data
|
|
199
|
+
# @param glyph_id [Integer] Glyph ID
|
|
200
|
+
# @return [SimpleGlyph, CompoundGlyph] Parsed glyph
|
|
201
|
+
# @raise [Fontisan::CorruptedTableError] If data is insufficient or invalid
|
|
202
|
+
def parse_glyph_data(data, glyph_id)
|
|
203
|
+
# Need at least 10 bytes for glyph header
|
|
204
|
+
if data.length < 10
|
|
205
|
+
raise Fontisan::CorruptedTableError,
|
|
206
|
+
"Insufficient glyph data for glyph #{glyph_id}: " \
|
|
207
|
+
"need at least 10 bytes, got #{data.length}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Parse numberOfContours (signed 16-bit) at offset 0
|
|
211
|
+
num_contours_raw = data[0, 2].unpack1("n")
|
|
212
|
+
num_contours = to_signed_16(num_contours_raw)
|
|
213
|
+
|
|
214
|
+
# Determine glyph type and parse accordingly
|
|
215
|
+
if num_contours >= 0
|
|
216
|
+
SimpleGlyph.parse(data, glyph_id)
|
|
217
|
+
elsif num_contours == -1
|
|
218
|
+
CompoundGlyph.parse(data, glyph_id)
|
|
219
|
+
else
|
|
220
|
+
raise Fontisan::CorruptedTableError,
|
|
221
|
+
"Invalid numberOfContours for glyph #{glyph_id}: #{num_contours}. " \
|
|
222
|
+
"Must be >= 0 for simple glyphs or -1 for compound glyphs."
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Convert unsigned 16-bit value to signed
|
|
227
|
+
#
|
|
228
|
+
# @param value [Integer] Unsigned 16-bit value
|
|
229
|
+
# @return [Integer] Signed 16-bit value
|
|
230
|
+
def to_signed_16(value)
|
|
231
|
+
value > 0x7FFF ? value - 0x10000 : value
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|