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/lib/extract_ttc.rb CHANGED
@@ -1,6 +1,28 @@
1
- require "extract_ttc/version"
2
- require "ffi"
3
- require "tempfile"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "extract_ttc/version"
4
+ require_relative "extract_ttc/constants"
5
+ require_relative "extract_ttc/configuration"
6
+
7
+ # Domain objects (binary data objects)
8
+ require_relative "extract_ttc/true_type_collection"
9
+ require_relative "extract_ttc/true_type_font"
10
+
11
+ # Model classes (value objects)
12
+ require_relative "extract_ttc/models/extraction_result"
13
+ require_relative "extract_ttc/models/validation_result"
14
+
15
+ # Utility classes
16
+ require_relative "extract_ttc/utilities/checksum_calculator"
17
+ require_relative "extract_ttc/utilities/output_path_generator"
18
+
19
+ # Command classes
20
+ require_relative "extract_ttc/commands/extract"
21
+ require_relative "extract_ttc/commands/list"
22
+ require_relative "extract_ttc/commands/info"
23
+
24
+ # Conditionally load CLI if Thor is available
25
+ require_relative "extract_ttc/cli" if defined?(Thor)
4
26
 
5
27
  module ExtractTtc
6
28
  class Error < StandardError; end
@@ -13,67 +35,73 @@ module ExtractTtc
13
35
 
14
36
  class UnknownResultError < Error; end
15
37
 
16
- extend FFI::Library
17
- # NOTE: ffi doesn't support bundles out of box https://github.com/ffi/ffi/issues/42#issuecomment-750031554
18
- # NOTE: rake-compiler doesn't support dylib generation https://github.com/rake-compiler/rake-compiler/issues/183
19
- macos_binary = "stripttc.bundle"
20
- lib_name = if File.exist?(File.join(File.dirname(__FILE__), macos_binary))
21
- macos_binary
22
- else
23
- "stripttc.so"
24
- end
25
- ffi_lib File.join(File.dirname(__FILE__), lib_name)
26
- attach_function :handlefile, [:string], :int
27
-
28
- def self.extract(path)
29
- stdout, stderr, code = capture3 do
30
- handlefile(path)
38
+ # Extract all fonts from a TTC file
39
+ #
40
+ # This is the main public API for the gem. It extracts all fonts from
41
+ # a TrueType Collection (TTC) file and writes them as separate TTF files.
42
+ #
43
+ # Uses the object-oriented architecture where domain objects
44
+ # (TrueTypeCollection, TrueTypeFont) encapsulate their own persistence logic.
45
+ #
46
+ # @param path [String] Path to the input TTC file
47
+ # @param output_dir [String, nil] Optional output directory
48
+ # @param config [Configuration, nil] Optional configuration object
49
+ # @return [Array<String>] Array of output file paths
50
+ #
51
+ # @raise [ReadFileError] If the input file cannot be read
52
+ # @raise [InvalidFileError] If the file is not a valid TTC file
53
+ # @raise [WriteFileError] If output files cannot be written
54
+ #
55
+ # @example Extract fonts to current directory
56
+ # ExtractTtc.extract("Helvetica.ttc")
57
+ # # => ["Helvetica_00.ttf", "Helvetica_01.ttf", ...]
58
+ #
59
+ # @example Extract fonts to specific directory
60
+ # ExtractTtc.extract("Helvetica.ttc", output_dir: "/tmp/fonts")
61
+ def self.extract(path, output_dir: nil, config: nil)
62
+ ensure_output_directory_exists(output_dir)
63
+
64
+ File.open(path, "rb") do |file|
65
+ ttc = TrueTypeCollection.read(file)
66
+ fonts = ttc.extract_fonts(file)
67
+
68
+ fonts.map.with_index do |font, index|
69
+ output_path = Utilities::OutputPathGenerator.generate(
70
+ path, index, output_dir: output_dir || config&.output_directory
71
+ )
72
+ font.to_file(output_path)
73
+ output_path
74
+ end
31
75
  end
32
-
33
- return handle_error(code, stderr) unless code.zero?
34
-
35
- fetch_filenames(stdout)
76
+ rescue Errno::ENOENT
77
+ raise ReadFileError, "Could not open file: #{path}"
78
+ rescue Errno::EACCES => e
79
+ raise WriteFileError, "Failed to open output file: #{e.message}"
80
+ rescue IOError, RuntimeError, StandardError => e
81
+ raise invalid_file?(e) ? invalid_file_error : write_file_error(e)
36
82
  end
37
83
 
38
- def self.handle_error(code, stderr)
39
- case code
40
- when -1
41
- raise ReadFileError, stderr
42
- when -2
43
- raise InvalidFileError, stderr
44
- when -3
45
- raise WriteFileError, stderr
46
- else
47
- raise UnknownResultError, "Return code: #{code}"
48
- end
84
+ # Check if error indicates invalid/corrupted TTC file
85
+ def self.invalid_file?(error)
86
+ error.message.match?(/end of file|expected|Invalid TTC/i)
49
87
  end
50
88
 
51
- def self.capture3
52
- stderr = status = nil
53
-
54
- stdout = capture_stream($stdout) do
55
- stderr = capture_stream($stderr) do
56
- status = yield
57
- end
58
- end
89
+ def self.invalid_file_error
90
+ InvalidFileError.new("File does not look like a ttc file")
91
+ end
59
92
 
60
- [stdout, stderr, status]
93
+ def self.write_file_error(error)
94
+ WriteFileError.new("Failed to open output file: #{error.message}")
61
95
  end
62
96
 
63
- def self.capture_stream(stream_io)
64
- origin_stream = stream_io.dup
97
+ def self.ensure_output_directory_exists(output_dir)
98
+ return unless output_dir
99
+ return if File.directory?(output_dir)
65
100
 
66
- Tempfile.open("captured_stream") do |captured_stream|
67
- stream_io.reopen(captured_stream)
68
- yield
69
- captured_stream.rewind
70
- return captured_stream.read
71
- end
72
- ensure
73
- stream_io.reopen(origin_stream)
101
+ require "fileutils"
102
+ FileUtils.mkdir_p(output_dir)
74
103
  end
75
104
 
76
- def self.fetch_filenames(stdout)
77
- stdout.split("=>").last.split
78
- end
105
+ private_class_method :invalid_file?, :invalid_file_error,
106
+ :write_file_error, :ensure_output_directory_exists
79
107
  end
@@ -0,0 +1,19 @@
1
+ module ExtractTtc
2
+ class Configuration
3
+ attr_accessor output_directory: String
4
+
5
+ attr_accessor overwrite_existing: bool
6
+
7
+ attr_accessor validate_checksums: bool
8
+
9
+ attr_accessor verbose: bool
10
+
11
+ def initialize: (?output_directory: String, ?overwrite_existing: bool, ?validate_checksums: bool, ?verbose: bool) -> void
12
+
13
+ def self.default: () -> Configuration
14
+
15
+ def merge: (Hash[Symbol, untyped] options) -> Configuration
16
+
17
+ def to_h: () -> Hash[Symbol, String | bool]
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module ExtractTtc
2
+ module Constants
3
+ TTC_TAG: String
4
+
5
+ TTC_VERSION_1: Integer
6
+
7
+ TTC_VERSION_2: Integer
8
+
9
+ HEAD_TAG: String
10
+
11
+ CHECKSUM_ADJUSTMENT_MAGIC: Integer
12
+
13
+ SUPPORTED_VERSIONS: Array[Integer]
14
+
15
+ TABLE_ALIGNMENT: Integer
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module ExtractTtc
2
+ module Models
3
+ class ExtractionResult
4
+ attr_reader output_files: Array[String]
5
+
6
+ attr_reader success: bool
7
+
8
+ attr_reader errors: Array[String]
9
+
10
+ def initialize: (?output_files: Array[String], ?success: bool, ?errors: Array[String]) -> void
11
+
12
+ def success?: () -> bool
13
+
14
+ def failure?: () -> bool
15
+
16
+ def add_error: (String message) -> ExtractionResult
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module ExtractTtc
2
+ module Models
3
+ class FontData
4
+ attr_reader offset_table: TrueTypeFontOffsetTable
5
+
6
+ attr_reader table_directory: Array[TableDirectoryEntry]
7
+
8
+ attr_reader table_data: Hash[String, String]
9
+
10
+ def initialize: (offset_table: TrueTypeFontOffsetTable, table_directory: Array[TableDirectoryEntry], table_data: Hash[String, String]) -> void
11
+
12
+ def head_table_entry: () -> TableDirectoryEntry?
13
+
14
+ def table_by_tag: (String tag) -> TableDirectoryEntry?
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module ExtractTtc
2
+ module Models
3
+ class TableDirectoryEntry
4
+ attr_reader tag: String
5
+
6
+ attr_reader checksum: Integer
7
+
8
+ attr_reader offset: Integer
9
+
10
+ attr_reader length: Integer
11
+
12
+ def initialize: (tag: String, checksum: Integer, offset: Integer, length: Integer) -> void
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module ExtractTtc
2
+ module Models
3
+ class TrueTypeCollectionHeader
4
+ attr_reader tag: String
5
+
6
+ attr_reader version: Integer
7
+
8
+ attr_reader num_fonts: Integer
9
+
10
+ attr_reader font_offsets: Array[Integer]
11
+
12
+ def initialize: (tag: String, version: Integer, num_fonts: Integer, font_offsets: Array[Integer]) -> void
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module ExtractTtc
2
+ module Models
3
+ class TrueTypeFontOffsetTable
4
+ attr_reader sfnt_version: Integer
5
+
6
+ attr_reader num_tables: Integer
7
+
8
+ attr_reader search_range: Integer
9
+
10
+ attr_reader entry_selector: Integer
11
+
12
+ attr_reader range_shift: Integer
13
+
14
+ def initialize: (sfnt_version: Integer, num_tables: Integer, search_range: Integer, entry_selector: Integer, range_shift: Integer) -> void
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module ExtractTtc
2
+ module Models
3
+ class ValidationResult
4
+ attr_reader valid: bool
5
+
6
+ attr_reader errors: Array[String]
7
+
8
+ def initialize: (?valid: bool, ?errors: Array[String]) -> void
9
+
10
+ def valid?: () -> bool
11
+
12
+ def invalid?: () -> bool
13
+
14
+ def add_error: (String message) -> ValidationResult
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module ExtractTtc
2
+ module Utilities
3
+ class ChecksumCalculator
4
+ def self.calculate_file_checksum: (String file_path) -> Integer
5
+
6
+ def self.calculate_adjustment: (Integer file_checksum) -> Integer
7
+
8
+ private
9
+
10
+ def self.calculate_checksum_from_io: (IO io) -> Integer
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module ExtractTtc
2
+ module Utilities
3
+ class OutputPathGenerator
4
+ DEFAULT_INDEX_FORMAT: String
5
+
6
+ def self.generate: (String input_path, Integer font_index, ?output_dir: String?) -> String
7
+
8
+ def self.generate_with_format: (String input_path, Integer font_index, String index_format, ?output_dir: String?) -> String
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module ExtractTtc
2
+ module Validators
3
+ class TrueTypeCollectionValidator
4
+ def self.validate_header: (Models::TrueTypeCollectionHeader ttc_header) -> Models::ValidationResult
5
+
6
+ def self.validate_font_data: (Models::FontData font_data) -> Models::ValidationResult
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ module ExtractTtc
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ReadFileError < Error
8
+ end
9
+
10
+ class InvalidFileError < Error
11
+ end
12
+
13
+ class WriteFileError < Error
14
+ end
15
+
16
+ class UnknownResultError < Error
17
+ end
18
+
19
+ def self.extract: (String path, ?output_dir: String?, ?config: Configuration?) -> Array[String]
20
+ end
metadata CHANGED
@@ -1,85 +1,101 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extract_ttc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.6
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-27 00:00:00.000000000 Z
11
+ date: 2025-11-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: bundler
14
+ name: bindata
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.3'
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 2.3.22
19
+ version: '2.5'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: '2.3'
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 2.3.22
26
+ version: '2.5'
33
27
  - !ruby/object:Gem::Dependency
34
- name: ffi
28
+ name: paint
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
37
31
  - - "~>"
38
32
  - !ruby/object:Gem::Version
39
- version: '1.0'
33
+ version: '2.0'
40
34
  type: :runtime
41
35
  prerelease: false
42
36
  version_requirements: !ruby/object:Gem::Requirement
43
37
  requirements:
44
38
  - - "~>"
45
39
  - !ruby/object:Gem::Version
46
- version: '1.0'
40
+ version: '2.0'
47
41
  - !ruby/object:Gem::Dependency
48
- name: rake
42
+ name: thor
49
43
  requirement: !ruby/object:Gem::Requirement
50
44
  requirements:
51
45
  - - "~>"
52
46
  - !ruby/object:Gem::Version
53
- version: '13'
47
+ version: '1.4'
54
48
  type: :runtime
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
51
  requirements:
58
52
  - - "~>"
59
53
  - !ruby/object:Gem::Version
60
- version: '13'
54
+ version: '1.4'
61
55
  description: Extract font collection to separate font files
62
56
  email:
63
57
  - operations@ribose.com
64
- executables: []
65
- extensions:
66
- - ext/stripttc/extconf.rb
58
+ executables:
59
+ - extract_ttc
60
+ extensions: []
67
61
  extra_rdoc_files: []
68
62
  files:
69
63
  - ".gitignore"
70
64
  - ".rspec"
71
65
  - ".rubocop.yml"
66
+ - ".rubocop_todo.yml"
72
67
  - Gemfile
73
68
  - LICENSE.adoc
74
69
  - README.adoc
75
70
  - Rakefile
76
- - ext/stripttc/LICENSE
77
- - ext/stripttc/dummy.c
78
- - ext/stripttc/extconf.rb
79
- - ext/stripttc/stripttc.c
71
+ - exe/extract_ttc
80
72
  - extract_ttc.gemspec
81
73
  - lib/extract_ttc.rb
74
+ - lib/extract_ttc/cli.rb
75
+ - lib/extract_ttc/commands/extract.rb
76
+ - lib/extract_ttc/commands/info.rb
77
+ - lib/extract_ttc/commands/list.rb
78
+ - lib/extract_ttc/configuration.rb
79
+ - lib/extract_ttc/constants.rb
80
+ - lib/extract_ttc/models/extraction_result.rb
81
+ - lib/extract_ttc/models/validation_result.rb
82
+ - lib/extract_ttc/true_type_collection.rb
83
+ - lib/extract_ttc/true_type_font.rb
84
+ - lib/extract_ttc/utilities/checksum_calculator.rb
85
+ - lib/extract_ttc/utilities/output_path_generator.rb
82
86
  - lib/extract_ttc/version.rb
87
+ - sig/extract_ttc.rbs
88
+ - sig/extract_ttc/configuration.rbs
89
+ - sig/extract_ttc/constants.rbs
90
+ - sig/extract_ttc/models/extraction_result.rbs
91
+ - sig/extract_ttc/models/font_data.rbs
92
+ - sig/extract_ttc/models/table_directory_entry.rbs
93
+ - sig/extract_ttc/models/true_type_collection_header.rbs
94
+ - sig/extract_ttc/models/true_type_font_offset_table.rbs
95
+ - sig/extract_ttc/models/validation_result.rbs
96
+ - sig/extract_ttc/utilities/checksum_calculator.rbs
97
+ - sig/extract_ttc/utilities/output_path_generator.rbs
98
+ - sig/extract_ttc/validators/true_type_collection_validator.rbs
83
99
  homepage: https://github.com/fontist/extract_ttc
84
100
  licenses:
85
101
  - BSD-3-Clause
@@ -88,7 +104,7 @@ metadata:
88
104
  source_code_uri: https://github.com/fontist/extract_ttc
89
105
  changelog_uri: https://github.com/fontist/extract_ttc
90
106
  rubygems_mfa_required: 'false'
91
- post_install_message:
107
+ post_install_message:
92
108
  rdoc_options: []
93
109
  require_paths:
94
110
  - lib
@@ -103,8 +119,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
119
  - !ruby/object:Gem::Version
104
120
  version: '0'
105
121
  requirements: []
106
- rubygems_version: 3.3.26
107
- signing_key:
122
+ rubygems_version: 3.5.22
123
+ signing_key:
108
124
  specification_version: 4
109
125
  summary: Extract TTC file to TTF files
110
126
  test_files: []
data/ext/stripttc/LICENSE DELETED
@@ -1,31 +0,0 @@
1
- BSD 3-Clause License for stripttc.c
2
-
3
- Copyright (c) George Williams and FontForge authors. All rights reserved.
4
- https://github.com/fontforge/fontforge/blob/master/AUTHORS
5
-
6
- Redistribution and use in source and binary forms, with or without
7
- modification, are permitted provided that the following conditions
8
- are met:
9
-
10
- Redistributions of source code must retain the above copyright
11
- notice, this list of conditions and the following disclaimer.
12
-
13
- Redistributions in binary form must reproduce the above copyright
14
- notice, this list of conditions and the following disclaimer in the
15
- documentation and/or other materials provided with the distribution.
16
-
17
- The name of the author may not be used to endorse or promote
18
- products derived from this software without specific prior written
19
- permission.
20
-
21
- THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS
22
- OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24
- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
25
- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
27
- GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
29
- IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
30
- OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
31
- IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/ext/stripttc/dummy.c DELETED
@@ -1,2 +0,0 @@
1
- // to keep linker happy
2
- void Init_stripttc() {}
@@ -1,5 +0,0 @@
1
- require "rbconfig"
2
- require "mkmf"
3
- create_makefile "stripttc"
4
- m = File.read("Makefile").gsub("--no-as-needed", "--as-needed")
5
- File.write("Makefile", m)