fontisan 0.2.0 → 0.2.2
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 +119 -308
- data/README.adoc +1525 -1323
- data/Rakefile +45 -47
- 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/cli.rb +92 -34
- data/lib/fontisan/collection/builder.rb +82 -0
- data/lib/fontisan/collection/offset_calculator.rb +2 -0
- data/lib/fontisan/collection/table_deduplicator.rb +76 -0
- data/lib/fontisan/commands/base_command.rb +21 -2
- data/lib/fontisan/commands/convert_command.rb +96 -165
- data/lib/fontisan/commands/info_command.rb +111 -5
- data/lib/fontisan/commands/instance_command.rb +77 -85
- data/lib/fontisan/commands/validate_command.rb +28 -0
- data/lib/fontisan/config/validation_rules.yml +1 -1
- data/lib/fontisan/constants.rb +34 -24
- data/lib/fontisan/converters/format_converter.rb +154 -1
- data/lib/fontisan/converters/outline_converter.rb +101 -34
- data/lib/fontisan/converters/woff_writer.rb +9 -4
- data/lib/fontisan/font_loader.rb +14 -9
- data/lib/fontisan/font_writer.rb +9 -6
- data/lib/fontisan/formatters/text_formatter.rb +45 -1
- data/lib/fontisan/hints/hint_converter.rb +131 -2
- data/lib/fontisan/hints/hint_validator.rb +284 -0
- data/lib/fontisan/hints/postscript_hint_applier.rb +219 -140
- data/lib/fontisan/hints/postscript_hint_extractor.rb +151 -16
- data/lib/fontisan/hints/truetype_hint_applier.rb +90 -44
- data/lib/fontisan/hints/truetype_hint_extractor.rb +134 -11
- 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 +6 -4
- data/lib/fontisan/models/collection_brief_info.rb +31 -0
- data/lib/fontisan/models/font_info.rb +3 -30
- data/lib/fontisan/models/hint.rb +183 -12
- data/lib/fontisan/models/outline.rb +4 -1
- data/lib/fontisan/open_type_font.rb +28 -10
- data/lib/fontisan/open_type_font_extensions.rb +54 -0
- data/lib/fontisan/optimizers/pattern_analyzer.rb +2 -1
- data/lib/fontisan/optimizers/subroutine_generator.rb +1 -1
- data/lib/fontisan/pipeline/format_detector.rb +249 -0
- data/lib/fontisan/pipeline/output_writer.rb +159 -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 +416 -0
- data/lib/fontisan/pipeline/variation_resolver.rb +165 -0
- data/lib/fontisan/subset/table_subsetter.rb +5 -5
- data/lib/fontisan/tables/cff/charstring.rb +58 -3
- data/lib/fontisan/tables/cff/charstring_builder.rb +34 -0
- data/lib/fontisan/tables/cff/charstring_parser.rb +249 -0
- data/lib/fontisan/tables/cff/charstring_rebuilder.rb +172 -0
- data/lib/fontisan/tables/cff/dict_builder.rb +19 -1
- data/lib/fontisan/tables/cff/hint_operation_injector.rb +209 -0
- data/lib/fontisan/tables/cff/offset_recalculator.rb +70 -0
- data/lib/fontisan/tables/cff/private_dict_writer.rb +131 -0
- data/lib/fontisan/tables/cff/table_builder.rb +221 -0
- data/lib/fontisan/tables/cff.rb +2 -0
- data/lib/fontisan/tables/cff2/charstring_parser.rb +14 -8
- data/lib/fontisan/tables/cff2/private_dict_blend_handler.rb +247 -0
- data/lib/fontisan/tables/cff2/region_matcher.rb +200 -0
- data/lib/fontisan/tables/cff2/table_builder.rb +580 -0
- data/lib/fontisan/tables/cff2/table_reader.rb +421 -0
- data/lib/fontisan/tables/cff2/variation_data_extractor.rb +212 -0
- data/lib/fontisan/tables/cff2.rb +10 -5
- data/lib/fontisan/tables/cvar.rb +2 -41
- 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/gvar.rb +2 -41
- data/lib/fontisan/tables/name.rb +4 -4
- data/lib/fontisan/true_type_font.rb +27 -10
- data/lib/fontisan/true_type_font_extensions.rb +54 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +42 -0
- data/lib/fontisan/validation/checksum_validator.rb +2 -2
- data/lib/fontisan/validation/table_validator.rb +1 -1
- data/lib/fontisan/validation/variable_font_validator.rb +218 -0
- data/lib/fontisan/variation/cache.rb +3 -1
- data/lib/fontisan/variation/converter.rb +121 -13
- 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/instance_writer.rb +341 -0
- data/lib/fontisan/variation/optimizer.rb +6 -3
- data/lib/fontisan/variation/subsetter.rb +32 -10
- data/lib/fontisan/variation/tuple_variation_header.rb +51 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +268 -0
- data/lib/fontisan/variation/variation_preserver.rb +291 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/version.rb.orig +9 -0
- data/lib/fontisan/woff2/glyf_transformer.rb +693 -0
- data/lib/fontisan/woff2/hmtx_transformer.rb +164 -0
- data/lib/fontisan/woff2_font.rb +489 -468
- data/lib/fontisan/woff_font.rb +16 -11
- data/lib/fontisan.rb +54 -2
- data/scripts/measure_optimization.rb +15 -7
- metadata +37 -2
data/lib/fontisan/tables/cvar.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
4
|
require_relative "../binary/base_record"
|
|
5
|
+
require_relative "../variation/tuple_variation_header"
|
|
5
6
|
|
|
6
7
|
module Fontisan
|
|
7
8
|
module Tables
|
|
@@ -26,46 +27,6 @@ module Fontisan
|
|
|
26
27
|
uint16 :tuple_variation_count
|
|
27
28
|
uint16 :data_offset
|
|
28
29
|
|
|
29
|
-
# Tuple variation header
|
|
30
|
-
class TupleVariationHeader < Binary::BaseRecord
|
|
31
|
-
uint16 :variation_data_size
|
|
32
|
-
uint16 :tuple_index
|
|
33
|
-
|
|
34
|
-
# Tuple index flags
|
|
35
|
-
EMBEDDED_PEAK_TUPLE = 0x8000
|
|
36
|
-
INTERMEDIATE_REGION = 0x4000
|
|
37
|
-
PRIVATE_POINT_NUMBERS = 0x2000
|
|
38
|
-
TUPLE_INDEX_MASK = 0x0FFF
|
|
39
|
-
|
|
40
|
-
# Check if tuple has embedded peak coordinates
|
|
41
|
-
#
|
|
42
|
-
# @return [Boolean] True if embedded
|
|
43
|
-
def embedded_peak_tuple?
|
|
44
|
-
(tuple_index & EMBEDDED_PEAK_TUPLE) != 0
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Check if tuple has intermediate region
|
|
48
|
-
#
|
|
49
|
-
# @return [Boolean] True if intermediate region
|
|
50
|
-
def intermediate_region?
|
|
51
|
-
(tuple_index & INTERMEDIATE_REGION) != 0
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Check if tuple has private point numbers
|
|
55
|
-
#
|
|
56
|
-
# @return [Boolean] True if private points
|
|
57
|
-
def private_point_numbers?
|
|
58
|
-
(tuple_index & PRIVATE_POINT_NUMBERS) != 0
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Get shared tuple index
|
|
62
|
-
#
|
|
63
|
-
# @return [Integer] Tuple index
|
|
64
|
-
def shared_tuple_index
|
|
65
|
-
tuple_index & TUPLE_INDEX_MASK
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
30
|
# Get version as a float
|
|
70
31
|
#
|
|
71
32
|
# @return [Float] Version number (e.g., 1.0)
|
|
@@ -109,7 +70,7 @@ module Fontisan
|
|
|
109
70
|
break if offset + 4 > data.bytesize
|
|
110
71
|
|
|
111
72
|
header_data = data.byteslice(offset, 4)
|
|
112
|
-
header = TupleVariationHeader.read(header_data)
|
|
73
|
+
header = Variation::TupleVariationHeader.read(header_data)
|
|
113
74
|
offset += 4
|
|
114
75
|
|
|
115
76
|
tuple_info = {
|
|
@@ -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/gvar.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
4
|
require_relative "../binary/base_record"
|
|
5
|
+
require_relative "../variation/tuple_variation_header"
|
|
5
6
|
|
|
6
7
|
module Fontisan
|
|
7
8
|
module Tables
|
|
@@ -34,46 +35,6 @@ module Fontisan
|
|
|
34
35
|
SHARED_POINT_NUMBERS = 0x8000
|
|
35
36
|
LONG_OFFSETS = 0x0001
|
|
36
37
|
|
|
37
|
-
# Tuple variation header
|
|
38
|
-
class TupleVariationHeader < Binary::BaseRecord
|
|
39
|
-
uint16 :variation_data_size
|
|
40
|
-
uint16 :tuple_index
|
|
41
|
-
|
|
42
|
-
# Tuple index flags
|
|
43
|
-
EMBEDDED_PEAK_TUPLE = 0x8000
|
|
44
|
-
INTERMEDIATE_REGION = 0x4000
|
|
45
|
-
PRIVATE_POINT_NUMBERS = 0x2000
|
|
46
|
-
TUPLE_INDEX_MASK = 0x0FFF
|
|
47
|
-
|
|
48
|
-
# Check if tuple has embedded peak coordinates
|
|
49
|
-
#
|
|
50
|
-
# @return [Boolean] True if embedded
|
|
51
|
-
def embedded_peak_tuple?
|
|
52
|
-
(tuple_index & EMBEDDED_PEAK_TUPLE) != 0
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Check if tuple has intermediate region
|
|
56
|
-
#
|
|
57
|
-
# @return [Boolean] True if intermediate region
|
|
58
|
-
def intermediate_region?
|
|
59
|
-
(tuple_index & INTERMEDIATE_REGION) != 0
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Check if tuple has private point numbers
|
|
63
|
-
#
|
|
64
|
-
# @return [Boolean] True if private points
|
|
65
|
-
def private_point_numbers?
|
|
66
|
-
(tuple_index & PRIVATE_POINT_NUMBERS) != 0
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Get shared tuple index
|
|
70
|
-
#
|
|
71
|
-
# @return [Integer] Tuple index
|
|
72
|
-
def shared_tuple_index
|
|
73
|
-
tuple_index & TUPLE_INDEX_MASK
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
38
|
# Get version as a float
|
|
78
39
|
#
|
|
79
40
|
# @return [Float] Version number (e.g., 1.0)
|
|
@@ -198,7 +159,7 @@ module Fontisan
|
|
|
198
159
|
header_data = io.read(4)
|
|
199
160
|
break if header_data.nil? || header_data.bytesize < 4
|
|
200
161
|
|
|
201
|
-
header = TupleVariationHeader.read(header_data)
|
|
162
|
+
header = Variation::TupleVariationHeader.read(header_data)
|
|
202
163
|
|
|
203
164
|
tuple_info = {
|
|
204
165
|
data_size: header.variation_data_size,
|
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
|
|
@@ -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
|
|
|
@@ -279,6 +280,20 @@ module Fontisan
|
|
|
279
280
|
true
|
|
280
281
|
end
|
|
281
282
|
|
|
283
|
+
# Check if font is TrueType flavored
|
|
284
|
+
#
|
|
285
|
+
# @return [Boolean] true for TrueType fonts
|
|
286
|
+
def truetype?
|
|
287
|
+
true
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Check if font is CFF flavored
|
|
291
|
+
#
|
|
292
|
+
# @return [Boolean] false for TrueType fonts
|
|
293
|
+
def cff?
|
|
294
|
+
false
|
|
295
|
+
end
|
|
296
|
+
|
|
282
297
|
# Check if font has a specific table
|
|
283
298
|
#
|
|
284
299
|
# @param tag [String] The table tag to check for
|
|
@@ -293,6 +308,7 @@ module Fontisan
|
|
|
293
308
|
# @return [Boolean] true if table is available in current mode
|
|
294
309
|
def table_available?(tag)
|
|
295
310
|
return false unless has_table?(tag)
|
|
311
|
+
|
|
296
312
|
LoadingModes.table_allowed?(@loading_mode, tag)
|
|
297
313
|
end
|
|
298
314
|
|
|
@@ -579,18 +595,19 @@ module Fontisan
|
|
|
579
595
|
# @param path [String] Path to the TTF file
|
|
580
596
|
# @return [void]
|
|
581
597
|
def update_checksum_adjustment_in_file(path)
|
|
582
|
-
#
|
|
583
|
-
|
|
598
|
+
# Use tempfile-based checksum calculation for Windows compatibility
|
|
599
|
+
# This keeps the tempfile alive until we're done with the checksum
|
|
600
|
+
File.open(path, "r+b") do |io|
|
|
601
|
+
checksum, _tmpfile = Utilities::ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
|
|
584
602
|
|
|
585
|
-
|
|
586
|
-
|
|
603
|
+
# Calculate adjustment
|
|
604
|
+
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
587
605
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
606
|
+
# Find head table position
|
|
607
|
+
head_entry = head_table
|
|
608
|
+
return unless head_entry
|
|
591
609
|
|
|
592
|
-
|
|
593
|
-
File.open(path, "r+b") do |io|
|
|
610
|
+
# Write adjustment to head table (offset 8 within head table)
|
|
594
611
|
io.seek(head_entry.offset + 8)
|
|
595
612
|
io.write([adjustment].pack("N"))
|
|
596
613
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# Extensions to TrueTypeFont for table-based construction
|
|
5
|
+
class TrueTypeFont
|
|
6
|
+
# Create font from hash of tables
|
|
7
|
+
#
|
|
8
|
+
# This is used during font conversion when we have tables but not a file.
|
|
9
|
+
#
|
|
10
|
+
# @param tables [Hash<String, String>] Map of table tag to binary data
|
|
11
|
+
# @return [TrueTypeFont] New font instance
|
|
12
|
+
def self.from_tables(tables)
|
|
13
|
+
# Create minimal header structure
|
|
14
|
+
font = new
|
|
15
|
+
font.initialize_storage
|
|
16
|
+
font.loading_mode = LoadingModes::FULL
|
|
17
|
+
|
|
18
|
+
# Store table data
|
|
19
|
+
font.table_data = tables
|
|
20
|
+
|
|
21
|
+
# Build header from tables
|
|
22
|
+
num_tables = tables.size
|
|
23
|
+
max_power = 0
|
|
24
|
+
n = num_tables
|
|
25
|
+
while n > 1
|
|
26
|
+
n >>= 1
|
|
27
|
+
max_power += 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
search_range = (1 << max_power) * 16
|
|
31
|
+
entry_selector = max_power
|
|
32
|
+
range_shift = (num_tables * 16) - search_range
|
|
33
|
+
|
|
34
|
+
font.header.sfnt_version = 0x00010000 # TrueType
|
|
35
|
+
font.header.num_tables = num_tables
|
|
36
|
+
font.header.search_range = search_range
|
|
37
|
+
font.header.entry_selector = entry_selector
|
|
38
|
+
font.header.range_shift = range_shift
|
|
39
|
+
|
|
40
|
+
# Build table directory
|
|
41
|
+
font.tables.clear
|
|
42
|
+
tables.each_key do |tag|
|
|
43
|
+
entry = TableDirectory.new
|
|
44
|
+
entry.tag = tag
|
|
45
|
+
entry.checksum = 0 # Will be calculated on write
|
|
46
|
+
entry.offset = 0 # Will be calculated on write
|
|
47
|
+
entry.table_length = tables[tag].bytesize
|
|
48
|
+
font.tables << entry
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
font
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
|
+
require "tempfile"
|
|
4
5
|
require_relative "../constants"
|
|
5
6
|
|
|
6
7
|
module Fontisan
|
|
@@ -101,6 +102,47 @@ module Fontisan
|
|
|
101
102
|
sum
|
|
102
103
|
end
|
|
103
104
|
|
|
105
|
+
# Calculate checksum from an IO object using a tempfile for Windows compatibility.
|
|
106
|
+
#
|
|
107
|
+
# This method creates a temporary file from the IO content to ensure proper
|
|
108
|
+
# file handle semantics on Windows, where file handles must remain open
|
|
109
|
+
# for checksum calculation. The tempfile reference is returned alongside
|
|
110
|
+
# the checksum to prevent premature garbage collection on Windows.
|
|
111
|
+
#
|
|
112
|
+
# @param io [IO] the IO object to read from (must be rewindable)
|
|
113
|
+
# @return [Array<Integer, Tempfile>] array containing [checksum, tempfile]
|
|
114
|
+
# The checksum value and the tempfile that must be kept alive until
|
|
115
|
+
# the caller is done with the checksum.
|
|
116
|
+
#
|
|
117
|
+
# @example
|
|
118
|
+
# checksum, tmpfile = ChecksumCalculator.calculate_checksum_from_io_with_tempfile(io)
|
|
119
|
+
# # Use checksum...
|
|
120
|
+
# # tmpfile will be GC'd when it goes out of scope, which is safe
|
|
121
|
+
#
|
|
122
|
+
# @note On Windows, Ruby's Tempfile automatically deletes the temp file when
|
|
123
|
+
# the Tempfile object is garbage collected. In multi-threaded environments,
|
|
124
|
+
# this can cause PermissionDenied errors if the file is deleted while
|
|
125
|
+
# another thread is still using it. By returning the tempfile reference,
|
|
126
|
+
# the caller can ensure it remains alive until all operations complete.
|
|
127
|
+
def self.calculate_checksum_from_io_with_tempfile(io)
|
|
128
|
+
io.rewind
|
|
129
|
+
|
|
130
|
+
# Create a tempfile to handle Windows file locking issues
|
|
131
|
+
tmpfile = Tempfile.new(["font", ".ttf"])
|
|
132
|
+
tmpfile.binmode
|
|
133
|
+
|
|
134
|
+
# Copy IO content to tempfile
|
|
135
|
+
IO.copy_stream(io, tmpfile)
|
|
136
|
+
tmpfile.close
|
|
137
|
+
|
|
138
|
+
# Calculate checksum from the tempfile
|
|
139
|
+
checksum = calculate_file_checksum(tmpfile.path)
|
|
140
|
+
|
|
141
|
+
# Return both checksum and tempfile to keep it alive
|
|
142
|
+
# The caller must keep the tempfile reference until done with checksum
|
|
143
|
+
[checksum, tmpfile]
|
|
144
|
+
end
|
|
145
|
+
|
|
104
146
|
private_class_method :calculate_checksum_from_io
|
|
105
147
|
end
|
|
106
148
|
end
|
|
@@ -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
|
|
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
|
|
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
|