fontisan 0.1.0

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/.rubocop_todo.yml +217 -0
  5. data/Gemfile +15 -0
  6. data/LICENSE +24 -0
  7. data/README.adoc +984 -0
  8. data/Rakefile +95 -0
  9. data/exe/fontisan +7 -0
  10. data/fontisan.gemspec +44 -0
  11. data/lib/fontisan/binary/base_record.rb +57 -0
  12. data/lib/fontisan/binary/structures.rb +84 -0
  13. data/lib/fontisan/cli.rb +192 -0
  14. data/lib/fontisan/commands/base_command.rb +82 -0
  15. data/lib/fontisan/commands/dump_table_command.rb +71 -0
  16. data/lib/fontisan/commands/features_command.rb +94 -0
  17. data/lib/fontisan/commands/glyphs_command.rb +50 -0
  18. data/lib/fontisan/commands/info_command.rb +120 -0
  19. data/lib/fontisan/commands/optical_size_command.rb +41 -0
  20. data/lib/fontisan/commands/scripts_command.rb +59 -0
  21. data/lib/fontisan/commands/tables_command.rb +52 -0
  22. data/lib/fontisan/commands/unicode_command.rb +76 -0
  23. data/lib/fontisan/commands/variable_command.rb +61 -0
  24. data/lib/fontisan/config/features.yml +143 -0
  25. data/lib/fontisan/config/scripts.yml +42 -0
  26. data/lib/fontisan/constants.rb +78 -0
  27. data/lib/fontisan/error.rb +15 -0
  28. data/lib/fontisan/font_loader.rb +109 -0
  29. data/lib/fontisan/formatters/text_formatter.rb +314 -0
  30. data/lib/fontisan/models/all_scripts_features_info.rb +21 -0
  31. data/lib/fontisan/models/features_info.rb +42 -0
  32. data/lib/fontisan/models/font_info.rb +99 -0
  33. data/lib/fontisan/models/glyph_info.rb +26 -0
  34. data/lib/fontisan/models/optical_size_info.rb +33 -0
  35. data/lib/fontisan/models/scripts_info.rb +39 -0
  36. data/lib/fontisan/models/table_info.rb +55 -0
  37. data/lib/fontisan/models/unicode_mappings.rb +42 -0
  38. data/lib/fontisan/models/variable_font_info.rb +82 -0
  39. data/lib/fontisan/open_type_collection.rb +97 -0
  40. data/lib/fontisan/open_type_font.rb +292 -0
  41. data/lib/fontisan/parsers/tag.rb +77 -0
  42. data/lib/fontisan/tables/cmap.rb +284 -0
  43. data/lib/fontisan/tables/fvar.rb +157 -0
  44. data/lib/fontisan/tables/gpos.rb +111 -0
  45. data/lib/fontisan/tables/gsub.rb +111 -0
  46. data/lib/fontisan/tables/head.rb +114 -0
  47. data/lib/fontisan/tables/layout_common.rb +73 -0
  48. data/lib/fontisan/tables/name.rb +188 -0
  49. data/lib/fontisan/tables/os2.rb +175 -0
  50. data/lib/fontisan/tables/post.rb +148 -0
  51. data/lib/fontisan/true_type_collection.rb +98 -0
  52. data/lib/fontisan/true_type_font.rb +313 -0
  53. data/lib/fontisan/utilities/checksum_calculator.rb +89 -0
  54. data/lib/fontisan/version.rb +5 -0
  55. data/lib/fontisan.rb +80 -0
  56. metadata +150 -0
data/Rakefile ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "fileutils"
6
+
7
+ require "rubocop/rake_task"
8
+
9
+ RuboCop::RakeTask.new
10
+
11
+ namespace :fixtures do
12
+ fixtures_dir = "spec/fixtures/fonts"
13
+
14
+ # Helper method to download and extract a font archive
15
+ def download_font(name, url, target_dir)
16
+ require "open-uri"
17
+ require "zip"
18
+
19
+ zip_file = "#{target_dir}/#{name}.zip"
20
+
21
+ puts "[fixtures:download] Downloading #{name}..."
22
+ FileUtils.mkdir_p(target_dir)
23
+
24
+ URI.open(url) do |remote|
25
+ File.binwrite(zip_file, remote.read)
26
+ end
27
+
28
+ puts "[fixtures:download] Extracting #{name}..."
29
+ Zip::File.open(zip_file) do |zip|
30
+ zip.each do |entry|
31
+ dest_path = File.join(target_dir, entry.name)
32
+ FileUtils.mkdir_p(File.dirname(dest_path))
33
+ entry.extract(dest_path) unless File.exist?(dest_path)
34
+ end
35
+ end
36
+
37
+ FileUtils.rm(zip_file)
38
+ puts "[fixtures:download] #{name} downloaded successfully"
39
+ rescue LoadError => e
40
+ warn "[fixtures:download] Error: Required gem not installed. Please run: gem install rubyzip"
41
+ raise e
42
+ end
43
+
44
+ # Font configurations with target directories and marker files
45
+ # All fonts are downloaded via Rake
46
+ fonts = {
47
+ "Libertinus" => {
48
+ url: "https://github.com/alerque/libertinus/releases/download/v7.051/Libertinus-7.051.zip",
49
+ target_dir: "#{fixtures_dir}/libertinus",
50
+ marker: "#{fixtures_dir}/libertinus/Libertinus-7.051/static/OTF/LibertinusSerif-Regular.otf",
51
+ },
52
+ "MonaSans" => {
53
+ url: "https://github.com/github/mona-sans/releases/download/v2.0/MonaSans.zip",
54
+ target_dir: "#{fixtures_dir}/MonaSans",
55
+ marker: "#{fixtures_dir}/MonaSans/MonaSans/variable/MonaSans[wdth,wght].ttf",
56
+ },
57
+ "NotoSerifCJK" => {
58
+ url: "https://github.com/notofonts/noto-cjk/releases/download/Serif2.003/01_NotoSerifCJK.ttc.zip",
59
+ target_dir: "#{fixtures_dir}/NotoSerifCJK",
60
+ marker: "#{fixtures_dir}/NotoSerifCJK/NotoSerifCJK.ttc",
61
+ },
62
+ "NotoSerifCJK-VF" => {
63
+ url: "https://github.com/notofonts/noto-cjk/releases/download/Serif2.003/02_NotoSerifCJK-OTF-VF.zip",
64
+ target_dir: "#{fixtures_dir}/NotoSerifCJK-VF",
65
+ marker: "#{fixtures_dir}/NotoSerifCJK-VF/Variable/OTC/NotoSerifCJK-VF.otf.ttc",
66
+ },
67
+ }
68
+
69
+ # Create file tasks for each font
70
+ fonts.each do |name, config|
71
+ file config[:marker] do
72
+ download_font(name, config[:url], config[:target_dir])
73
+ end
74
+ end
75
+
76
+ desc "Download all test fixture fonts"
77
+ task download: fonts.values.map { |config| config[:marker] }
78
+
79
+ desc "Clean downloaded fixtures"
80
+ task :clean do
81
+ %w[libertinus MonaSans NotoSerifCJK NotoSerifCJK-VF].each do |dir|
82
+ path = File.join(fixtures_dir, dir)
83
+ if File.exist?(path)
84
+ FileUtils.rm_rf(path)
85
+ puts "[fixtures:clean] Removed #{path}"
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ # RSpec task depends on fixtures
92
+ RSpec::Core::RakeTask.new(spec: "fixtures:download")
93
+
94
+ # Default task runs spec and rubocop
95
+ task default: %i[spec rubocop]
data/exe/fontisan ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/fontisan"
5
+ require_relative "../lib/fontisan/cli"
6
+
7
+ Fontisan::Cli.start(ARGV)
data/fontisan.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/fontisan/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "fontisan"
7
+ spec.version = Fontisan::VERSION
8
+ spec.authors = ["Ribose Inc."]
9
+ spec.email = ["open.source@ribose.com"]
10
+
11
+ spec.summary = "Font analysis tools and utilities for OpenType fonts"
12
+ spec.description = <<~HEREDOC
13
+ Fontisan provides font analysis tools and utilities. It is
14
+ designed as a pure Ruby implementation with full object-oriented architecture,
15
+ supporting extraction of information from OpenType and TrueType fonts (OTF, TTF, OTC, TTC).
16
+
17
+ The gem provides both a Ruby library API and a command-line interface,
18
+ with structured output formats (YAML, JSON, text).
19
+ HEREDOC
20
+
21
+ spec.homepage = "https://github.com/fontist/fontisan"
22
+ spec.license = "BSD-2-Clause"
23
+ spec.required_ruby_version = ">= 3.0.0"
24
+
25
+ spec.metadata["homepage_uri"] = spec.homepage
26
+ spec.metadata["source_code_uri"] = "https://github.com/fontist/fontisan"
27
+ spec.metadata["changelog_uri"] = "https://github.com/fontist/fontisan/blob/main/CHANGELOG.md"
28
+ spec.metadata["rubygems_mfa_required"] = "true"
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(__dir__) do
33
+ `git ls-files -z`.split("\x0").reject do |f|
34
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
35
+ end
36
+ end
37
+ spec.bindir = "exe"
38
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
39
+ spec.require_paths = ["lib"]
40
+
41
+ spec.add_dependency "bindata", "~> 2.5"
42
+ spec.add_dependency "lutaml-model", "~> 0.7"
43
+ spec.add_dependency "thor", "~> 1.4"
44
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+
5
+ module Fontisan
6
+ module Binary
7
+ # Base class for all BinData record definitions
8
+ #
9
+ # Provides common configuration for OpenType binary structures:
10
+ # - Big-endian byte order (OpenType standard)
11
+ # - Common helper methods
12
+ #
13
+ # All table parsers and binary structures should inherit from this class
14
+ # to ensure consistent behavior across the codebase.
15
+ #
16
+ # @example Defining a simple structure
17
+ # class MyTable < Binary::BaseRecord
18
+ # uint16 :version
19
+ # uint16 :count
20
+ # end
21
+ class BaseRecord < BinData::Record
22
+ endian :big # OpenType uses big-endian byte order
23
+
24
+ # Override read to handle nil data gracefully
25
+ def self.read(io)
26
+ return new if io.nil? || (io.respond_to?(:empty?) && io.empty?)
27
+
28
+ super
29
+ end
30
+
31
+ # Check if the record is valid
32
+ #
33
+ # @return [Boolean] True if valid, false otherwise
34
+ def valid?
35
+ true
36
+ end
37
+
38
+ private
39
+
40
+ # Convert 16.16 fixed-point integer to float
41
+ #
42
+ # @param value [Integer] Fixed-point value
43
+ # @return [Float] Floating-point value
44
+ def fixed_to_float(value)
45
+ # Treat as unsigned for the conversion
46
+ unsigned = value & 0xFFFFFFFF
47
+ integer_part = (unsigned >> 16) & 0xFFFF
48
+ fractional_part = unsigned & 0xFFFF
49
+
50
+ # Handle sign for the integer part
51
+ integer_part -= 0x10000 if integer_part >= 0x8000
52
+
53
+ integer_part + (fractional_part / 65_536.0)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_record"
4
+
5
+ module Fontisan
6
+ module Binary
7
+ # OpenType Offset Table (Font Header)
8
+ #
9
+ # This structure appears at the beginning of every OpenType font file.
10
+ # It contains metadata about the table directory.
11
+ #
12
+ # Structure:
13
+ # - uint32: sfnt_version (0x00010000 for TrueType, 'OTTO' for CFF)
14
+ # - uint16: num_tables (number of tables in font)
15
+ # - uint16: search_range (maximum power of 2 <= num_tables) * 16
16
+ # - uint16: entry_selector (log2 of maximum power of 2 <= num_tables)
17
+ # - uint16: range_shift (num_tables * 16 - search_range)
18
+ class OffsetTable < BaseRecord
19
+ uint32 :sfnt_version
20
+ uint16 :num_tables
21
+ uint16 :search_range
22
+ uint16 :entry_selector
23
+ uint16 :range_shift
24
+
25
+ # Check if this is a TrueType font (version 0x00010000 or 'true')
26
+ #
27
+ # @return [Boolean] True if TrueType font
28
+ def truetype?
29
+ [0x00010000, 0x74727565].include?(sfnt_version) # 'true'
30
+ end
31
+
32
+ # Check if this is an OpenType/CFF font (version 'OTTO')
33
+ #
34
+ # @return [Boolean] True if CFF font
35
+ def cff?
36
+ sfnt_version == 0x4F54544F # 'OTTO'
37
+ end
38
+
39
+ # Get sfnt version as a tag string
40
+ #
41
+ # @return [String] Version tag ('OTTO' or version number)
42
+ def version_tag
43
+ if cff?
44
+ "OTTO"
45
+ elsif truetype?
46
+ "TrueType"
47
+ else
48
+ format("0x%08X", sfnt_version)
49
+ end
50
+ end
51
+ end
52
+
53
+ # OpenType Table Directory Entry
54
+ #
55
+ # Each entry describes one table in the font file.
56
+ #
57
+ # Structure:
58
+ # - char[4]: tag (table identifier)
59
+ # - uint32: checksum (checksum for this table)
60
+ # - uint32: offset (byte offset from beginning of font file)
61
+ # - uint32: table_length (length of table in bytes)
62
+ class TableDirectoryEntry < BaseRecord
63
+ string :tag, length: 4
64
+ uint32 :checksum
65
+ uint32 :offset
66
+ uint32 :table_length
67
+
68
+ # Convert tag to Tag object for comparison
69
+ #
70
+ # @return [Parsers::Tag] Tag object
71
+ def tag_object
72
+ Parsers::Tag.new(tag)
73
+ end
74
+
75
+ # Check if this entry has a specific tag
76
+ #
77
+ # @param other_tag [String, Parsers::Tag] Tag to compare
78
+ # @return [Boolean] True if tags match
79
+ def tag?(other_tag)
80
+ tag_object == (other_tag.is_a?(Parsers::Tag) ? other_tag : Parsers::Tag.new(other_tag))
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Fontisan
6
+ # Command-line interface for Fontisan.
7
+ #
8
+ # This class provides the Thor-based CLI with commands for extracting
9
+ # font information and listing tables. It supports multiple output formats
10
+ # (text, YAML, JSON) and various options for controlling output behavior.
11
+ #
12
+ # @example Run the info command
13
+ # Fontisan::Cli.start(['info', 'font.ttf'])
14
+ class Cli < Thor
15
+ class_option :format, type: :string, default: "text",
16
+ desc: "Output format (text, yaml, json)",
17
+ aliases: "-f"
18
+ class_option :font_index, type: :numeric, default: 0,
19
+ desc: "Font index for TTC files",
20
+ aliases: "-i"
21
+ class_option :verbose, type: :boolean, default: false,
22
+ desc: "Enable verbose output",
23
+ aliases: "-v"
24
+ class_option :quiet, type: :boolean, default: false,
25
+ desc: "Suppress non-error output",
26
+ aliases: "-q"
27
+
28
+ desc "info FONT_FILE", "Display font information"
29
+ # Extract and display comprehensive font metadata.
30
+ #
31
+ # @param font_file [String] Path to the font file
32
+ def info(font_file)
33
+ command = Commands::InfoCommand.new(font_file, options)
34
+ result = command.run
35
+ output_result(result)
36
+ rescue Errno::ENOENT, Error => e
37
+ handle_error(e)
38
+ end
39
+
40
+ desc "tables FONT_FILE", "List OpenType tables"
41
+ # List all OpenType tables in the font file.
42
+ #
43
+ # @param font_file [String] Path to the font file
44
+ def tables(font_file)
45
+ command = Commands::TablesCommand.new(font_file, options)
46
+ result = command.run
47
+ output_result(result)
48
+ rescue Errno::ENOENT, Error => e
49
+ handle_error(e)
50
+ end
51
+
52
+ desc "glyphs FONT_FILE", "List glyph names"
53
+ # List glyph names from the font file.
54
+ #
55
+ # @param font_file [String] Path to the font file
56
+ def glyphs(font_file)
57
+ command = Commands::GlyphsCommand.new(font_file, options)
58
+ result = command.run
59
+ output_result(result)
60
+ rescue Errno::ENOENT, Error => e
61
+ handle_error(e)
62
+ end
63
+
64
+ desc "unicode FONT_FILE", "List Unicode to glyph mappings"
65
+ # List Unicode to glyph index mappings from the font file.
66
+ #
67
+ # @param font_file [String] Path to the font file
68
+ def unicode(font_file)
69
+ command = Commands::UnicodeCommand.new(font_file, options)
70
+ result = command.run
71
+ output_result(result)
72
+ rescue Errno::ENOENT, Error => e
73
+ handle_error(e)
74
+ end
75
+
76
+ desc "variable FONT_FILE", "Display variable font information"
77
+ # Display variable font variation axes and instances.
78
+ #
79
+ # @param font_file [String] Path to the font file
80
+ def variable(font_file)
81
+ command = Commands::VariableCommand.new(font_file, options)
82
+ result = command.run
83
+ output_result(result)
84
+ rescue Errno::ENOENT, Error => e
85
+ handle_error(e)
86
+ end
87
+
88
+ desc "optical-size FONT_FILE", "Display optical size information"
89
+ # Display optical size information from the font file.
90
+ #
91
+ # @param font_file [String] Path to the font file
92
+ def optical_size(font_file)
93
+ command = Commands::OpticalSizeCommand.new(font_file, options)
94
+ result = command.run
95
+ output_result(result)
96
+ rescue Errno::ENOENT, Error => e
97
+ handle_error(e)
98
+ end
99
+
100
+ desc "scripts FONT_FILE", "List supported scripts from GSUB/GPOS tables"
101
+ # List all scripts supported by the font from GSUB and GPOS tables.
102
+ #
103
+ # @param font_file [String] Path to the font file
104
+ def scripts(font_file)
105
+ command = Commands::ScriptsCommand.new(font_file, options)
106
+ result = command.run
107
+ output_result(result)
108
+ rescue Errno::ENOENT, Error => e
109
+ handle_error(e)
110
+ end
111
+
112
+ desc "features FONT_FILE", "List GSUB/GPOS features"
113
+ option :script, type: :string,
114
+ desc: "Script tag to query (e.g., latn, cyrl, arab). If not specified, shows features for all scripts",
115
+ aliases: "-s"
116
+ # List OpenType features available for scripts.
117
+ # If no script is specified, shows features for all scripts.
118
+ #
119
+ # @param font_file [String] Path to the font file
120
+ def features(font_file)
121
+ command = Commands::FeaturesCommand.new(font_file, options)
122
+ result = command.run
123
+ output_result(result)
124
+ rescue Errno::ENOENT, Error => e
125
+ handle_error(e)
126
+ end
127
+
128
+ desc "dump-table FONT_FILE TABLE_TAG", "Dump raw table data to stdout"
129
+ # Dump raw binary table data to stdout.
130
+ #
131
+ # @param font_file [String] Path to the font file
132
+ # @param table_tag [String] Four-character table tag (e.g., 'name', 'head')
133
+ def dump_table(font_file, table_tag)
134
+ command = Commands::DumpTableCommand.new(font_file, table_tag, options)
135
+ raw_data = command.run
136
+
137
+ # Write binary data directly to stdout
138
+ $stdout.binmode
139
+ $stdout.write(raw_data)
140
+ rescue Errno::ENOENT, Error => e
141
+ handle_error(e)
142
+ end
143
+
144
+ desc "version", "Display version information"
145
+ # Display the Fontisan version.
146
+ def version
147
+ puts "Fontisan version #{Fontisan::VERSION}"
148
+ end
149
+
150
+ private
151
+
152
+ # Output the result in the requested format.
153
+ #
154
+ # @param result [Object] The result object to output
155
+ def output_result(result)
156
+ output = case options[:format]
157
+ when "yaml"
158
+ result.to_yaml
159
+ when "json"
160
+ result.to_json
161
+ else
162
+ format_as_text(result)
163
+ end
164
+
165
+ puts output unless options[:quiet]
166
+ end
167
+
168
+ # Format result as human-readable text.
169
+ #
170
+ # @param result [Object] The result object to format
171
+ # @return [String] Formatted text output
172
+ def format_as_text(result)
173
+ formatter = Formatters::TextFormatter.new
174
+ formatter.format(result)
175
+ end
176
+
177
+ # Handle errors based on verbosity settings.
178
+ #
179
+ # @param error [Error, Errno::ENOENT] The error to handle
180
+ def handle_error(error)
181
+ raise error if options[:verbose]
182
+
183
+ # Convert Errno::ENOENT to user-friendly message
184
+ if error.is_a?(Errno::ENOENT)
185
+ end
186
+ message = error.message
187
+
188
+ warn message unless options[:quiet]
189
+ exit 1
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../font_loader"
4
+ require_relative "../error"
5
+
6
+ module Fontisan
7
+ module Commands
8
+ # Abstract base class for all CLI commands.
9
+ #
10
+ # Provides common functionality for loading fonts using FontLoader for
11
+ # automatic format detection. Works polymorphically with TrueTypeFont
12
+ # and OpenTypeFont instances.
13
+ #
14
+ # Subclasses must implement the `run` method to define command-specific behavior.
15
+ #
16
+ # @example Creating a command subclass
17
+ # class MyCommand < BaseCommand
18
+ # def run
19
+ # info = Models::FontInfo.new
20
+ # info.family_name = font.table("name").english_name(Tables::Name::FAMILY)
21
+ # info
22
+ # end
23
+ # end
24
+ class BaseCommand
25
+ # Initialize a new command with a font file path and options.
26
+ #
27
+ # @param font_path [String] Path to the font file
28
+ # @param options [Hash] Optional command options
29
+ # @option options [Integer] :font_index Index of font in TTC/OTC collection (default: 0)
30
+ def initialize(font_path, options = {})
31
+ @font_path = font_path
32
+ @options = options
33
+ @font = load_font
34
+ end
35
+
36
+ # Execute the command.
37
+ #
38
+ # This method must be implemented by subclasses.
39
+ #
40
+ # @raise [NotImplementedError] if not implemented by subclass
41
+ # @return [Models::*] Command-specific result as lutaml-model object
42
+ def run
43
+ raise NotImplementedError, "Subclasses must implement the run method"
44
+ end
45
+
46
+ protected
47
+
48
+ # @!attribute [r] font_path
49
+ # @return [String] Path to the font file
50
+ # @!attribute [r] font
51
+ # @return [TrueTypeFont, OpenTypeFont] Loaded font instance
52
+ # @!attribute [r] options
53
+ # @return [Hash] Command options
54
+ attr_reader :font_path, :font, :options
55
+
56
+ private
57
+
58
+ # Load the font using FontLoader.
59
+ #
60
+ # Uses FontLoader for automatic format detection and loading.
61
+ # Returns either TrueTypeFont or OpenTypeFont depending on file format.
62
+ #
63
+ # @return [TrueTypeFont, OpenTypeFont] The loaded font
64
+ # @raise [Errno::ENOENT] if file does not exist
65
+ # @raise [UnsupportedFormatError] for WOFF/WOFF2 or other unsupported formats
66
+ # @raise [InvalidFontError] for corrupted or unknown formats
67
+ # @raise [Error] for other loading failures
68
+ def load_font
69
+ FontLoader.load(@font_path, font_index: @options[:font_index] || 0)
70
+ rescue Errno::ENOENT
71
+ # Re-raise file not found as-is
72
+ raise
73
+ rescue UnsupportedFormatError, InvalidFontError
74
+ # Re-raise format errors as-is
75
+ raise
76
+ rescue StandardError => e
77
+ # Wrap other errors
78
+ raise Error, "Failed to load font: #{e.message}"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../font_loader"
4
+ require_relative "../error"
5
+
6
+ module Fontisan
7
+ module Commands
8
+ # Command to dump raw table data from fonts
9
+ #
10
+ # This command extracts the binary data of a specific OpenType table
11
+ # and outputs it directly. This is useful for examining table contents
12
+ # or extracting tables for external processing.
13
+ class DumpTableCommand
14
+ # Initialize a new dump table command
15
+ #
16
+ # @param font_path [String] Path to the font file
17
+ # @param table_tag [String] Four-character table tag (e.g., 'name', 'head')
18
+ # @param options [Hash] Optional command options
19
+ # @option options [Integer] :font_index Index of font in TTC/OTC collection (default: 0)
20
+ def initialize(font_path, table_tag, options = {})
21
+ @font_path = font_path
22
+ @table_tag = table_tag
23
+ @options = options
24
+ @font = load_font
25
+ end
26
+
27
+ # Execute the dump table command
28
+ #
29
+ # @return [String] Raw binary table data
30
+ # @raise [Error] if table does not exist or data is not available
31
+ def run
32
+ unless @font.has_table?(@table_tag)
33
+ raise Error,
34
+ "Font does not have '#{@table_tag}' table"
35
+ end
36
+
37
+ # Get raw table data
38
+ table_data = @font.instance_variable_get(:@table_data)
39
+ raw_data = table_data[@table_tag]
40
+
41
+ unless raw_data
42
+ raise Error,
43
+ "Table data not available for '#{@table_tag}'"
44
+ end
45
+
46
+ raw_data
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :font
52
+
53
+ # Load the font using FontLoader
54
+ #
55
+ # @return [TrueTypeFont, OpenTypeFont] The loaded font
56
+ # @raise [Errno::ENOENT] if file does not exist
57
+ # @raise [UnsupportedFormatError] for WOFF/WOFF2 or other unsupported formats
58
+ # @raise [InvalidFontError] for corrupted or unknown formats
59
+ # @raise [Error] for other loading failures
60
+ def load_font
61
+ FontLoader.load(@font_path, font_index: @options[:font_index] || 0)
62
+ rescue Errno::ENOENT
63
+ raise
64
+ rescue UnsupportedFormatError, InvalidFontError
65
+ raise
66
+ rescue StandardError => e
67
+ raise Error, "Failed to load font: #{e.message}"
68
+ end
69
+ end
70
+ end
71
+ end