extract_ttc 0.3.6 → 0.3.7
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/.gitignore +0 -4
- data/.rubocop.yml +7 -9
- data/.rubocop_todo.yml +135 -0
- data/Gemfile +6 -6
- data/README.adoc +856 -55
- data/Rakefile +7 -101
- data/exe/extract_ttc +7 -0
- data/extract_ttc.gemspec +3 -4
- data/lib/extract_ttc/cli.rb +47 -0
- data/lib/extract_ttc/commands/extract.rb +88 -0
- data/lib/extract_ttc/commands/info.rb +112 -0
- data/lib/extract_ttc/commands/list.rb +60 -0
- data/lib/extract_ttc/configuration.rb +126 -0
- data/lib/extract_ttc/constants.rb +42 -0
- data/lib/extract_ttc/models/extraction_result.rb +56 -0
- data/lib/extract_ttc/models/validation_result.rb +53 -0
- data/lib/extract_ttc/true_type_collection.rb +79 -0
- data/lib/extract_ttc/true_type_font.rb +239 -0
- data/lib/extract_ttc/utilities/checksum_calculator.rb +89 -0
- data/lib/extract_ttc/utilities/output_path_generator.rb +100 -0
- data/lib/extract_ttc/version.rb +1 -1
- data/lib/extract_ttc.rb +83 -55
- data/sig/extract_ttc/configuration.rbs +19 -0
- data/sig/extract_ttc/constants.rbs +17 -0
- data/sig/extract_ttc/models/extraction_result.rbs +19 -0
- data/sig/extract_ttc/models/font_data.rbs +17 -0
- data/sig/extract_ttc/models/table_directory_entry.rbs +15 -0
- data/sig/extract_ttc/models/true_type_collection_header.rbs +15 -0
- data/sig/extract_ttc/models/true_type_font_offset_table.rbs +17 -0
- data/sig/extract_ttc/models/validation_result.rbs +17 -0
- data/sig/extract_ttc/utilities/checksum_calculator.rbs +13 -0
- data/sig/extract_ttc/utilities/output_path_generator.rbs +11 -0
- data/sig/extract_ttc/validators/true_type_collection_validator.rbs +9 -0
- data/sig/extract_ttc.rbs +20 -0
- metadata +44 -28
- data/ext/stripttc/LICENSE +0 -31
- data/ext/stripttc/dummy.c +0 -2
- data/ext/stripttc/extconf.rb +0 -5
- data/ext/stripttc/stripttc.c +0 -187
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExtractTtc
|
|
4
|
+
module Models
|
|
5
|
+
# Represents the result of a validation operation
|
|
6
|
+
#
|
|
7
|
+
# This model encapsulates the outcome of validating files, headers, or other
|
|
8
|
+
# data structures, including whether the validation passed and any error
|
|
9
|
+
# messages generated during validation.
|
|
10
|
+
#
|
|
11
|
+
# This is an immutable value object with methods to query validity status.
|
|
12
|
+
class ValidationResult
|
|
13
|
+
attr_reader :valid, :errors
|
|
14
|
+
|
|
15
|
+
# Initialize a new validation result
|
|
16
|
+
#
|
|
17
|
+
# @param valid [Boolean] Whether the validation passed
|
|
18
|
+
# @param errors [Array<String>] Array of error messages (empty if valid)
|
|
19
|
+
def initialize(valid: true, errors: [])
|
|
20
|
+
@valid = valid
|
|
21
|
+
@errors = errors.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Check if the validation passed
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean] true if valid, false otherwise
|
|
27
|
+
def valid?
|
|
28
|
+
@valid
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if the validation failed
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean] true if invalid, false otherwise
|
|
34
|
+
def invalid?
|
|
35
|
+
!@valid
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Add an error message to the result
|
|
39
|
+
#
|
|
40
|
+
# This creates a new ValidationResult with the error added, as the object
|
|
41
|
+
# is immutable.
|
|
42
|
+
#
|
|
43
|
+
# @param message [String] The error message to add
|
|
44
|
+
# @return [ValidationResult] A new result object with the error added
|
|
45
|
+
def add_error(message)
|
|
46
|
+
self.class.new(
|
|
47
|
+
valid: false,
|
|
48
|
+
errors: @errors.dup << message,
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
require_relative "constants"
|
|
5
|
+
|
|
6
|
+
module ExtractTtc
|
|
7
|
+
# TrueType Collection domain object using BinData
|
|
8
|
+
#
|
|
9
|
+
# Represents a complete TrueType Collection file using BinData's declarative
|
|
10
|
+
# DSL for binary structure definition. The structure definition IS the
|
|
11
|
+
# documentation, and BinData handles all low-level reading/writing.
|
|
12
|
+
#
|
|
13
|
+
# @example Reading and extracting fonts
|
|
14
|
+
# File.open("Helvetica.ttc", "rb") do |io|
|
|
15
|
+
# ttc = TrueTypeCollection.read(io)
|
|
16
|
+
# puts ttc.num_fonts # => 6
|
|
17
|
+
# fonts = ttc.extract_fonts(io) # => [TrueTypeFont, TrueTypeFont, ...]
|
|
18
|
+
# end
|
|
19
|
+
class TrueTypeCollection < BinData::Record
|
|
20
|
+
endian :big
|
|
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
|
|
29
|
+
#
|
|
30
|
+
# @param path [String] Path to the TTC file
|
|
31
|
+
# @return [TrueTypeCollection] A new instance
|
|
32
|
+
# @raise [ArgumentError] if path is nil or empty
|
|
33
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
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}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Extract fonts as TrueTypeFont objects
|
|
50
|
+
#
|
|
51
|
+
# Reads each font from the TTC file and returns them as TrueTypeFont objects.
|
|
52
|
+
#
|
|
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"
|
|
57
|
+
|
|
58
|
+
font_offsets.map do |offset|
|
|
59
|
+
TrueTypeFont.from_ttc(io, offset)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Validate format correctness
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean] true if the format is valid, false otherwise
|
|
66
|
+
def valid?
|
|
67
|
+
tag == Constants::TTC_TAG && num_fonts.positive? && font_offsets.length == num_fonts
|
|
68
|
+
rescue StandardError
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get the TTC version as a single integer
|
|
73
|
+
#
|
|
74
|
+
# @return [Integer] Version number (e.g., 0x00010000 for version 1.0)
|
|
75
|
+
def version
|
|
76
|
+
(major_version << 16) | minor_version
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bindata"
|
|
4
|
+
require_relative "constants"
|
|
5
|
+
require_relative "utilities/checksum_calculator"
|
|
6
|
+
|
|
7
|
+
module ExtractTtc
|
|
8
|
+
# TTF Offset Table structure
|
|
9
|
+
class OffsetTable < BinData::Record
|
|
10
|
+
endian :big
|
|
11
|
+
uint32 :sfnt_version
|
|
12
|
+
uint16 :num_tables
|
|
13
|
+
uint16 :search_range
|
|
14
|
+
uint16 :entry_selector
|
|
15
|
+
uint16 :range_shift
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# TTF Table Directory Entry structure
|
|
19
|
+
class TableDirectory < BinData::Record
|
|
20
|
+
endian :big
|
|
21
|
+
string :tag, length: 4
|
|
22
|
+
uint32 :checksum
|
|
23
|
+
uint32 :offset
|
|
24
|
+
uint32 :table_length
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# TrueType Font domain object using BinData
|
|
28
|
+
#
|
|
29
|
+
# Represents a complete TrueType Font file using BinData's declarative
|
|
30
|
+
# DSL for binary structure definition. The structure definition IS the
|
|
31
|
+
# documentation, and BinData handles all low-level reading/writing.
|
|
32
|
+
#
|
|
33
|
+
# @example Writing a font
|
|
34
|
+
# ttc = TrueTypeCollection.from_file("Helvetica.ttc")
|
|
35
|
+
# fonts = ttc.extract_fonts
|
|
36
|
+
# fonts[0].to_file("Helvetica_0.ttf")
|
|
37
|
+
#
|
|
38
|
+
# @example Reading a font
|
|
39
|
+
# ttf = TrueTypeFont.from_file("font.ttf")
|
|
40
|
+
# puts ttf.header.num_tables # => 14
|
|
41
|
+
class TrueTypeFont < BinData::Record
|
|
42
|
+
endian :big
|
|
43
|
+
|
|
44
|
+
offset_table :header
|
|
45
|
+
array :tables, type: :table_directory, initial_length: -> {
|
|
46
|
+
header.num_tables
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Table data is stored separately since it's at variable offsets
|
|
50
|
+
attr_accessor :table_data
|
|
51
|
+
|
|
52
|
+
# Read TrueType Font from a file
|
|
53
|
+
#
|
|
54
|
+
# @param path [String] Path to the TTF file
|
|
55
|
+
# @return [TrueTypeFont] A new instance
|
|
56
|
+
# @raise [ArgumentError] if path is nil or empty
|
|
57
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
58
|
+
# @raise [RuntimeError] if file format is invalid
|
|
59
|
+
def self.from_file(path)
|
|
60
|
+
if path.nil? || path.to_s.empty?
|
|
61
|
+
raise ArgumentError,
|
|
62
|
+
"path cannot be nil or empty"
|
|
63
|
+
end
|
|
64
|
+
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
65
|
+
|
|
66
|
+
File.open(path, "rb") do |io|
|
|
67
|
+
font = read(io)
|
|
68
|
+
font.read_table_data(io)
|
|
69
|
+
font
|
|
70
|
+
end
|
|
71
|
+
rescue BinData::ValidityError, EOFError => e
|
|
72
|
+
raise "Invalid TTF file: #{e.message}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Read TrueType Font from TTC at specific offset
|
|
76
|
+
#
|
|
77
|
+
# @param io [IO] Open file handle
|
|
78
|
+
# @param offset [Integer] Byte offset to the font
|
|
79
|
+
# @return [TrueTypeFont] A new instance
|
|
80
|
+
def self.from_ttc(io, offset)
|
|
81
|
+
io.seek(offset)
|
|
82
|
+
font = read(io)
|
|
83
|
+
font.read_table_data(io)
|
|
84
|
+
font
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Read table data for all tables
|
|
88
|
+
#
|
|
89
|
+
# @param io [IO] Open file handle
|
|
90
|
+
# @return [void]
|
|
91
|
+
def read_table_data(io)
|
|
92
|
+
@table_data = {}
|
|
93
|
+
tables.each do |entry|
|
|
94
|
+
io.seek(entry.offset)
|
|
95
|
+
@table_data[entry.tag] = io.read(entry.table_length)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Write TrueType Font to a file
|
|
100
|
+
#
|
|
101
|
+
# Writes the complete TTF structure to disk, including proper checksum
|
|
102
|
+
# calculation and table alignment.
|
|
103
|
+
#
|
|
104
|
+
# @param path [String] Path where the TTF file will be written
|
|
105
|
+
# @return [Integer] Number of bytes written
|
|
106
|
+
# @raise [IOError] if writing fails
|
|
107
|
+
def to_file(path)
|
|
108
|
+
File.open(path, "wb") do |io|
|
|
109
|
+
# Write header and tables (directory)
|
|
110
|
+
write_structure(io)
|
|
111
|
+
|
|
112
|
+
# Write table data with updated offsets
|
|
113
|
+
write_table_data_with_offsets(io)
|
|
114
|
+
|
|
115
|
+
io.pos
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Update checksum adjustment in head table
|
|
119
|
+
update_checksum_adjustment_in_file(path) if head_table
|
|
120
|
+
|
|
121
|
+
File.size(path)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Validate format correctness
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean] true if the TTF format is valid, false otherwise
|
|
127
|
+
def valid?
|
|
128
|
+
return false unless header
|
|
129
|
+
return false unless tables.respond_to?(:length)
|
|
130
|
+
return false unless @table_data.is_a?(Hash)
|
|
131
|
+
return false if tables.length != header.num_tables
|
|
132
|
+
return false unless head_table
|
|
133
|
+
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check if font has a specific table
|
|
138
|
+
#
|
|
139
|
+
# @param tag [String] The table tag to check for
|
|
140
|
+
# @return [Boolean] true if table exists, false otherwise
|
|
141
|
+
def has_table?(tag)
|
|
142
|
+
tables.any? { |entry| entry.tag == tag }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Find a table entry by tag
|
|
146
|
+
#
|
|
147
|
+
# @param tag [String] The table tag to find
|
|
148
|
+
# @return [TableDirectory, nil] The table entry or nil
|
|
149
|
+
def find_table_entry(tag)
|
|
150
|
+
tables.find { |entry| entry.tag == tag }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get the head table entry
|
|
154
|
+
#
|
|
155
|
+
# @return [TableDirectory, nil] The head table entry or nil
|
|
156
|
+
def head_table
|
|
157
|
+
find_table_entry(Constants::HEAD_TAG)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Write the structure (header + table directory) to IO
|
|
163
|
+
#
|
|
164
|
+
# @param io [IO] Open file handle
|
|
165
|
+
# @return [void]
|
|
166
|
+
def write_structure(io)
|
|
167
|
+
# Write header
|
|
168
|
+
header.write(io)
|
|
169
|
+
|
|
170
|
+
# Write table directory with placeholder offsets
|
|
171
|
+
tables.each do |entry|
|
|
172
|
+
io.write(entry.tag)
|
|
173
|
+
io.write([entry.checksum].pack("N"))
|
|
174
|
+
io.write([0].pack("N")) # Placeholder offset
|
|
175
|
+
io.write([entry.table_length].pack("N"))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Write table data and update offsets in directory
|
|
180
|
+
#
|
|
181
|
+
# @param io [IO] Open file handle
|
|
182
|
+
# @return [void]
|
|
183
|
+
def write_table_data_with_offsets(io)
|
|
184
|
+
tables.each_with_index do |entry, index|
|
|
185
|
+
# Record current position
|
|
186
|
+
current_position = io.pos
|
|
187
|
+
|
|
188
|
+
# Write table data
|
|
189
|
+
data = @table_data[entry.tag]
|
|
190
|
+
raise IOError, "Missing table data for tag '#{entry.tag}'" if data.nil?
|
|
191
|
+
|
|
192
|
+
io.write(data)
|
|
193
|
+
|
|
194
|
+
# Add padding to align to 4-byte boundary
|
|
195
|
+
padding = (Constants::TABLE_ALIGNMENT - (io.pos % Constants::TABLE_ALIGNMENT)) % Constants::TABLE_ALIGNMENT
|
|
196
|
+
io.write("\x00" * padding) if padding.positive?
|
|
197
|
+
|
|
198
|
+
# Zero out checksumAdjustment field in head table
|
|
199
|
+
if entry.tag == Constants::HEAD_TAG
|
|
200
|
+
current_pos = io.pos
|
|
201
|
+
io.seek(current_position + 8)
|
|
202
|
+
io.write([0].pack("N"))
|
|
203
|
+
io.seek(current_pos)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Update offset in table directory
|
|
207
|
+
# Table directory starts at byte 12, each entry is 16 bytes
|
|
208
|
+
# Offset field is at byte 8 within each entry
|
|
209
|
+
directory_offset_position = 12 + (index * 16) + 8
|
|
210
|
+
current_pos = io.pos
|
|
211
|
+
io.seek(directory_offset_position)
|
|
212
|
+
io.write([current_position].pack("N"))
|
|
213
|
+
io.seek(current_pos)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Update checksumAdjustment field in head table
|
|
218
|
+
#
|
|
219
|
+
# @param path [String] Path to the TTF file
|
|
220
|
+
# @return [void]
|
|
221
|
+
def update_checksum_adjustment_in_file(path)
|
|
222
|
+
# Calculate file checksum
|
|
223
|
+
checksum = Utilities::ChecksumCalculator.calculate_file_checksum(path)
|
|
224
|
+
|
|
225
|
+
# Calculate adjustment
|
|
226
|
+
adjustment = Utilities::ChecksumCalculator.calculate_adjustment(checksum)
|
|
227
|
+
|
|
228
|
+
# Find head table position
|
|
229
|
+
head_entry = head_table
|
|
230
|
+
return unless head_entry
|
|
231
|
+
|
|
232
|
+
# Write adjustment to head table (offset 8 within head table)
|
|
233
|
+
File.open(path, "r+b") do |io|
|
|
234
|
+
io.seek(head_entry.offset + 8)
|
|
235
|
+
io.write([adjustment].pack("N"))
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../constants"
|
|
4
|
+
|
|
5
|
+
module ExtractTtc
|
|
6
|
+
module Utilities
|
|
7
|
+
# ChecksumCalculator provides stateless utility methods for calculating font file checksums.
|
|
8
|
+
#
|
|
9
|
+
# This class implements the TrueType/OpenType checksum algorithm which sums all uint32
|
|
10
|
+
# values in a file. The checksum is used to verify file integrity and calculate the
|
|
11
|
+
# checksumAdjustment value stored in the 'head' table.
|
|
12
|
+
#
|
|
13
|
+
# @example Calculate file checksum
|
|
14
|
+
# checksum = ChecksumCalculator.calculate_file_checksum("font.ttf")
|
|
15
|
+
# # => 2842116234
|
|
16
|
+
#
|
|
17
|
+
# @example Calculate checksum adjustment
|
|
18
|
+
# adjustment = ChecksumCalculator.calculate_adjustment(checksum)
|
|
19
|
+
# # => 1452851062
|
|
20
|
+
class ChecksumCalculator
|
|
21
|
+
# Calculate the checksum of an entire font file.
|
|
22
|
+
#
|
|
23
|
+
# The checksum is calculated by summing all uint32 (4-byte) values in the file.
|
|
24
|
+
# Files that are not multiples of 4 bytes are padded with zeros. The sum is
|
|
25
|
+
# masked to 32 bits to prevent overflow.
|
|
26
|
+
#
|
|
27
|
+
# @param file_path [String] path to the font file
|
|
28
|
+
# @return [Integer] the calculated uint32 checksum
|
|
29
|
+
# @raise [Errno::ENOENT] if the file does not exist
|
|
30
|
+
# @raise [Errno::EACCES] if the file cannot be read
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# checksum = ChecksumCalculator.calculate_file_checksum("font.ttf")
|
|
34
|
+
# # => 2842116234
|
|
35
|
+
def self.calculate_file_checksum(file_path)
|
|
36
|
+
File.open(file_path, "rb") do |file|
|
|
37
|
+
calculate_checksum_from_io(file)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Calculate the checksum adjustment value for the 'head' table.
|
|
42
|
+
#
|
|
43
|
+
# The checksum adjustment is stored at offset 8 in the 'head' table and is
|
|
44
|
+
# calculated as: CHECKSUM_ADJUSTMENT_MAGIC - file_checksum.
|
|
45
|
+
# This value ensures that the checksum of the entire font file equals the
|
|
46
|
+
# magic number.
|
|
47
|
+
#
|
|
48
|
+
# @param file_checksum [Integer] the calculated file checksum
|
|
49
|
+
# @return [Integer] the checksum adjustment value to write to the 'head' table
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# adjustment = ChecksumCalculator.calculate_adjustment(2842116234)
|
|
53
|
+
# # => 1452851062
|
|
54
|
+
def self.calculate_adjustment(file_checksum)
|
|
55
|
+
(Constants::CHECKSUM_ADJUSTMENT_MAGIC - file_checksum) & 0xFFFFFFFF
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Calculate checksum from an IO object.
|
|
59
|
+
#
|
|
60
|
+
# Reads the IO stream in 4-byte chunks and calculates the uint32 checksum.
|
|
61
|
+
# This is the core checksum algorithm implementation.
|
|
62
|
+
#
|
|
63
|
+
# @param io [IO] the IO object to read from
|
|
64
|
+
# @return [Integer] the calculated uint32 checksum
|
|
65
|
+
# @api private
|
|
66
|
+
def self.calculate_checksum_from_io(io)
|
|
67
|
+
io.rewind
|
|
68
|
+
sum = 0
|
|
69
|
+
|
|
70
|
+
until io.eof?
|
|
71
|
+
# Read 4 bytes at a time
|
|
72
|
+
bytes = io.read(4)
|
|
73
|
+
break if bytes.nil? || bytes.empty?
|
|
74
|
+
|
|
75
|
+
# Pad with zeros if less than 4 bytes
|
|
76
|
+
bytes += "\0" * (4 - bytes.length) if bytes.length < 4
|
|
77
|
+
|
|
78
|
+
# Convert to uint32 (network byte order, big-endian)
|
|
79
|
+
value = bytes.unpack1("N")
|
|
80
|
+
sum = (sum + value) & 0xFFFFFFFF
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sum
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method :calculate_checksum_from_io
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExtractTtc
|
|
4
|
+
module Utilities
|
|
5
|
+
# OutputPathGenerator provides stateless utility methods for generating output file paths.
|
|
6
|
+
#
|
|
7
|
+
# This class generates standardized output paths for extracted TTF files from TTC files,
|
|
8
|
+
# using a consistent naming convention with zero-padded indices.
|
|
9
|
+
#
|
|
10
|
+
# @example Generate output path with defaults
|
|
11
|
+
# path = OutputPathGenerator.generate("Helvetica.ttc", 0)
|
|
12
|
+
# # => "Helvetica_00.ttf"
|
|
13
|
+
#
|
|
14
|
+
# @example Generate output path with custom directory
|
|
15
|
+
# path = OutputPathGenerator.generate("Helvetica.ttc", 5, output_dir: "/tmp/fonts")
|
|
16
|
+
# # => "/tmp/fonts/Helvetica_05.ttf"
|
|
17
|
+
class OutputPathGenerator
|
|
18
|
+
# Default format string for zero-padded font indices.
|
|
19
|
+
# Produces two-digit indices (00, 01, 02, etc.)
|
|
20
|
+
DEFAULT_INDEX_FORMAT = "%02d"
|
|
21
|
+
|
|
22
|
+
# Generate an output TTF file path for an extracted font.
|
|
23
|
+
#
|
|
24
|
+
# The output path is constructed from the input file's basename, a zero-padded
|
|
25
|
+
# font index, and an optional output directory. The resulting filename follows
|
|
26
|
+
# the pattern: "basename_XX.ttf" where XX is the zero-padded index.
|
|
27
|
+
#
|
|
28
|
+
# @param input_path [String] path to the input TTC file
|
|
29
|
+
# @param font_index [Integer] zero-based index of the font being extracted
|
|
30
|
+
# @param output_dir [String, nil] optional output directory (defaults to current directory ".")
|
|
31
|
+
# @return [String] the generated output file path
|
|
32
|
+
# @raise [ArgumentError] if font_index is negative
|
|
33
|
+
#
|
|
34
|
+
# @example Generate path in current directory
|
|
35
|
+
# OutputPathGenerator.generate("fonts/Helvetica.ttc", 0)
|
|
36
|
+
# # => "Helvetica_00.ttf"
|
|
37
|
+
#
|
|
38
|
+
# @example Generate path in specific directory
|
|
39
|
+
# OutputPathGenerator.generate("Helvetica.ttc", 3, output_dir: "/tmp")
|
|
40
|
+
# # => "/tmp/Helvetica_03.ttf"
|
|
41
|
+
#
|
|
42
|
+
# @example High font index
|
|
43
|
+
# OutputPathGenerator.generate("Font.ttc", 15)
|
|
44
|
+
# # => "Font_15.ttf"
|
|
45
|
+
def self.generate(input_path, font_index, output_dir: nil)
|
|
46
|
+
if font_index.negative?
|
|
47
|
+
raise ArgumentError,
|
|
48
|
+
"font_index must be non-negative"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
basename = File.basename(input_path, ".*")
|
|
52
|
+
formatted_index = sprintf(DEFAULT_INDEX_FORMAT, font_index)
|
|
53
|
+
filename = "#{basename}_#{formatted_index}.ttf"
|
|
54
|
+
|
|
55
|
+
if output_dir.nil? || output_dir.empty? || output_dir == "."
|
|
56
|
+
filename
|
|
57
|
+
else
|
|
58
|
+
File.join(output_dir, filename)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Generate output path with a custom index format.
|
|
63
|
+
#
|
|
64
|
+
# Allows specifying a custom sprintf format string for the index padding,
|
|
65
|
+
# enabling different padding widths or styles.
|
|
66
|
+
#
|
|
67
|
+
# @param input_path [String] path to the input TTC file
|
|
68
|
+
# @param font_index [Integer] zero-based index of the font being extracted
|
|
69
|
+
# @param index_format [String] sprintf format string for index formatting
|
|
70
|
+
# @param output_dir [String, nil] optional output directory
|
|
71
|
+
# @return [String] the generated output file path
|
|
72
|
+
# @raise [ArgumentError] if font_index is negative
|
|
73
|
+
#
|
|
74
|
+
# @example Three-digit padding
|
|
75
|
+
# OutputPathGenerator.generate_with_format("Font.ttc", 5, "%03d")
|
|
76
|
+
# # => "Font_005.ttf"
|
|
77
|
+
#
|
|
78
|
+
# @example No padding
|
|
79
|
+
# OutputPathGenerator.generate_with_format("Font.ttc", 5, "%d")
|
|
80
|
+
# # => "Font_5.ttf"
|
|
81
|
+
def self.generate_with_format(input_path, font_index, index_format,
|
|
82
|
+
output_dir: nil)
|
|
83
|
+
if font_index.negative?
|
|
84
|
+
raise ArgumentError,
|
|
85
|
+
"font_index must be non-negative"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
basename = File.basename(input_path, ".*")
|
|
89
|
+
formatted_index = sprintf(index_format, font_index)
|
|
90
|
+
filename = "#{basename}_#{formatted_index}.ttf"
|
|
91
|
+
|
|
92
|
+
if output_dir.nil? || output_dir.empty?
|
|
93
|
+
filename
|
|
94
|
+
else
|
|
95
|
+
File.join(output_dir, filename)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/extract_ttc/version.rb
CHANGED