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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +217 -0
- data/Gemfile +15 -0
- data/LICENSE +24 -0
- data/README.adoc +984 -0
- data/Rakefile +95 -0
- data/exe/fontisan +7 -0
- data/fontisan.gemspec +44 -0
- data/lib/fontisan/binary/base_record.rb +57 -0
- data/lib/fontisan/binary/structures.rb +84 -0
- data/lib/fontisan/cli.rb +192 -0
- data/lib/fontisan/commands/base_command.rb +82 -0
- data/lib/fontisan/commands/dump_table_command.rb +71 -0
- data/lib/fontisan/commands/features_command.rb +94 -0
- data/lib/fontisan/commands/glyphs_command.rb +50 -0
- data/lib/fontisan/commands/info_command.rb +120 -0
- data/lib/fontisan/commands/optical_size_command.rb +41 -0
- data/lib/fontisan/commands/scripts_command.rb +59 -0
- data/lib/fontisan/commands/tables_command.rb +52 -0
- data/lib/fontisan/commands/unicode_command.rb +76 -0
- data/lib/fontisan/commands/variable_command.rb +61 -0
- data/lib/fontisan/config/features.yml +143 -0
- data/lib/fontisan/config/scripts.yml +42 -0
- data/lib/fontisan/constants.rb +78 -0
- data/lib/fontisan/error.rb +15 -0
- data/lib/fontisan/font_loader.rb +109 -0
- data/lib/fontisan/formatters/text_formatter.rb +314 -0
- data/lib/fontisan/models/all_scripts_features_info.rb +21 -0
- data/lib/fontisan/models/features_info.rb +42 -0
- data/lib/fontisan/models/font_info.rb +99 -0
- data/lib/fontisan/models/glyph_info.rb +26 -0
- data/lib/fontisan/models/optical_size_info.rb +33 -0
- data/lib/fontisan/models/scripts_info.rb +39 -0
- data/lib/fontisan/models/table_info.rb +55 -0
- data/lib/fontisan/models/unicode_mappings.rb +42 -0
- data/lib/fontisan/models/variable_font_info.rb +82 -0
- data/lib/fontisan/open_type_collection.rb +97 -0
- data/lib/fontisan/open_type_font.rb +292 -0
- data/lib/fontisan/parsers/tag.rb +77 -0
- data/lib/fontisan/tables/cmap.rb +284 -0
- data/lib/fontisan/tables/fvar.rb +157 -0
- data/lib/fontisan/tables/gpos.rb +111 -0
- data/lib/fontisan/tables/gsub.rb +111 -0
- data/lib/fontisan/tables/head.rb +114 -0
- data/lib/fontisan/tables/layout_common.rb +73 -0
- data/lib/fontisan/tables/name.rb +188 -0
- data/lib/fontisan/tables/os2.rb +175 -0
- data/lib/fontisan/tables/post.rb +148 -0
- data/lib/fontisan/true_type_collection.rb +98 -0
- data/lib/fontisan/true_type_font.rb +313 -0
- data/lib/fontisan/utilities/checksum_calculator.rb +89 -0
- data/lib/fontisan/version.rb +5 -0
- data/lib/fontisan.rb +80 -0
- 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
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
|
data/lib/fontisan/cli.rb
ADDED
|
@@ -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
|