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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -4
  3. data/.rubocop.yml +7 -9
  4. data/.rubocop_todo.yml +135 -0
  5. data/Gemfile +6 -6
  6. data/README.adoc +856 -55
  7. data/Rakefile +7 -101
  8. data/exe/extract_ttc +7 -0
  9. data/extract_ttc.gemspec +3 -4
  10. data/lib/extract_ttc/cli.rb +47 -0
  11. data/lib/extract_ttc/commands/extract.rb +88 -0
  12. data/lib/extract_ttc/commands/info.rb +112 -0
  13. data/lib/extract_ttc/commands/list.rb +60 -0
  14. data/lib/extract_ttc/configuration.rb +126 -0
  15. data/lib/extract_ttc/constants.rb +42 -0
  16. data/lib/extract_ttc/models/extraction_result.rb +56 -0
  17. data/lib/extract_ttc/models/validation_result.rb +53 -0
  18. data/lib/extract_ttc/true_type_collection.rb +79 -0
  19. data/lib/extract_ttc/true_type_font.rb +239 -0
  20. data/lib/extract_ttc/utilities/checksum_calculator.rb +89 -0
  21. data/lib/extract_ttc/utilities/output_path_generator.rb +100 -0
  22. data/lib/extract_ttc/version.rb +1 -1
  23. data/lib/extract_ttc.rb +83 -55
  24. data/sig/extract_ttc/configuration.rbs +19 -0
  25. data/sig/extract_ttc/constants.rbs +17 -0
  26. data/sig/extract_ttc/models/extraction_result.rbs +19 -0
  27. data/sig/extract_ttc/models/font_data.rbs +17 -0
  28. data/sig/extract_ttc/models/table_directory_entry.rbs +15 -0
  29. data/sig/extract_ttc/models/true_type_collection_header.rbs +15 -0
  30. data/sig/extract_ttc/models/true_type_font_offset_table.rbs +17 -0
  31. data/sig/extract_ttc/models/validation_result.rbs +17 -0
  32. data/sig/extract_ttc/utilities/checksum_calculator.rbs +13 -0
  33. data/sig/extract_ttc/utilities/output_path_generator.rbs +11 -0
  34. data/sig/extract_ttc/validators/true_type_collection_validator.rbs +9 -0
  35. data/sig/extract_ttc.rbs +20 -0
  36. metadata +44 -28
  37. data/ext/stripttc/LICENSE +0 -31
  38. data/ext/stripttc/dummy.c +0 -2
  39. data/ext/stripttc/extconf.rb +0 -5
  40. data/ext/stripttc/stripttc.c +0 -187
data/Rakefile CHANGED
@@ -1,106 +1,12 @@
1
- require "bundler/gem_tasks"
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
- ext_thru_rc_dock = %w[
32
- x86_64-linux
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
- desc "Build the native gem for #{plat}"
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
- RakeCompilerDock.sh <<~RCD, platform: plat
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
- ext_thru_musl_cc.each do |plat|
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
- task = Gem::PackageTask.new(s)
103
- task.define
104
- end
105
- end
106
- end
12
+ task default: %i[spec rubocop]
data/exe/extract_ttc ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/extract_ttc"
5
+ require_relative "../lib/extract_ttc/cli"
6
+
7
+ ExtractTtc::Cli.start(ARGV)
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.add_runtime_dependency "bundler", "~> 2.3", ">= 2.3.22"
29
- spec.add_runtime_dependency "ffi", "~> 1.0"
30
- spec.add_runtime_dependency "rake", "~> 13"
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