fontisan 0.2.1 → 0.2.3
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 +58 -392
- data/README.adoc +1509 -1430
- data/Rakefile +3 -2
- data/benchmark/variation_quick_bench.rb +4 -4
- data/docs/FONT_HINTING.adoc +562 -0
- data/docs/VARIABLE_FONT_OPERATIONS.adoc +599 -0
- data/lib/fontisan/base_collection.rb +296 -0
- data/lib/fontisan/cli.rb +10 -3
- data/lib/fontisan/collection/builder.rb +2 -1
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/commands/base_command.rb +5 -2
- data/lib/fontisan/commands/convert_command.rb +6 -2
- data/lib/fontisan/commands/info_command.rb +129 -5
- data/lib/fontisan/commands/instance_command.rb +8 -7
- data/lib/fontisan/commands/validate_command.rb +4 -1
- data/lib/fontisan/constants.rb +24 -24
- data/lib/fontisan/converters/format_converter.rb +8 -4
- data/lib/fontisan/converters/outline_converter.rb +21 -16
- data/lib/fontisan/converters/woff_writer.rb +8 -3
- data/lib/fontisan/font_loader.rb +120 -30
- data/lib/fontisan/font_writer.rb +2 -0
- data/lib/fontisan/formatters/text_formatter.rb +116 -19
- data/lib/fontisan/hints/hint_converter.rb +43 -47
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +1 -3
- data/lib/fontisan/hints/postscript_hint_extractor.rb +78 -43
- data/lib/fontisan/hints/truetype_hint_extractor.rb +22 -26
- data/lib/fontisan/hints/truetype_instruction_analyzer.rb +261 -0
- data/lib/fontisan/hints/truetype_instruction_generator.rb +266 -0
- data/lib/fontisan/loading_modes.rb +4 -4
- data/lib/fontisan/models/collection_brief_info.rb +37 -0
- data/lib/fontisan/models/collection_info.rb +6 -1
- data/lib/fontisan/models/font_export.rb +2 -2
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +22 -23
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/models/validation_report.rb +1 -1
- data/lib/fontisan/open_type_collection.rb +17 -220
- data/lib/fontisan/open_type_font.rb +3 -1
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/output_writer.rb +8 -3
- data/lib/fontisan/pipeline/transformation_pipeline.rb +8 -3
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +38 -12
- data/lib/fontisan/tables/cff/charstring_parser.rb +23 -11
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +14 -14
- data/lib/fontisan/tables/cff/dict_builder.rb +4 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +6 -4
- data/lib/fontisan/tables/cff/offset_recalculator.rb +1 -1
- data/lib/fontisan/tables/cff/private_dict_writer.rb +10 -4
- data/lib/fontisan/tables/cff/table_builder.rb +1 -1
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +7 -6
- data/lib/fontisan/tables/cff2/region_matcher.rb +2 -2
- data/lib/fontisan/tables/cff2/table_builder.rb +26 -20
- data/lib/fontisan/tables/cff2/table_reader.rb +35 -33
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +2 -2
- data/lib/fontisan/tables/cff2.rb +1 -1
- data/lib/fontisan/tables/glyf/compound_glyph_resolver.rb +2 -1
- data/lib/fontisan/tables/glyf/curve_converter.rb +10 -4
- data/lib/fontisan/tables/glyf/glyph_builder.rb +27 -10
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_collection.rb +29 -113
- data/lib/fontisan/true_type_font.rb +3 -1
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +2 -1
- data/lib/fontisan/variation/delta_applier.rb +2 -1
- data/lib/fontisan/variation/inspector.rb +2 -1
- data/lib/fontisan/variation/instance_generator.rb +2 -1
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/variation_preserver.rb +4 -1
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/glyf_transformer.rb +57 -30
- data/lib/fontisan/woff2_font.rb +31 -15
- data/lib/fontisan.rb +42 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +9 -2
|
@@ -61,7 +61,7 @@ module Fontisan
|
|
|
61
61
|
major_version: read_uint8,
|
|
62
62
|
minor_version: read_uint8,
|
|
63
63
|
header_size: read_uint8,
|
|
64
|
-
top_dict_length: read_uint16
|
|
64
|
+
top_dict_length: read_uint16,
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
# Validate CFF2 version
|
|
@@ -105,7 +105,7 @@ module Fontisan
|
|
|
105
105
|
# Parse Variable Store structure
|
|
106
106
|
@variable_store = {
|
|
107
107
|
regions: read_region_list,
|
|
108
|
-
item_variation_data: read_item_variation_data
|
|
108
|
+
item_variation_data: read_item_variation_data,
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
@variable_store
|
|
@@ -140,7 +140,7 @@ module Fontisan
|
|
|
140
140
|
axes << {
|
|
141
141
|
start_coord: read_f2dot14,
|
|
142
142
|
peak_coord: read_f2dot14,
|
|
143
|
-
end_coord: read_f2dot14
|
|
143
|
+
end_coord: read_f2dot14,
|
|
144
144
|
}
|
|
145
145
|
end
|
|
146
146
|
|
|
@@ -158,13 +158,11 @@ module Fontisan
|
|
|
158
158
|
|
|
159
159
|
item_variation_data = []
|
|
160
160
|
|
|
161
|
-
data_count.times do |
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
# break
|
|
167
|
-
end
|
|
161
|
+
data_count.times do |_idx|
|
|
162
|
+
item_data = read_single_item_variation_data
|
|
163
|
+
item_variation_data << item_data
|
|
164
|
+
rescue EOFError
|
|
165
|
+
# break
|
|
168
166
|
end
|
|
169
167
|
|
|
170
168
|
item_variation_data
|
|
@@ -186,32 +184,32 @@ module Fontisan
|
|
|
186
184
|
|
|
187
185
|
# Read delta sets
|
|
188
186
|
delta_sets = []
|
|
189
|
-
item_count.times do |
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
delta_sets << deltas
|
|
206
|
-
rescue EOFError => e
|
|
207
|
-
# break
|
|
187
|
+
item_count.times do |_item_idx|
|
|
188
|
+
deltas = []
|
|
189
|
+
|
|
190
|
+
# Short deltas (16-bit)
|
|
191
|
+
short_delta_count.times do
|
|
192
|
+
break if @io.eof?
|
|
193
|
+
|
|
194
|
+
deltas << read_int16
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Long deltas (8-bit) for remaining regions
|
|
198
|
+
(region_index_count - short_delta_count).times do
|
|
199
|
+
break if @io.eof?
|
|
200
|
+
|
|
201
|
+
deltas << read_int8
|
|
208
202
|
end
|
|
203
|
+
|
|
204
|
+
delta_sets << deltas
|
|
205
|
+
rescue EOFError
|
|
206
|
+
# break
|
|
209
207
|
end
|
|
210
208
|
|
|
211
209
|
{
|
|
212
210
|
item_count: item_count,
|
|
213
211
|
region_indices: region_indices,
|
|
214
|
-
delta_sets: delta_sets
|
|
212
|
+
delta_sets: delta_sets,
|
|
215
213
|
}
|
|
216
214
|
end
|
|
217
215
|
|
|
@@ -249,7 +247,10 @@ module Fontisan
|
|
|
249
247
|
# @raise [EOFError] If not enough bytes available
|
|
250
248
|
def read_safely(bytes, description)
|
|
251
249
|
data = @io.read(bytes)
|
|
252
|
-
|
|
250
|
+
if data.nil? || data.bytesize < bytes
|
|
251
|
+
raise EOFError,
|
|
252
|
+
"Unexpected EOF while reading #{description}"
|
|
253
|
+
end
|
|
253
254
|
|
|
254
255
|
data
|
|
255
256
|
end
|
|
@@ -269,7 +270,8 @@ module Fontisan
|
|
|
269
270
|
|
|
270
271
|
if operator_byte?(byte)
|
|
271
272
|
operator = read_dict_operator(io, byte)
|
|
272
|
-
dict[operator] =
|
|
273
|
+
dict[operator] =
|
|
274
|
+
operands.size == 1 ? operands.first : operands.dup
|
|
273
275
|
operands.clear
|
|
274
276
|
else
|
|
275
277
|
# Operand (number)
|
|
@@ -416,4 +418,4 @@ module Fontisan
|
|
|
416
418
|
end
|
|
417
419
|
end
|
|
418
420
|
end
|
|
419
|
-
end
|
|
421
|
+
end
|
data/lib/fontisan/tables/cff2.rb
CHANGED
|
@@ -118,7 +118,8 @@ module Fontisan
|
|
|
118
118
|
resolve(component_glyph, visited, depth + 1)
|
|
119
119
|
else
|
|
120
120
|
# Convert simple glyph to outline
|
|
121
|
-
Models::Outline.from_truetype(component_glyph,
|
|
121
|
+
Models::Outline.from_truetype(component_glyph,
|
|
122
|
+
component.glyph_index)
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
# Apply transformation matrix
|
|
@@ -119,7 +119,10 @@ module Fontisan
|
|
|
119
119
|
# @raise [ArgumentError] If parameters are invalid
|
|
120
120
|
def self.calculate_error(cubic, quadratics)
|
|
121
121
|
validate_cubic_curve!(cubic)
|
|
122
|
-
|
|
122
|
+
unless quadratics.is_a?(Array)
|
|
123
|
+
raise ArgumentError,
|
|
124
|
+
"quadratics must be Array"
|
|
125
|
+
end
|
|
123
126
|
raise ArgumentError, "quadratics cannot be empty" if quadratics.empty?
|
|
124
127
|
|
|
125
128
|
max_error = 0.0
|
|
@@ -305,7 +308,8 @@ module Fontisan
|
|
|
305
308
|
required.each do |key|
|
|
306
309
|
value = quad[key]
|
|
307
310
|
unless value.is_a?(Numeric)
|
|
308
|
-
raise ArgumentError,
|
|
311
|
+
raise ArgumentError,
|
|
312
|
+
"quad[:#{key}] must be Numeric, got: #{value.class}"
|
|
309
313
|
end
|
|
310
314
|
end
|
|
311
315
|
end
|
|
@@ -324,14 +328,16 @@ module Fontisan
|
|
|
324
328
|
required.each do |key|
|
|
325
329
|
value = cubic[key]
|
|
326
330
|
unless value.is_a?(Numeric)
|
|
327
|
-
raise ArgumentError,
|
|
331
|
+
raise ArgumentError,
|
|
332
|
+
"cubic[:#{key}] must be Numeric, got: #{value.class}"
|
|
328
333
|
end
|
|
329
334
|
end
|
|
330
335
|
end
|
|
331
336
|
|
|
332
337
|
private_class_method def self.validate_max_error!(max_error)
|
|
333
338
|
unless max_error.is_a?(Numeric)
|
|
334
|
-
raise ArgumentError,
|
|
339
|
+
raise ArgumentError,
|
|
340
|
+
"max_error must be Numeric, got: #{max_error.class}"
|
|
335
341
|
end
|
|
336
342
|
|
|
337
343
|
if max_error <= 0
|
|
@@ -106,7 +106,11 @@ module Fontisan
|
|
|
106
106
|
# @raise [ArgumentError] If parameters are invalid
|
|
107
107
|
def self.build_compound_glyph(components, bbox, instructions: "".b)
|
|
108
108
|
raise ArgumentError, "components cannot be nil" if components.nil?
|
|
109
|
-
|
|
109
|
+
|
|
110
|
+
unless components.is_a?(Array)
|
|
111
|
+
raise ArgumentError,
|
|
112
|
+
"components must be Array"
|
|
113
|
+
end
|
|
110
114
|
raise ArgumentError, "components cannot be empty" if components.empty?
|
|
111
115
|
|
|
112
116
|
validate_bbox!(bbox)
|
|
@@ -114,7 +118,8 @@ module Fontisan
|
|
|
114
118
|
build_compound_glyph_data(components, bbox, instructions)
|
|
115
119
|
end
|
|
116
120
|
|
|
117
|
-
private_class_method def self.build_simple_glyph_data(contours, bbox,
|
|
121
|
+
private_class_method def self.build_simple_glyph_data(contours, bbox,
|
|
122
|
+
instructions)
|
|
118
123
|
num_contours = contours.length
|
|
119
124
|
|
|
120
125
|
# Build endPtsOfContours array
|
|
@@ -136,7 +141,8 @@ module Fontisan
|
|
|
136
141
|
|
|
137
142
|
# Header (10 bytes)
|
|
138
143
|
data << [num_contours].pack("n") # numberOfContours
|
|
139
|
-
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
144
|
+
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
145
|
+
bbox[:y_max]].pack("n4")
|
|
140
146
|
|
|
141
147
|
# endPtsOfContours
|
|
142
148
|
data << end_pts_of_contours.pack("n*")
|
|
@@ -155,18 +161,21 @@ module Fontisan
|
|
|
155
161
|
data
|
|
156
162
|
end
|
|
157
163
|
|
|
158
|
-
private_class_method def self.build_compound_glyph_data(components, bbox,
|
|
164
|
+
private_class_method def self.build_compound_glyph_data(components, bbox,
|
|
165
|
+
instructions)
|
|
159
166
|
data = (+"").force_encoding(Encoding::BINARY)
|
|
160
167
|
|
|
161
168
|
# Header (10 bytes) - numberOfContours = -1 for compound
|
|
162
169
|
data << [-1].pack("n") # Use signed pack, will convert to 0xFFFF
|
|
163
|
-
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
170
|
+
data << [bbox[:x_min], bbox[:y_min], bbox[:x_max],
|
|
171
|
+
bbox[:y_max]].pack("n4")
|
|
164
172
|
|
|
165
173
|
# Encode components
|
|
166
174
|
has_instructions = instructions.bytesize.positive?
|
|
167
175
|
components.each_with_index do |component, index|
|
|
168
176
|
is_last = (index == components.length - 1)
|
|
169
|
-
component_data = encode_component(component, is_last,
|
|
177
|
+
component_data = encode_component(component, is_last,
|
|
178
|
+
has_instructions)
|
|
170
179
|
data << component_data
|
|
171
180
|
end
|
|
172
181
|
|
|
@@ -179,7 +188,8 @@ module Fontisan
|
|
|
179
188
|
data
|
|
180
189
|
end
|
|
181
190
|
|
|
182
|
-
private_class_method def self.encode_component(component, is_last,
|
|
191
|
+
private_class_method def self.encode_component(component, is_last,
|
|
192
|
+
has_instructions)
|
|
183
193
|
validate_component!(component)
|
|
184
194
|
|
|
185
195
|
glyph_index = component[:glyph_index]
|
|
@@ -344,7 +354,8 @@ module Fontisan
|
|
|
344
354
|
data
|
|
345
355
|
end
|
|
346
356
|
|
|
347
|
-
private_class_method def self.encode_coordinate_values(flags, deltas,
|
|
357
|
+
private_class_method def self.encode_coordinate_values(flags, deltas,
|
|
358
|
+
axis)
|
|
348
359
|
data = (+"").force_encoding(Encoding::BINARY)
|
|
349
360
|
short_flag = axis == :x ? X_SHORT_VECTOR : Y_SHORT_VECTOR
|
|
350
361
|
same_flag = axis == :x ? X_IS_SAME_OR_POSITIVE_X_SHORT : Y_IS_SAME_OR_POSITIVE_Y_SHORT
|
|
@@ -400,7 +411,10 @@ module Fontisan
|
|
|
400
411
|
# Convert float to F2DOT14 fixed-point format
|
|
401
412
|
# F2DOT14: 2 bits integer, 14 bits fractional
|
|
402
413
|
# Range: -2.0 to ~1.99993896484375
|
|
403
|
-
|
|
414
|
+
if value < -2.0 || value > 2.0
|
|
415
|
+
raise ArgumentError,
|
|
416
|
+
"value out of F2DOT14 range"
|
|
417
|
+
end
|
|
404
418
|
|
|
405
419
|
fixed = (value * 16_384.0).round
|
|
406
420
|
# Convert to unsigned 16-bit
|
|
@@ -434,7 +448,10 @@ module Fontisan
|
|
|
434
448
|
end
|
|
435
449
|
|
|
436
450
|
private_class_method def self.validate_component!(component)
|
|
437
|
-
|
|
451
|
+
unless component.is_a?(Hash)
|
|
452
|
+
raise ArgumentError,
|
|
453
|
+
"component must be Hash"
|
|
454
|
+
end
|
|
438
455
|
unless component[:glyph_index]
|
|
439
456
|
raise ArgumentError, "component must have :glyph_index"
|
|
440
457
|
end
|
data/lib/fontisan/tables/name.rb
CHANGED
|
@@ -173,13 +173,13 @@ module Fontisan
|
|
|
173
173
|
record = find_name_record(
|
|
174
174
|
name_id,
|
|
175
175
|
platform: PLATFORM_WINDOWS,
|
|
176
|
-
language: WINDOWS_LANGUAGE_EN_US
|
|
176
|
+
language: WINDOWS_LANGUAGE_EN_US,
|
|
177
177
|
)
|
|
178
178
|
|
|
179
179
|
record ||= find_name_record(
|
|
180
180
|
name_id,
|
|
181
181
|
platform: PLATFORM_MACINTOSH,
|
|
182
|
-
language: MAC_LANGUAGE_ENGLISH
|
|
182
|
+
language: MAC_LANGUAGE_ENGLISH,
|
|
183
183
|
)
|
|
184
184
|
|
|
185
185
|
return nil unless record
|
|
@@ -236,10 +236,10 @@ module Fontisan
|
|
|
236
236
|
decoded = case record.platform_id
|
|
237
237
|
when PLATFORM_WINDOWS, PLATFORM_UNICODE
|
|
238
238
|
string_data.dup.force_encoding("UTF-16BE")
|
|
239
|
-
|
|
239
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
240
240
|
when PLATFORM_MACINTOSH
|
|
241
241
|
string_data.dup.force_encoding("ASCII-8BIT")
|
|
242
|
-
|
|
242
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
243
243
|
else
|
|
244
244
|
string_data.dup.force_encoding("UTF-8")
|
|
245
245
|
end
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "constants"
|
|
3
|
+
require_relative "base_collection"
|
|
5
4
|
|
|
6
5
|
module Fontisan
|
|
7
|
-
# TrueType Collection domain object
|
|
6
|
+
# TrueType Collection domain object
|
|
8
7
|
#
|
|
9
|
-
# Represents a complete TrueType Collection file
|
|
10
|
-
#
|
|
11
|
-
# documentation, and BinData handles all low-level reading/writing.
|
|
8
|
+
# Represents a complete TrueType Collection file. Inherits all shared
|
|
9
|
+
# functionality from BaseCollection and implements TTC-specific behavior.
|
|
12
10
|
#
|
|
13
11
|
# @example Reading and extracting fonts
|
|
14
12
|
# File.open("Helvetica.ttc", "rb") do |io|
|
|
@@ -16,51 +14,25 @@ module Fontisan
|
|
|
16
14
|
# puts ttc.num_fonts # => 6
|
|
17
15
|
# fonts = ttc.extract_fonts(io) # => [TrueTypeFont, TrueTypeFont, ...]
|
|
18
16
|
# end
|
|
19
|
-
class TrueTypeCollection <
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
string :tag, length: 4, assert: "ttcf"
|
|
23
|
-
uint16 :major_version
|
|
24
|
-
uint16 :minor_version
|
|
25
|
-
uint32 :num_fonts
|
|
26
|
-
array :font_offsets, type: :uint32, initial_length: :num_fonts
|
|
27
|
-
|
|
28
|
-
# Read TrueType Collection from a file
|
|
17
|
+
class TrueTypeCollection < BaseCollection
|
|
18
|
+
# Get the font class for TrueType collections
|
|
29
19
|
#
|
|
30
|
-
# @
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# @raise [RuntimeError] if file format is invalid
|
|
35
|
-
def self.from_file(path)
|
|
36
|
-
if path.nil? || path.to_s.empty?
|
|
37
|
-
raise ArgumentError,
|
|
38
|
-
"path cannot be nil or empty"
|
|
39
|
-
end
|
|
40
|
-
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
41
|
-
|
|
42
|
-
File.open(path, "rb") { |io| read(io) }
|
|
43
|
-
rescue BinData::ValidityError => e
|
|
44
|
-
raise "Invalid TTC file: #{e.message}"
|
|
45
|
-
rescue EOFError => e
|
|
46
|
-
raise "Invalid TTC file: unexpected end of file - #{e.message}"
|
|
20
|
+
# @return [Class] TrueTypeFont class
|
|
21
|
+
def self.font_class
|
|
22
|
+
require_relative "true_type_font"
|
|
23
|
+
TrueTypeFont
|
|
47
24
|
end
|
|
48
25
|
|
|
49
|
-
#
|
|
26
|
+
# Get the collection format identifier
|
|
50
27
|
#
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# @return [Array<TrueTypeFont>] Array of font objects
|
|
55
|
-
def extract_fonts(io)
|
|
56
|
-
require_relative "true_type_font"
|
|
57
|
-
|
|
58
|
-
font_offsets.map do |offset|
|
|
59
|
-
TrueTypeFont.from_ttc(io, offset)
|
|
60
|
-
end
|
|
28
|
+
# @return [String] "TTC" for TrueType Collection
|
|
29
|
+
def self.collection_format
|
|
30
|
+
"TTC"
|
|
61
31
|
end
|
|
62
32
|
|
|
63
|
-
# Get a single font from the collection
|
|
33
|
+
# Get a single font from the collection
|
|
34
|
+
#
|
|
35
|
+
# Overrides BaseCollection to use TrueType-specific from_ttc method.
|
|
64
36
|
#
|
|
65
37
|
# @param index [Integer] Index of the font (0-based)
|
|
66
38
|
# @param io [IO] Open file handle
|
|
@@ -73,43 +45,27 @@ module Fontisan
|
|
|
73
45
|
TrueTypeFont.from_ttc(io, font_offsets[index], mode: mode)
|
|
74
46
|
end
|
|
75
47
|
|
|
76
|
-
#
|
|
48
|
+
# Extract fonts as TrueTypeFont objects
|
|
77
49
|
#
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
num_fonts
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Validate format correctness
|
|
50
|
+
# Reads each font from the TTC file and returns them as TrueTypeFont objects.
|
|
51
|
+
# This method uses the TTC-specific from_ttc method.
|
|
84
52
|
#
|
|
85
|
-
# @
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
false
|
|
90
|
-
end
|
|
53
|
+
# @param io [IO] Open file handle to read fonts from
|
|
54
|
+
# @return [Array<TrueTypeFont>] Array of font objects
|
|
55
|
+
def extract_fonts(io)
|
|
56
|
+
require_relative "true_type_font"
|
|
91
57
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def version
|
|
96
|
-
(major_version << 16) | minor_version
|
|
58
|
+
font_offsets.map do |offset|
|
|
59
|
+
TrueTypeFont.from_ttc(io, offset)
|
|
60
|
+
end
|
|
97
61
|
end
|
|
98
62
|
|
|
99
63
|
# List all fonts in the collection with basic metadata
|
|
100
64
|
#
|
|
101
|
-
#
|
|
102
|
-
# This is the API method used by the `ls` command for collections.
|
|
65
|
+
# Overrides BaseCollection to use TrueType-specific from_ttc method.
|
|
103
66
|
#
|
|
104
67
|
# @param io [IO] Open file handle to read fonts from
|
|
105
68
|
# @return [CollectionListInfo] List of fonts with metadata
|
|
106
|
-
#
|
|
107
|
-
# @example List fonts in collection
|
|
108
|
-
# File.open("fonts.ttc", "rb") do |io|
|
|
109
|
-
# ttc = TrueTypeCollection.read(io)
|
|
110
|
-
# list = ttc.list_fonts(io)
|
|
111
|
-
# list.fonts.each { |f| puts "#{f.index}: #{f.family_name}" }
|
|
112
|
-
# end
|
|
113
69
|
def list_fonts(io)
|
|
114
70
|
require_relative "models/collection_list_info"
|
|
115
71
|
require_relative "models/collection_font_summary"
|
|
@@ -159,51 +115,11 @@ module Fontisan
|
|
|
159
115
|
)
|
|
160
116
|
end
|
|
161
117
|
|
|
162
|
-
# Get comprehensive collection metadata
|
|
163
|
-
#
|
|
164
|
-
# Returns a CollectionInfo model with header information, offsets,
|
|
165
|
-
# and table sharing statistics.
|
|
166
|
-
# This is the API method used by the `info` command for collections.
|
|
167
|
-
#
|
|
168
|
-
# @param io [IO] Open file handle to read fonts from
|
|
169
|
-
# @param path [String] Collection file path (for file size)
|
|
170
|
-
# @return [CollectionInfo] Collection metadata
|
|
171
|
-
#
|
|
172
|
-
# @example Get collection info
|
|
173
|
-
# File.open("fonts.ttc", "rb") do |io|
|
|
174
|
-
# ttc = TrueTypeCollection.read(io)
|
|
175
|
-
# info = ttc.collection_info(io, "fonts.ttc")
|
|
176
|
-
# puts "Version: #{info.version_string}"
|
|
177
|
-
# end
|
|
178
|
-
def collection_info(io, path)
|
|
179
|
-
require_relative "models/collection_info"
|
|
180
|
-
require_relative "models/table_sharing_info"
|
|
181
|
-
|
|
182
|
-
# Calculate table sharing statistics
|
|
183
|
-
table_sharing = calculate_table_sharing(io)
|
|
184
|
-
|
|
185
|
-
# Get file size
|
|
186
|
-
file_size = path ? File.size(path) : 0
|
|
187
|
-
|
|
188
|
-
Models::CollectionInfo.new(
|
|
189
|
-
collection_path: path,
|
|
190
|
-
collection_format: "TTC",
|
|
191
|
-
ttc_tag: tag,
|
|
192
|
-
major_version: major_version,
|
|
193
|
-
minor_version: minor_version,
|
|
194
|
-
num_fonts: num_fonts,
|
|
195
|
-
font_offsets: font_offsets.to_a,
|
|
196
|
-
file_size_bytes: file_size,
|
|
197
|
-
table_sharing: table_sharing,
|
|
198
|
-
)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
118
|
private
|
|
202
119
|
|
|
203
120
|
# Calculate table sharing statistics
|
|
204
121
|
#
|
|
205
|
-
#
|
|
206
|
-
# space savings from deduplication.
|
|
122
|
+
# Overrides BaseCollection to use TrueType-specific from_ttc method.
|
|
207
123
|
#
|
|
208
124
|
# @param io [IO] Open file handle
|
|
209
125
|
# @return [TableSharingInfo] Sharing statistics
|
|
@@ -233,7 +233,8 @@ module Fontisan
|
|
|
233
233
|
batch_entries.each do |entry|
|
|
234
234
|
relative_offset = entry.offset - batch_offset
|
|
235
235
|
tag_key = entry.tag.dup.force_encoding("UTF-8")
|
|
236
|
-
@table_data[tag_key] =
|
|
236
|
+
@table_data[tag_key] =
|
|
237
|
+
batch_data[relative_offset, entry.table_length]
|
|
237
238
|
end
|
|
238
239
|
end
|
|
239
240
|
|
|
@@ -307,6 +308,7 @@ module Fontisan
|
|
|
307
308
|
# @return [Boolean] true if table is available in current mode
|
|
308
309
|
def table_available?(tag)
|
|
309
310
|
return false unless has_table?(tag)
|
|
311
|
+
|
|
310
312
|
LoadingModes.table_allowed?(@loading_mode, tag)
|
|
311
313
|
end
|
|
312
314
|
|
|
@@ -114,7 +114,7 @@ module Fontisan
|
|
|
114
114
|
skip_tables = @checksum_config["skip_tables"] || []
|
|
115
115
|
|
|
116
116
|
font.tables.each do |table_entry|
|
|
117
|
-
tag = table_entry.tag.to_s
|
|
117
|
+
tag = table_entry.tag.to_s # Convert BinData field to string
|
|
118
118
|
|
|
119
119
|
# Skip tables that are exempt from checksum validation
|
|
120
120
|
next if skip_tables.include?(tag)
|
|
@@ -125,7 +125,7 @@ module Fontisan
|
|
|
125
125
|
|
|
126
126
|
# Calculate checksum for the table
|
|
127
127
|
calculated_checksum = calculate_table_checksum(table_data)
|
|
128
|
-
declared_checksum = table_entry.checksum.to_i
|
|
128
|
+
declared_checksum = table_entry.checksum.to_i # Convert BinData field to integer
|
|
129
129
|
|
|
130
130
|
# Special handling for head table (checksum adjustment field should be 0)
|
|
131
131
|
if tag == Constants::HEAD_TAG
|
|
@@ -44,6 +44,7 @@ module Fontisan
|
|
|
44
44
|
@ttl = ttl
|
|
45
45
|
@cache = {}
|
|
46
46
|
@access_times = {}
|
|
47
|
+
@access_counter = 0
|
|
47
48
|
@stats = {
|
|
48
49
|
hits: 0,
|
|
49
50
|
misses: 0,
|
|
@@ -219,7 +220,8 @@ module Fontisan
|
|
|
219
220
|
#
|
|
220
221
|
# @param key [String] Cache key
|
|
221
222
|
def touch(key)
|
|
222
|
-
@
|
|
223
|
+
@access_counter += 1
|
|
224
|
+
@access_times[key] = @access_counter
|
|
223
225
|
end
|
|
224
226
|
|
|
225
227
|
# Evict entries if cache is full
|
|
@@ -162,7 +162,8 @@ module Fontisan
|
|
|
162
162
|
# Note: CFF2 blend deltas are per-coordinate, we need to map to x/y
|
|
163
163
|
# This is a simplified mapping - full implementation would track
|
|
164
164
|
# which coordinates are being varied
|
|
165
|
-
regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0,
|
|
165
|
+
regions_map[region_key][:deltas_per_point][idx / 2] ||= { x: 0,
|
|
166
|
+
y: 0 }
|
|
166
167
|
if idx.even?
|
|
167
168
|
regions_map[region_key][:deltas_per_point][idx / 2][:x] = delta
|
|
168
169
|
else
|
|
@@ -79,7 +79,8 @@ module Fontisan
|
|
|
79
79
|
# Apply each active tuple's deltas
|
|
80
80
|
adjusted_points = base_points.dup
|
|
81
81
|
matches.each do |match|
|
|
82
|
-
apply_tuple_deltas(adjusted_points, match, tuple_data,
|
|
82
|
+
apply_tuple_deltas(adjusted_points, match, tuple_data,
|
|
83
|
+
base_points.length)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
adjusted_points
|
|
@@ -107,7 +107,8 @@ module Fontisan
|
|
|
107
107
|
index: index,
|
|
108
108
|
name: instance_name(instance[:subfamily_name_id]),
|
|
109
109
|
postscript_name: instance_name(instance[:postscript_name_id]),
|
|
110
|
-
coordinates: instance_coordinates(instance[:coordinates],
|
|
110
|
+
coordinates: instance_coordinates(instance[:coordinates],
|
|
111
|
+
@context.axes),
|
|
111
112
|
}
|
|
112
113
|
end
|
|
113
114
|
end
|
|
@@ -248,7 +248,8 @@ module Fontisan
|
|
|
248
248
|
# @param scalars [Array<Float>] Region scalars
|
|
249
249
|
# @return [Hash] Interpolated point
|
|
250
250
|
def interpolate_point(base_point, delta_points, scalars)
|
|
251
|
-
@context.interpolator.interpolate_point(base_point, delta_points,
|
|
251
|
+
@context.interpolator.interpolate_point(base_point, delta_points,
|
|
252
|
+
scalars)
|
|
252
253
|
end
|
|
253
254
|
|
|
254
255
|
private
|
|
@@ -209,9 +209,12 @@ module Fontisan
|
|
|
209
209
|
coords2 = r2.region_axes[i]
|
|
210
210
|
|
|
211
211
|
# Compare start, peak, end coordinates
|
|
212
|
-
return false unless coords_similar?(coords1.start_coord,
|
|
213
|
-
|
|
214
|
-
return false unless coords_similar?(coords1.
|
|
212
|
+
return false unless coords_similar?(coords1.start_coord,
|
|
213
|
+
coords2.start_coord)
|
|
214
|
+
return false unless coords_similar?(coords1.peak_coord,
|
|
215
|
+
coords2.peak_coord)
|
|
216
|
+
return false unless coords_similar?(coords1.end_coord,
|
|
217
|
+
coords2.end_coord)
|
|
215
218
|
end
|
|
216
219
|
|
|
217
220
|
true
|