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
data/Rakefile
CHANGED
|
@@ -1,106 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
require "rake/clean"
|
|
3
|
-
require "rake/extensiontask"
|
|
4
|
-
require "rake_compiler_dock"
|
|
5
|
-
|
|
6
|
-
require "rubygems"
|
|
7
|
-
require "rubygems/package_task"
|
|
8
|
-
|
|
9
|
-
# ++ Allow rake-compiler-dock configuration without dev. dependencies
|
|
10
|
-
begin
|
|
11
|
-
require "rubocop/rake_task"
|
|
12
|
-
RuboCop::RakeTask.new
|
|
13
|
-
rescue LoadError
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
begin
|
|
17
|
-
require "rspec/core/rake_task"
|
|
18
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
19
|
-
rescue LoadError
|
|
20
|
-
end
|
|
21
|
-
# -- Allow rake-compiler-dock configuration without dev. dependencies
|
|
22
|
-
|
|
23
|
-
ruby_cc_version = "3.1.0"
|
|
24
|
-
bundler_ver = ENV["BUNDLER_VER"] || "2.3.22"
|
|
25
|
-
|
|
26
|
-
task default: :spec
|
|
27
|
-
task spec: :compile
|
|
28
|
-
|
|
29
|
-
spec = Gem::Specification.load("extract_ttc.gemspec")
|
|
1
|
+
# frozen_string_literal: true
|
|
30
2
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
aarch64-linux
|
|
34
|
-
x64-mingw32
|
|
35
|
-
x64-mingw-ucrt
|
|
36
|
-
x86_64-darwin
|
|
37
|
-
arm64-darwin
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
# TODO automate build with:
|
|
41
|
-
# "rbsys/x86_64-linux-musl:latest" - for x86_64-linux-musl
|
|
42
|
-
# "*" - find/create image for aarch64-linux-musl
|
|
43
|
-
ext_thru_musl_cc = %w[x86_64-linux-musl aarch64-linux-musl]
|
|
44
|
-
|
|
45
|
-
# HACK: Prevent rake-compiler from overriding required_ruby_version,
|
|
46
|
-
# because the shared library here is Ruby-agnostic.
|
|
47
|
-
# See https://github.com/rake-compiler/rake-compiler/issues/153
|
|
48
|
-
module FixRequiredRubyVersion
|
|
49
|
-
def required_ruby_version=(*); end
|
|
50
|
-
end
|
|
51
|
-
Gem::Specification.prepend(FixRequiredRubyVersion)
|
|
52
|
-
|
|
53
|
-
exttask = Rake::ExtensionTask.new("stripttc", spec) do |ext|
|
|
54
|
-
ext.lib_dir = "lib"
|
|
55
|
-
ext.cross_compile = true
|
|
56
|
-
ext.cross_platform = ext_thru_rc_dock + ext_thru_musl_cc
|
|
57
|
-
ext.cross_compiling do |s|
|
|
58
|
-
s.files.reject! { |path| File.fnmatch?("ext/*", path) }
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
namespace "gem" do
|
|
63
|
-
desc "Cache dependencies"
|
|
64
|
-
task "cache" do
|
|
65
|
-
sh <<~RCD
|
|
66
|
-
bundle config set cache_all true &&
|
|
67
|
-
bundle config set --local without 'development' &&
|
|
68
|
-
bundle package
|
|
69
|
-
RCD
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
ext_thru_rc_dock.each do |plat|
|
|
73
|
-
desc "Build native gems with rake-compiler-dock in parallel"
|
|
74
|
-
multitask "parallel" => plat
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
75
5
|
|
|
76
|
-
|
|
77
|
-
task plat => "cache" do
|
|
78
|
-
ruby_cc_ver = if plat == "x64-mingw32"
|
|
79
|
-
"3.0.0"
|
|
80
|
-
else
|
|
81
|
-
ruby_cc_version
|
|
82
|
-
end
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
83
7
|
|
|
84
|
-
|
|
85
|
-
gem install bundler:#{bundler_ver} --no-document &&
|
|
86
|
-
bundle install --local &&
|
|
87
|
-
bundle exec rake native:#{plat} \
|
|
88
|
-
pkg/#{exttask.gem_spec.full_name}-#{plat}.gem \
|
|
89
|
-
RUBY_CC_VERSION=#{ruby_cc_ver}
|
|
90
|
-
RCD
|
|
91
|
-
end
|
|
92
|
-
end
|
|
8
|
+
require "rubocop/rake_task"
|
|
93
9
|
|
|
94
|
-
|
|
95
|
-
desc "Define the gem task to build on the #{plat} platform (binary gem)"
|
|
96
|
-
task plat do
|
|
97
|
-
s = spec.dup
|
|
98
|
-
s.platform = Gem::Platform.new(plat)
|
|
99
|
-
s.files += Dir.glob("lib/extract_ttc/*.{dll,so,dylib}")
|
|
100
|
-
s.extensions = []
|
|
10
|
+
RuboCop::RakeTask.new
|
|
101
11
|
|
|
102
|
-
|
|
103
|
-
task.define
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
12
|
+
task default: %i[spec rubocop]
|
data/exe/extract_ttc
ADDED
data/extract_ttc.gemspec
CHANGED
|
@@ -25,10 +25,9 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
26
26
|
spec.require_paths = ["lib"]
|
|
27
27
|
|
|
28
|
-
spec.
|
|
29
|
-
spec.
|
|
30
|
-
spec.
|
|
28
|
+
spec.add_dependency "bindata", "~> 2.5"
|
|
29
|
+
spec.add_dependency "paint", "~> 2.0"
|
|
30
|
+
spec.add_dependency "thor", "~> 1.4"
|
|
31
31
|
|
|
32
|
-
spec.extensions << "ext/stripttc/extconf.rb"
|
|
33
32
|
spec.metadata["rubygems_mfa_required"] = "false"
|
|
34
33
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "commands/extract"
|
|
5
|
+
require_relative "commands/list"
|
|
6
|
+
require_relative "commands/info"
|
|
7
|
+
|
|
8
|
+
module ExtractTtc
|
|
9
|
+
class Cli < Thor
|
|
10
|
+
desc "extract FILE", "Extract TTF files from a TTC file"
|
|
11
|
+
method_option :output_dir,
|
|
12
|
+
aliases: "-o",
|
|
13
|
+
type: :string,
|
|
14
|
+
desc: "Output directory for TTF files"
|
|
15
|
+
method_option :verbose,
|
|
16
|
+
aliases: "-v",
|
|
17
|
+
type: :boolean,
|
|
18
|
+
default: false,
|
|
19
|
+
desc: "Enable verbose output"
|
|
20
|
+
def extract(file)
|
|
21
|
+
exit_code = ExtractCommand.new(options).run(file)
|
|
22
|
+
exit(exit_code) unless exit_code.zero?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
desc "ls FILE", "List fonts contained in a TTC file"
|
|
26
|
+
method_option :verbose,
|
|
27
|
+
aliases: "-v",
|
|
28
|
+
type: :boolean,
|
|
29
|
+
default: false,
|
|
30
|
+
desc: "Enable verbose output"
|
|
31
|
+
def ls(file)
|
|
32
|
+
exit_code = ListCommand.new(options).run(file)
|
|
33
|
+
exit(exit_code) unless exit_code.zero?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "info FILE", "Show detailed information about a TTC file"
|
|
37
|
+
method_option :verbose,
|
|
38
|
+
aliases: "-v",
|
|
39
|
+
type: :boolean,
|
|
40
|
+
default: false,
|
|
41
|
+
desc: "Show detailed font information"
|
|
42
|
+
def info(file)
|
|
43
|
+
exit_code = InfoCommand.new(options).run(file)
|
|
44
|
+
exit(exit_code) unless exit_code.zero?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "paint"
|
|
4
|
+
|
|
5
|
+
module ExtractTtc
|
|
6
|
+
class ExtractCommand
|
|
7
|
+
def initialize(options = {})
|
|
8
|
+
@options = options
|
|
9
|
+
@verbose = options[:verbose] || false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run(file_path)
|
|
13
|
+
validate_file_exists(file_path)
|
|
14
|
+
ensure_output_directory
|
|
15
|
+
|
|
16
|
+
log_verbose("Extracting fonts from #{file_path}...")
|
|
17
|
+
|
|
18
|
+
output_paths = ExtractTtc.extract(
|
|
19
|
+
file_path,
|
|
20
|
+
output_dir: @options[:output_dir],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
display_results(output_paths)
|
|
24
|
+
|
|
25
|
+
0 # Success exit code
|
|
26
|
+
rescue ExtractTtc::ReadFileError => e
|
|
27
|
+
display_error("File read error: #{e.message}")
|
|
28
|
+
1
|
|
29
|
+
rescue ExtractTtc::InvalidFileError => e
|
|
30
|
+
display_error("Invalid file: #{e.message}")
|
|
31
|
+
2
|
|
32
|
+
rescue ExtractTtc::WriteFileError => e
|
|
33
|
+
display_error("Write error: #{e.message}")
|
|
34
|
+
3
|
|
35
|
+
rescue ExtractTtc::Error => e
|
|
36
|
+
display_error("Extraction error: #{e.message}")
|
|
37
|
+
4
|
|
38
|
+
rescue RuntimeError => e
|
|
39
|
+
# BinData errors often come as RuntimeError
|
|
40
|
+
display_error("Invalid file: #{e.message}")
|
|
41
|
+
2
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
display_error("Unexpected error: #{e.message}")
|
|
44
|
+
5
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :options, :verbose
|
|
50
|
+
|
|
51
|
+
def validate_file_exists(file_path)
|
|
52
|
+
return if File.exist?(file_path)
|
|
53
|
+
|
|
54
|
+
raise ExtractTtc::ReadFileError, "File not found: #{file_path}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ensure_output_directory
|
|
58
|
+
return unless @options[:output_dir]
|
|
59
|
+
return if File.directory?(@options[:output_dir])
|
|
60
|
+
|
|
61
|
+
require "fileutils"
|
|
62
|
+
FileUtils.mkdir_p(@options[:output_dir])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def display_results(output_paths)
|
|
66
|
+
if output_paths.empty?
|
|
67
|
+
puts Paint["⚠️ No fonts were extracted.", :yellow]
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
puts Paint["✅ Successfully extracted #{output_paths.size} font(s):",
|
|
72
|
+
:green, :bold]
|
|
73
|
+
output_paths.each do |path|
|
|
74
|
+
puts " #{Paint['📄', :cyan]} #{path}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def display_error(message)
|
|
79
|
+
warn Paint["❌ Error: ", :red] + message
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def log_verbose(message)
|
|
83
|
+
return unless @verbose
|
|
84
|
+
|
|
85
|
+
puts Paint["ℹ️ ", :white] + message
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "paint"
|
|
4
|
+
|
|
5
|
+
module ExtractTtc
|
|
6
|
+
# Command to show detailed information about a TTC file
|
|
7
|
+
class InfoCommand
|
|
8
|
+
def initialize(options = {})
|
|
9
|
+
@options = options
|
|
10
|
+
@verbose = options[:verbose] || false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(file_path)
|
|
14
|
+
validate_file_exists(file_path)
|
|
15
|
+
|
|
16
|
+
File.open(file_path, "rb") do |file|
|
|
17
|
+
ttc = TrueTypeCollection.read(file)
|
|
18
|
+
|
|
19
|
+
display_header_info(ttc, file_path)
|
|
20
|
+
display_font_info(ttc, file) if @verbose
|
|
21
|
+
|
|
22
|
+
0 # Success
|
|
23
|
+
end
|
|
24
|
+
rescue ExtractTtc::ReadFileError => e
|
|
25
|
+
display_error("File read error: #{e.message}")
|
|
26
|
+
1
|
|
27
|
+
rescue ExtractTtc::InvalidFileError => e
|
|
28
|
+
display_error("Invalid file: #{e.message}")
|
|
29
|
+
2
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
display_error("Error: #{e.message}")
|
|
32
|
+
3
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def validate_file_exists(file_path)
|
|
38
|
+
return if File.exist?(file_path)
|
|
39
|
+
|
|
40
|
+
raise ExtractTtc::ReadFileError, "File not found: #{file_path}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def display_header_info(ttc, file_path)
|
|
44
|
+
filesize = File.size(file_path)
|
|
45
|
+
|
|
46
|
+
puts Paint["═══ TTC File Information ═══", :cyan, :bold]
|
|
47
|
+
puts
|
|
48
|
+
puts Paint["📦 File: ", :bold] + file_path
|
|
49
|
+
puts Paint["💾 Size: ", :bold] + Paint[format_bytes(filesize), :green]
|
|
50
|
+
puts
|
|
51
|
+
puts Paint["═══ Header ═══", :cyan, :bold]
|
|
52
|
+
puts Paint["🏷️ Tag: ", :bold] + Paint[ttc.tag.to_s, :yellow]
|
|
53
|
+
puts Paint["📌 Version: ",
|
|
54
|
+
:bold] + "#{ttc.major_version}.#{ttc.minor_version}" +
|
|
55
|
+
Paint[" (0x#{ttc.version.to_i.to_s(16).upcase})", :white]
|
|
56
|
+
puts Paint["🔢 Number of fonts: ",
|
|
57
|
+
:bold] + Paint[ttc.num_fonts.to_s, :green]
|
|
58
|
+
puts
|
|
59
|
+
puts Paint["═══ Font Offsets ═══", :cyan, :bold]
|
|
60
|
+
ttc.font_offsets.each_with_index do |offset, index|
|
|
61
|
+
puts "#{Paint[" #{index}.",
|
|
62
|
+
:white]} #{Paint['Offset: ',
|
|
63
|
+
:bold]}#{offset.to_s.rjust(8)}#{Paint[" (0x#{offset.to_i.to_s(16).upcase})",
|
|
64
|
+
:white]}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def display_font_info(ttc, file)
|
|
69
|
+
puts
|
|
70
|
+
puts Paint["═══ Font Details ═══", :cyan, :bold]
|
|
71
|
+
|
|
72
|
+
ttc.num_fonts.times do |index|
|
|
73
|
+
offset = ttc.font_offsets[index]
|
|
74
|
+
font = TrueTypeFont.from_ttc(file, offset)
|
|
75
|
+
|
|
76
|
+
puts
|
|
77
|
+
puts Paint["📝 Font #{index}:", :magenta, :bold]
|
|
78
|
+
puts " #{Paint['SFNT version: ',
|
|
79
|
+
:bold]}#{Paint["0x#{font.header.sfnt_version.to_i.to_s(16).upcase}",
|
|
80
|
+
:cyan]}"
|
|
81
|
+
puts " #{Paint['Number of tables: ',
|
|
82
|
+
:bold]}#{Paint[font.header.num_tables.to_s, :green]}"
|
|
83
|
+
puts " #{Paint['Tables:', :bold]}"
|
|
84
|
+
|
|
85
|
+
font.tables.each do |table|
|
|
86
|
+
puts " " + Paint["•", :yellow] + " " +
|
|
87
|
+
table.tag.to_s.ljust(8) +
|
|
88
|
+
Paint["checksum: ",
|
|
89
|
+
:white] + Paint["0x#{table.checksum.to_i.to_s(16).upcase.rjust(8, '0')}",
|
|
90
|
+
:cyan] +
|
|
91
|
+
Paint[" offset: ", :white] + table.offset.to_i.to_s.rjust(8) +
|
|
92
|
+
Paint[" length: ",
|
|
93
|
+
:white] + Paint[table.table_length.to_i.to_s.rjust(8), :green]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def format_bytes(bytes)
|
|
99
|
+
if bytes < 1024
|
|
100
|
+
"#{bytes} B"
|
|
101
|
+
elsif bytes < 1024 * 1024
|
|
102
|
+
"#{(bytes / 1024.0).round(2)} KB"
|
|
103
|
+
else
|
|
104
|
+
"#{(bytes / (1024.0 * 1024)).round(2)} MB"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def display_error(message)
|
|
109
|
+
warn Paint["❌ Error: ", :red] + message
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "paint"
|
|
4
|
+
|
|
5
|
+
module ExtractTtc
|
|
6
|
+
# Command to list fonts contained in a TTC file
|
|
7
|
+
class ListCommand
|
|
8
|
+
def initialize(options = {})
|
|
9
|
+
@options = options
|
|
10
|
+
@verbose = options[:verbose] || false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(file_path)
|
|
14
|
+
validate_file_exists(file_path)
|
|
15
|
+
|
|
16
|
+
File.open(file_path, "rb") do |file|
|
|
17
|
+
ttc = TrueTypeCollection.read(file)
|
|
18
|
+
|
|
19
|
+
display_font_list(ttc, file_path)
|
|
20
|
+
|
|
21
|
+
0 # Success
|
|
22
|
+
end
|
|
23
|
+
rescue ExtractTtc::ReadFileError => e
|
|
24
|
+
display_error("File read error: #{e.message}")
|
|
25
|
+
1
|
|
26
|
+
rescue ExtractTtc::InvalidFileError => e
|
|
27
|
+
display_error("Invalid file: #{e.message}")
|
|
28
|
+
2
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
display_error("Error: #{e.message}")
|
|
31
|
+
3
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def validate_file_exists(file_path)
|
|
37
|
+
return if File.exist?(file_path)
|
|
38
|
+
|
|
39
|
+
raise ExtractTtc::ReadFileError, "File not found: #{file_path}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def display_font_list(ttc, file_path)
|
|
43
|
+
puts Paint["📦 TTC File: ", :cyan, :bold] + file_path
|
|
44
|
+
puts Paint[" Fonts: ", :bold] + Paint[ttc.num_fonts.to_s, :green]
|
|
45
|
+
puts
|
|
46
|
+
|
|
47
|
+
ttc.num_fonts.times do |index|
|
|
48
|
+
output_name = Utilities::OutputPathGenerator.generate(
|
|
49
|
+
file_path,
|
|
50
|
+
index,
|
|
51
|
+
)
|
|
52
|
+
puts "#{Paint[" #{index}.", :white]} 📄 #{Paint[output_name, :yellow]}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def display_error(message)
|
|
57
|
+
warn Paint["❌ Error: ", :red] + message
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExtractTtc
|
|
4
|
+
# Configuration class for runtime settings
|
|
5
|
+
#
|
|
6
|
+
# This plain Ruby class encapsulates all runtime configuration options for
|
|
7
|
+
# the extract_ttc gem. It provides sensible defaults and supports merging
|
|
8
|
+
# with user-provided options.
|
|
9
|
+
#
|
|
10
|
+
# The configuration is immutable by design - the merge method returns a new
|
|
11
|
+
# Configuration instance rather than modifying the existing one.
|
|
12
|
+
#
|
|
13
|
+
# @example Creating a default configuration
|
|
14
|
+
# config = Configuration.default
|
|
15
|
+
# config.output_directory # => "."
|
|
16
|
+
# config.verbose # => false
|
|
17
|
+
#
|
|
18
|
+
# @example Merging with custom options
|
|
19
|
+
# config = Configuration.default
|
|
20
|
+
# new_config = config.merge(output_directory: "/tmp", verbose: true)
|
|
21
|
+
# new_config.output_directory # => "/tmp"
|
|
22
|
+
# new_config.verbose # => true
|
|
23
|
+
# config.verbose # => false (original unchanged)
|
|
24
|
+
class Configuration
|
|
25
|
+
# @return [String] Directory path where extracted fonts will be written
|
|
26
|
+
attr_accessor :output_directory
|
|
27
|
+
|
|
28
|
+
# @return [Boolean] Whether to overwrite existing files during extraction
|
|
29
|
+
attr_accessor :overwrite_existing
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] Whether to validate font table checksums
|
|
32
|
+
attr_accessor :validate_checksums
|
|
33
|
+
|
|
34
|
+
# @return [Boolean] Whether to enable verbose output during operations
|
|
35
|
+
attr_accessor :verbose
|
|
36
|
+
|
|
37
|
+
# Initialize a new configuration instance
|
|
38
|
+
#
|
|
39
|
+
# @param output_directory [String] Directory for output files (default: ".")
|
|
40
|
+
# @param overwrite_existing [Boolean] Allow overwriting files (default: false)
|
|
41
|
+
# @param validate_checksums [Boolean] Validate table checksums (default: true)
|
|
42
|
+
# @param verbose [Boolean] Enable verbose output (default: false)
|
|
43
|
+
def initialize(
|
|
44
|
+
output_directory: ".",
|
|
45
|
+
overwrite_existing: false,
|
|
46
|
+
validate_checksums: true,
|
|
47
|
+
verbose: false
|
|
48
|
+
)
|
|
49
|
+
@output_directory = output_directory
|
|
50
|
+
@overwrite_existing = overwrite_existing
|
|
51
|
+
@validate_checksums = validate_checksums
|
|
52
|
+
@verbose = verbose
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Create a configuration instance with default values
|
|
56
|
+
#
|
|
57
|
+
# This is a convenience factory method that returns a new Configuration
|
|
58
|
+
# with all default settings applied.
|
|
59
|
+
#
|
|
60
|
+
# @return [Configuration] A new configuration with defaults
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# config = Configuration.default
|
|
64
|
+
# config.output_directory # => "."
|
|
65
|
+
# config.overwrite_existing # => false
|
|
66
|
+
def self.default
|
|
67
|
+
new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Merge this configuration with new options
|
|
71
|
+
#
|
|
72
|
+
# Creates a new Configuration instance with values merged from the provided
|
|
73
|
+
# options hash. The original configuration is not modified, ensuring
|
|
74
|
+
# immutability.
|
|
75
|
+
#
|
|
76
|
+
# @param options [Hash] Hash of configuration options to merge
|
|
77
|
+
# @option options [String] :output_directory Directory for output files
|
|
78
|
+
# @option options [Boolean] :overwrite_existing Allow overwriting files
|
|
79
|
+
# @option options [Boolean] :validate_checksums Validate table checksums
|
|
80
|
+
# @option options [Boolean] :verbose Enable verbose output
|
|
81
|
+
# @return [Configuration] A new configuration with merged values
|
|
82
|
+
#
|
|
83
|
+
# @example Merging with new options
|
|
84
|
+
# config = Configuration.default
|
|
85
|
+
# new_config = config.merge(verbose: true, output_directory: "/tmp")
|
|
86
|
+
# new_config.verbose # => true
|
|
87
|
+
# new_config.output_directory # => "/tmp"
|
|
88
|
+
# new_config.overwrite_existing # => false (from original)
|
|
89
|
+
def merge(options)
|
|
90
|
+
self.class.new(
|
|
91
|
+
output_directory: options.fetch(:output_directory, @output_directory),
|
|
92
|
+
overwrite_existing: options.fetch(:overwrite_existing,
|
|
93
|
+
@overwrite_existing),
|
|
94
|
+
validate_checksums: options.fetch(:validate_checksums,
|
|
95
|
+
@validate_checksums),
|
|
96
|
+
verbose: options.fetch(:verbose, @verbose),
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Convert configuration to hash representation
|
|
101
|
+
#
|
|
102
|
+
# Returns a hash containing all configuration settings with their current
|
|
103
|
+
# values. This is useful for serialization, debugging, or passing the
|
|
104
|
+
# configuration to other components.
|
|
105
|
+
#
|
|
106
|
+
# @return [Hash] Hash representation of the configuration
|
|
107
|
+
#
|
|
108
|
+
# @example Converting to hash
|
|
109
|
+
# config = Configuration.default.merge(verbose: true)
|
|
110
|
+
# config.to_h
|
|
111
|
+
# # => {
|
|
112
|
+
# # output_directory: ".",
|
|
113
|
+
# # overwrite_existing: false,
|
|
114
|
+
# # validate_checksums: true,
|
|
115
|
+
# # verbose: true
|
|
116
|
+
# # }
|
|
117
|
+
def to_h
|
|
118
|
+
{
|
|
119
|
+
output_directory: @output_directory,
|
|
120
|
+
overwrite_existing: @overwrite_existing,
|
|
121
|
+
validate_checksums: @validate_checksums,
|
|
122
|
+
verbose: @verbose,
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExtractTtc
|
|
4
|
+
# Constants module containing immutable constant definitions for TTC/TTF font file operations.
|
|
5
|
+
#
|
|
6
|
+
# This module defines all magic numbers, version identifiers, and file format constants
|
|
7
|
+
# used throughout the extract_ttc gem. These values are based on the TrueType Collection
|
|
8
|
+
# and TrueType Font specifications.
|
|
9
|
+
module Constants
|
|
10
|
+
# TrueType Collection file signature tag.
|
|
11
|
+
# All valid TTC files must begin with this 4-byte tag.
|
|
12
|
+
TTC_TAG = "ttcf"
|
|
13
|
+
|
|
14
|
+
# TrueType Collection Version 1.0 identifier.
|
|
15
|
+
# Represents the original TTC format version.
|
|
16
|
+
TTC_VERSION_1 = 0x00010000
|
|
17
|
+
|
|
18
|
+
# TrueType Collection Version 2.0 identifier.
|
|
19
|
+
# Represents the extended TTC format with digital signature support.
|
|
20
|
+
TTC_VERSION_2 = 0x00020000
|
|
21
|
+
|
|
22
|
+
# Head table tag identifier.
|
|
23
|
+
# The 'head' table contains global font header information including
|
|
24
|
+
# the checksum adjustment field.
|
|
25
|
+
HEAD_TAG = "head"
|
|
26
|
+
|
|
27
|
+
# Magic number used for font file checksum adjustment calculation.
|
|
28
|
+
# This constant is used in conjunction with the file checksum to compute
|
|
29
|
+
# the checksumAdjustment value stored in the 'head' table.
|
|
30
|
+
# Formula: checksumAdjustment = CHECKSUM_ADJUSTMENT_MAGIC - file_checksum
|
|
31
|
+
CHECKSUM_ADJUSTMENT_MAGIC = 0xB1B0AFBA
|
|
32
|
+
|
|
33
|
+
# Supported TTC version numbers.
|
|
34
|
+
# An array of valid version identifiers for TrueType Collection files.
|
|
35
|
+
SUPPORTED_VERSIONS = [TTC_VERSION_1, TTC_VERSION_2].freeze
|
|
36
|
+
|
|
37
|
+
# Table data alignment boundary in bytes.
|
|
38
|
+
# All table data in TTF files must be aligned to 4-byte boundaries,
|
|
39
|
+
# with padding added as necessary.
|
|
40
|
+
TABLE_ALIGNMENT = 4
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ExtractTtc
|
|
4
|
+
module Models
|
|
5
|
+
# Represents the result of a font extraction operation
|
|
6
|
+
#
|
|
7
|
+
# This model encapsulates the outcome of extracting fonts from a TTC file,
|
|
8
|
+
# including the list of output files created, success status, and any errors
|
|
9
|
+
# encountered during the process.
|
|
10
|
+
#
|
|
11
|
+
# This is an immutable value object with methods to query success/failure status.
|
|
12
|
+
class ExtractionResult
|
|
13
|
+
attr_reader :output_files, :success, :errors
|
|
14
|
+
|
|
15
|
+
# Initialize a new extraction result
|
|
16
|
+
#
|
|
17
|
+
# @param output_files [Array<String>] Array of output file paths created
|
|
18
|
+
# @param success [Boolean] Whether the extraction was successful
|
|
19
|
+
# @param errors [Array<String>] Array of error messages (empty if successful)
|
|
20
|
+
def initialize(output_files: [], success: true, errors: [])
|
|
21
|
+
@output_files = output_files.freeze
|
|
22
|
+
@success = success
|
|
23
|
+
@errors = errors.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Check if the extraction was successful
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if successful, false otherwise
|
|
29
|
+
def success?
|
|
30
|
+
@success
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if the extraction failed
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean] true if failed, false otherwise
|
|
36
|
+
def failure?
|
|
37
|
+
!@success
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Add an error message to the result
|
|
41
|
+
#
|
|
42
|
+
# This creates a new ExtractionResult with the error added, as the object
|
|
43
|
+
# is immutable.
|
|
44
|
+
#
|
|
45
|
+
# @param message [String] The error message to add
|
|
46
|
+
# @return [ExtractionResult] A new result object with the error added
|
|
47
|
+
def add_error(message)
|
|
48
|
+
self.class.new(
|
|
49
|
+
output_files: @output_files.dup,
|
|
50
|
+
success: false,
|
|
51
|
+
errors: @errors.dup << message,
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|