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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "base_command"
|
|
6
|
+
require_relative "../models/features_info"
|
|
7
|
+
require_relative "../models/all_scripts_features_info"
|
|
8
|
+
|
|
9
|
+
module Fontisan
|
|
10
|
+
module Commands
|
|
11
|
+
# Command to extract and display features from GSUB/GPOS tables
|
|
12
|
+
class FeaturesCommand < BaseCommand
|
|
13
|
+
def run
|
|
14
|
+
script = @options[:script]
|
|
15
|
+
|
|
16
|
+
# If no script specified, show features for all scripts
|
|
17
|
+
return features_for_all_scripts unless script
|
|
18
|
+
|
|
19
|
+
# Show features for specific script
|
|
20
|
+
features_for_script(script)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def features_for_script(script)
|
|
26
|
+
result = Models::FeaturesInfo.new
|
|
27
|
+
result.script = script
|
|
28
|
+
features_set = Set.new
|
|
29
|
+
|
|
30
|
+
# Collect features from GSUB table
|
|
31
|
+
if font.has_table?(Constants::GSUB_TAG)
|
|
32
|
+
gsub = font.table(Constants::GSUB_TAG)
|
|
33
|
+
features_set.merge(gsub.features(script_tag: script))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Collect features from GPOS table
|
|
37
|
+
if font.has_table?(Constants::GPOS_TAG)
|
|
38
|
+
gpos = font.table(Constants::GPOS_TAG)
|
|
39
|
+
features_set.merge(gpos.features(script_tag: script))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Load feature descriptions
|
|
43
|
+
descriptions = load_feature_descriptions
|
|
44
|
+
|
|
45
|
+
# Build feature records
|
|
46
|
+
result.features = features_set.sort.map do |tag|
|
|
47
|
+
Models::FeatureRecord.new(
|
|
48
|
+
tag: tag,
|
|
49
|
+
description: descriptions[tag] || "Unknown feature",
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
result.feature_count = result.features.length
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def features_for_all_scripts
|
|
58
|
+
result = Models::AllScriptsFeaturesInfo.new
|
|
59
|
+
scripts_set = Set.new
|
|
60
|
+
|
|
61
|
+
# Collect all scripts
|
|
62
|
+
if font.has_table?(Constants::GSUB_TAG)
|
|
63
|
+
gsub = font.table(Constants::GSUB_TAG)
|
|
64
|
+
scripts_set.merge(gsub.scripts)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if font.has_table?(Constants::GPOS_TAG)
|
|
68
|
+
gpos = font.table(Constants::GPOS_TAG)
|
|
69
|
+
scripts_set.merge(gpos.scripts)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get features for each script
|
|
73
|
+
result.scripts_features = scripts_set.sort.map do |script_tag|
|
|
74
|
+
features_for_script(script_tag)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load_feature_descriptions
|
|
81
|
+
config_path = File.join(
|
|
82
|
+
File.dirname(__FILE__),
|
|
83
|
+
"..",
|
|
84
|
+
"config",
|
|
85
|
+
"features.yml",
|
|
86
|
+
)
|
|
87
|
+
YAML.load_file(config_path)
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
warn "Warning: Could not load feature descriptions: #{e.message}"
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Commands
|
|
5
|
+
# Command to list glyph names from a font file
|
|
6
|
+
#
|
|
7
|
+
# Retrieves glyph names from the post table. Different post table versions
|
|
8
|
+
# provide different levels of glyph name information:
|
|
9
|
+
# - Version 1.0: Standard 258 Mac glyph names
|
|
10
|
+
# - Version 2.0: Custom glyph names
|
|
11
|
+
# - Version 3.0+: No glyph names
|
|
12
|
+
#
|
|
13
|
+
# @example List glyph names from a font
|
|
14
|
+
# command = GlyphsCommand.new("font.ttf")
|
|
15
|
+
# result = command.run
|
|
16
|
+
# puts result.glyph_count
|
|
17
|
+
class GlyphsCommand < BaseCommand
|
|
18
|
+
# Execute the command to retrieve glyph names
|
|
19
|
+
#
|
|
20
|
+
# @return [Models::GlyphInfo] Information about glyphs in the font
|
|
21
|
+
def run
|
|
22
|
+
glyph_info = Models::GlyphInfo.new
|
|
23
|
+
|
|
24
|
+
# Try to get glyph names from post table first
|
|
25
|
+
if font.has_table?(Constants::POST_TAG)
|
|
26
|
+
post_table = font.table(Constants::POST_TAG)
|
|
27
|
+
names = post_table.glyph_names
|
|
28
|
+
|
|
29
|
+
if names&.any?
|
|
30
|
+
glyph_info.glyph_names = names
|
|
31
|
+
glyph_info.glyph_count = names.length
|
|
32
|
+
glyph_info.source = "post_#{post_table.version}"
|
|
33
|
+
return glyph_info
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Future: Try CFF table if no post table or no names
|
|
38
|
+
# if font.has_table?('CFF ')
|
|
39
|
+
# # Get names from CFF
|
|
40
|
+
# end
|
|
41
|
+
|
|
42
|
+
# No glyph name information available
|
|
43
|
+
glyph_info.glyph_names = []
|
|
44
|
+
glyph_info.glyph_count = 0
|
|
45
|
+
glyph_info.source = "none"
|
|
46
|
+
glyph_info
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Commands
|
|
5
|
+
# Command to extract font metadata information.
|
|
6
|
+
#
|
|
7
|
+
# This command extracts comprehensive font information from various tables:
|
|
8
|
+
# - name table: family names, version, copyright, etc.
|
|
9
|
+
# - OS/2 table: vendor ID, embedding permissions
|
|
10
|
+
# - head table: font revision, units per em
|
|
11
|
+
#
|
|
12
|
+
# @example Extract font information
|
|
13
|
+
# command = InfoCommand.new("path/to/font.ttf")
|
|
14
|
+
# info = command.run
|
|
15
|
+
# puts info.family_name
|
|
16
|
+
class InfoCommand < BaseCommand
|
|
17
|
+
# Extract font information from all available tables.
|
|
18
|
+
#
|
|
19
|
+
# @return [Models::FontInfo] Font metadata information
|
|
20
|
+
def run
|
|
21
|
+
info = Models::FontInfo.new
|
|
22
|
+
populate_font_format(info)
|
|
23
|
+
populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
|
|
24
|
+
populate_from_os2_table(info) if font.has_table?(Constants::OS2_TAG)
|
|
25
|
+
populate_from_head_table(info) if font.has_table?(Constants::HEAD_TAG)
|
|
26
|
+
info
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Populate font format and variable status based on font class and table presence.
|
|
32
|
+
#
|
|
33
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
34
|
+
def populate_font_format(info)
|
|
35
|
+
# Determine base format from font class
|
|
36
|
+
info.font_format = case font
|
|
37
|
+
when TrueTypeFont
|
|
38
|
+
"truetype"
|
|
39
|
+
when OpenTypeFont
|
|
40
|
+
"cff"
|
|
41
|
+
else
|
|
42
|
+
"unknown"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if variable font
|
|
46
|
+
info.is_variable = font.has_table?(Constants::FVAR_TAG)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Populate FontInfo from the name table.
|
|
50
|
+
#
|
|
51
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
52
|
+
def populate_from_name_table(info)
|
|
53
|
+
name_table = font.table(Constants::NAME_TAG)
|
|
54
|
+
return unless name_table
|
|
55
|
+
|
|
56
|
+
info.family_name = name_table.english_name(Tables::Name::FAMILY)
|
|
57
|
+
info.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
|
|
58
|
+
info.full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
59
|
+
info.postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
60
|
+
info.postscript_cid_name = name_table.english_name(Tables::Name::POSTSCRIPT_CID)
|
|
61
|
+
info.preferred_family = name_table.english_name(Tables::Name::PREFERRED_FAMILY)
|
|
62
|
+
info.preferred_subfamily = name_table.english_name(Tables::Name::PREFERRED_SUBFAMILY)
|
|
63
|
+
info.mac_font_menu_name = name_table.english_name(Tables::Name::COMPATIBLE_FULL)
|
|
64
|
+
info.version = name_table.english_name(Tables::Name::VERSION)
|
|
65
|
+
info.unique_id = name_table.english_name(Tables::Name::UNIQUE_ID)
|
|
66
|
+
info.description = name_table.english_name(Tables::Name::DESCRIPTION)
|
|
67
|
+
info.designer = name_table.english_name(Tables::Name::DESIGNER)
|
|
68
|
+
info.designer_url = name_table.english_name(Tables::Name::DESIGNER_URL)
|
|
69
|
+
info.manufacturer = name_table.english_name(Tables::Name::MANUFACTURER)
|
|
70
|
+
info.vendor_url = name_table.english_name(Tables::Name::VENDOR_URL)
|
|
71
|
+
info.trademark = name_table.english_name(Tables::Name::TRADEMARK)
|
|
72
|
+
info.copyright = name_table.english_name(Tables::Name::COPYRIGHT)
|
|
73
|
+
info.license_description = name_table.english_name(Tables::Name::LICENSE_DESCRIPTION)
|
|
74
|
+
info.license_url = name_table.english_name(Tables::Name::LICENSE_URL)
|
|
75
|
+
info.sample_text = name_table.english_name(Tables::Name::SAMPLE_TEXT)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Populate FontInfo from the OS/2 table.
|
|
79
|
+
#
|
|
80
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
81
|
+
def populate_from_os2_table(info)
|
|
82
|
+
os2_table = font.table(Constants::OS2_TAG)
|
|
83
|
+
return unless os2_table
|
|
84
|
+
|
|
85
|
+
info.vendor_id = os2_table.vendor_id
|
|
86
|
+
info.permissions = format_permissions(os2_table.type_flags)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Populate FontInfo from the head table.
|
|
90
|
+
#
|
|
91
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
92
|
+
def populate_from_head_table(info)
|
|
93
|
+
head_table = font.table(Constants::HEAD_TAG)
|
|
94
|
+
return unless head_table
|
|
95
|
+
|
|
96
|
+
info.font_revision = head_table.font_revision
|
|
97
|
+
info.units_per_em = head_table.units_per_em
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Format OS/2 embedding permission flags into a human-readable string.
|
|
101
|
+
#
|
|
102
|
+
# @param flags [Integer] OS/2 fsType flags
|
|
103
|
+
# @return [String] Formatted permission string
|
|
104
|
+
def format_permissions(flags)
|
|
105
|
+
emb = flags & 15
|
|
106
|
+
result = case emb
|
|
107
|
+
when 0 then "Installable"
|
|
108
|
+
when 2 then "Restricted License"
|
|
109
|
+
when 4 then "Preview & Print"
|
|
110
|
+
when 8 then "Editable"
|
|
111
|
+
else "Unknown (#{emb})"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
result += ", No subsetting" if flags.anybits?(0x100)
|
|
115
|
+
result += ", Bitmap only" if flags.anybits?(0x200)
|
|
116
|
+
result
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_command"
|
|
4
|
+
require_relative "../models/optical_size_info"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Commands
|
|
8
|
+
# Command to extract optical size information from fonts
|
|
9
|
+
#
|
|
10
|
+
# Optical size information indicates the design size range for which a font
|
|
11
|
+
# is optimized. This information can come from:
|
|
12
|
+
# - OS/2 table version 5+ (usLowerOpticalPointSize, usUpperOpticalPointSize)
|
|
13
|
+
# - GPOS 'size' feature (not yet implemented)
|
|
14
|
+
class OpticalSizeCommand < BaseCommand
|
|
15
|
+
# Execute the optical size extraction command
|
|
16
|
+
#
|
|
17
|
+
# @return [Models::OpticalSizeInfo] Optical size information
|
|
18
|
+
def run
|
|
19
|
+
result = Models::OpticalSizeInfo.new
|
|
20
|
+
|
|
21
|
+
# Try OS/2 table first
|
|
22
|
+
if font.has_table?(Constants::OS2_TAG)
|
|
23
|
+
os2_table = font.table(Constants::OS2_TAG)
|
|
24
|
+
|
|
25
|
+
if os2_table.has_optical_point_size?
|
|
26
|
+
result.has_optical_size = true
|
|
27
|
+
result.source = "os2"
|
|
28
|
+
result.lower_point_size = os2_table.lower_optical_point_size
|
|
29
|
+
result.upper_point_size = os2_table.upper_optical_point_size
|
|
30
|
+
return result
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# No optical size information
|
|
35
|
+
result.has_optical_size = false
|
|
36
|
+
result.source = "none"
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require_relative "base_command"
|
|
6
|
+
require_relative "../models/scripts_info"
|
|
7
|
+
|
|
8
|
+
module Fontisan
|
|
9
|
+
module Commands
|
|
10
|
+
# Command to extract and display scripts from GSUB/GPOS tables
|
|
11
|
+
class ScriptsCommand < BaseCommand
|
|
12
|
+
def run
|
|
13
|
+
result = Models::ScriptsInfo.new
|
|
14
|
+
scripts_set = Set.new
|
|
15
|
+
|
|
16
|
+
# Collect scripts from GSUB table
|
|
17
|
+
if font.has_table?(Constants::GSUB_TAG)
|
|
18
|
+
gsub = font.table(Constants::GSUB_TAG)
|
|
19
|
+
scripts_set.merge(gsub.scripts)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Collect scripts from GPOS table
|
|
23
|
+
if font.has_table?(Constants::GPOS_TAG)
|
|
24
|
+
gpos = font.table(Constants::GPOS_TAG)
|
|
25
|
+
scripts_set.merge(gpos.scripts)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Load script descriptions from configuration
|
|
29
|
+
descriptions = load_script_descriptions
|
|
30
|
+
|
|
31
|
+
# Build script records
|
|
32
|
+
result.scripts = scripts_set.sort.map do |tag|
|
|
33
|
+
Models::ScriptRecord.new(
|
|
34
|
+
tag: tag,
|
|
35
|
+
description: descriptions[tag] || "Unknown script",
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result.script_count = result.scripts.length
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def load_script_descriptions
|
|
46
|
+
config_path = File.join(
|
|
47
|
+
File.dirname(__FILE__),
|
|
48
|
+
"..",
|
|
49
|
+
"config",
|
|
50
|
+
"scripts.yml",
|
|
51
|
+
)
|
|
52
|
+
YAML.load_file(config_path)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
warn "Warning: Could not load script descriptions: #{e.message}"
|
|
55
|
+
{}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Commands
|
|
5
|
+
# Command to list all tables in a font file.
|
|
6
|
+
#
|
|
7
|
+
# This command extracts metadata about all tables present in a font file,
|
|
8
|
+
# including their tags, lengths, offsets, and checksums.
|
|
9
|
+
#
|
|
10
|
+
# @example List font tables
|
|
11
|
+
# command = TablesCommand.new("path/to/font.ttf")
|
|
12
|
+
# table_info = command.run
|
|
13
|
+
# puts "Tables: #{table_info.num_tables}"
|
|
14
|
+
class TablesCommand < BaseCommand
|
|
15
|
+
# Extract table information from the font.
|
|
16
|
+
#
|
|
17
|
+
# @return [Models::TableInfo] Font table metadata
|
|
18
|
+
def run
|
|
19
|
+
table_info = Models::TableInfo.new
|
|
20
|
+
table_info.sfnt_version = format_sfnt_version(font.header.sfnt_version)
|
|
21
|
+
table_info.num_tables = font.tables.length
|
|
22
|
+
|
|
23
|
+
table_info.tables = font.tables.map do |entry|
|
|
24
|
+
Models::TableEntry.new(
|
|
25
|
+
tag: entry.tag,
|
|
26
|
+
length: entry.table_length,
|
|
27
|
+
offset: entry.offset,
|
|
28
|
+
checksum: entry.checksum,
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
table_info
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Format the SFNT version into a human-readable string.
|
|
38
|
+
#
|
|
39
|
+
# @param version [Integer] SFNT version number
|
|
40
|
+
# @return [String] Formatted version string
|
|
41
|
+
def format_sfnt_version(version)
|
|
42
|
+
if version == 0x00010000
|
|
43
|
+
"TrueType (0x00010000)"
|
|
44
|
+
elsif version == 0x4F54544F # 'OTTO'
|
|
45
|
+
"OpenType CFF (OTTO)"
|
|
46
|
+
else
|
|
47
|
+
format("0x%08X", version)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_command"
|
|
4
|
+
require_relative "../models/unicode_mappings"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Commands
|
|
8
|
+
# Command to list Unicode to glyph index mappings from a font file
|
|
9
|
+
#
|
|
10
|
+
# Retrieves character code to glyph index mappings from the cmap table.
|
|
11
|
+
# Optionally includes glyph names from the post table if available.
|
|
12
|
+
#
|
|
13
|
+
# @example List Unicode mappings from a font
|
|
14
|
+
# command = UnicodeCommand.new("font.ttf")
|
|
15
|
+
# result = command.run
|
|
16
|
+
# puts result.count
|
|
17
|
+
class UnicodeCommand < BaseCommand
|
|
18
|
+
# Execute the command to retrieve Unicode mappings
|
|
19
|
+
#
|
|
20
|
+
# @return [Models::UnicodeMappings] Unicode to glyph mappings
|
|
21
|
+
def run
|
|
22
|
+
result = Models::UnicodeMappings.new
|
|
23
|
+
result.mappings = []
|
|
24
|
+
result.count = 0
|
|
25
|
+
|
|
26
|
+
return result unless font.has_table?(Constants::CMAP_TAG)
|
|
27
|
+
|
|
28
|
+
cmap_table = font.table(Constants::CMAP_TAG)
|
|
29
|
+
mappings_hash = cmap_table.unicode_mappings
|
|
30
|
+
|
|
31
|
+
return result if mappings_hash.empty?
|
|
32
|
+
|
|
33
|
+
# Optionally get glyph names if post table exists
|
|
34
|
+
glyph_names = fetch_glyph_names if font.has_table?(Constants::POST_TAG)
|
|
35
|
+
|
|
36
|
+
# Convert hash to array of mapping objects, sorted by codepoint
|
|
37
|
+
result.mappings = mappings_hash.map do |codepoint, glyph_index|
|
|
38
|
+
Models::UnicodeMapping.new(
|
|
39
|
+
codepoint: format_codepoint(codepoint),
|
|
40
|
+
glyph_index: glyph_index,
|
|
41
|
+
glyph_name: glyph_names&.[](glyph_index),
|
|
42
|
+
)
|
|
43
|
+
end.sort_by(&:codepoint)
|
|
44
|
+
|
|
45
|
+
result.count = result.mappings.length
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Format codepoint as U+XXXX or U+XXXXXX
|
|
52
|
+
#
|
|
53
|
+
# @param codepoint [Integer] Unicode codepoint value
|
|
54
|
+
# @return [String] Formatted codepoint string
|
|
55
|
+
def format_codepoint(codepoint)
|
|
56
|
+
if codepoint < 0x10000
|
|
57
|
+
format("U+%04X", codepoint)
|
|
58
|
+
else
|
|
59
|
+
format("U+%X", codepoint)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Fetch glyph names from post table
|
|
64
|
+
#
|
|
65
|
+
# @return [Array<String>, nil] Array of glyph names or nil
|
|
66
|
+
def fetch_glyph_names
|
|
67
|
+
post_table = font.table(Constants::POST_TAG)
|
|
68
|
+
names = post_table.glyph_names
|
|
69
|
+
names if names&.any?
|
|
70
|
+
rescue StandardError
|
|
71
|
+
# If post table parsing fails, continue without glyph names
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Commands
|
|
5
|
+
# Command to extract variable font information.
|
|
6
|
+
#
|
|
7
|
+
# This command extracts variation axes and named instances from variable
|
|
8
|
+
# fonts using the fvar (Font Variations) table.
|
|
9
|
+
#
|
|
10
|
+
# @example Extract variable font information
|
|
11
|
+
# command = VariableCommand.new("path/to/variable-font.ttf")
|
|
12
|
+
# info = command.run
|
|
13
|
+
# puts info.axes.first.tag
|
|
14
|
+
class VariableCommand < BaseCommand
|
|
15
|
+
# Extract variable font information from the fvar table.
|
|
16
|
+
#
|
|
17
|
+
# @return [Models::VariableFontInfo] Variable font information
|
|
18
|
+
def run
|
|
19
|
+
result = Models::VariableFontInfo.new
|
|
20
|
+
|
|
21
|
+
# Check if font has fvar table
|
|
22
|
+
unless font.has_table?(Constants::FVAR_TAG)
|
|
23
|
+
result.is_variable = false
|
|
24
|
+
result.axis_count = 0
|
|
25
|
+
result.instance_count = 0
|
|
26
|
+
result.axes = []
|
|
27
|
+
result.instances = []
|
|
28
|
+
return result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
fvar_table = font.table(Constants::FVAR_TAG)
|
|
32
|
+
name_table = font.table(Constants::NAME_TAG) if font.has_table?(Constants::NAME_TAG)
|
|
33
|
+
|
|
34
|
+
result.is_variable = true
|
|
35
|
+
result.axis_count = fvar_table.axis_count
|
|
36
|
+
result.instance_count = fvar_table.instance_count
|
|
37
|
+
|
|
38
|
+
# Extract axes information
|
|
39
|
+
result.axes = fvar_table.axes.map do |axis|
|
|
40
|
+
Models::AxisInfo.new(
|
|
41
|
+
tag: axis.axis_tag,
|
|
42
|
+
name: name_table&.english_name(axis.axis_name_id),
|
|
43
|
+
min_value: axis.min_value,
|
|
44
|
+
default_value: axis.default_value,
|
|
45
|
+
max_value: axis.max_value,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Extract instances information
|
|
50
|
+
result.instances = fvar_table.instances.map do |instance|
|
|
51
|
+
Models::InstanceInfo.new(
|
|
52
|
+
name: name_table&.english_name(instance[:name_id]),
|
|
53
|
+
coordinates: instance[:coordinates],
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
result
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# OpenType feature tags and descriptions
|
|
2
|
+
# Reference: OpenType specification
|
|
3
|
+
aalt: Access All Alternates
|
|
4
|
+
abvf: Above-base Forms
|
|
5
|
+
abvm: Above-base Mark Positioning
|
|
6
|
+
abvs: Above-base Substitutions
|
|
7
|
+
afrc: Alternative Fractions
|
|
8
|
+
akhn: Akhands
|
|
9
|
+
blwf: Below-base Forms
|
|
10
|
+
blwm: Below-base Mark Positioning
|
|
11
|
+
blws: Below-base Substitutions
|
|
12
|
+
c2pc: Petite Capitals From Capitals
|
|
13
|
+
c2sc: Small Capitals From Capitals
|
|
14
|
+
calt: Contextual Alternates
|
|
15
|
+
case: Case-Sensitive Forms
|
|
16
|
+
ccmp: Glyph Composition/Decomposition
|
|
17
|
+
cfar: Conjunct Form After Ro
|
|
18
|
+
cjct: Conjunct Forms
|
|
19
|
+
clig: Contextual Ligatures
|
|
20
|
+
cpct: Centered CJK Punctuation
|
|
21
|
+
cpsp: Capital Spacing
|
|
22
|
+
cswh: Contextual Swash
|
|
23
|
+
curs: Cursive Positioning
|
|
24
|
+
cv01: Character Variant 1
|
|
25
|
+
cv02: Character Variant 2
|
|
26
|
+
cv03: Character Variant 3
|
|
27
|
+
cv99: Character Variant 99
|
|
28
|
+
dlig: Discretionary Ligatures
|
|
29
|
+
dist: Distances
|
|
30
|
+
dnom: Denominators
|
|
31
|
+
dtls: Dotless Forms
|
|
32
|
+
expt: Expert Forms
|
|
33
|
+
falt: Final Glyph on Line Alternates
|
|
34
|
+
fin2: Terminal Forms #2
|
|
35
|
+
fin3: Terminal Forms #3
|
|
36
|
+
fina: Terminal Forms
|
|
37
|
+
frac: Fractions
|
|
38
|
+
fwid: Full Widths
|
|
39
|
+
half: Half Forms
|
|
40
|
+
haln: Halant Forms
|
|
41
|
+
halt: Alternate Half Widths
|
|
42
|
+
hist: Historical Forms
|
|
43
|
+
hkna: Horizontal Kana Alternates
|
|
44
|
+
hlig: Historical Ligatures
|
|
45
|
+
hngl: Hangul
|
|
46
|
+
hojo: Hojo Kanji Forms
|
|
47
|
+
hwid: Half Widths
|
|
48
|
+
init: Initial Forms
|
|
49
|
+
isol: Isolated Forms
|
|
50
|
+
ital: Italics
|
|
51
|
+
jalt: Justification Alternates
|
|
52
|
+
jp04: JIS2004 Forms
|
|
53
|
+
jp78: JIS78 Forms
|
|
54
|
+
jp83: JIS83 Forms
|
|
55
|
+
jp90: JIS90 Forms
|
|
56
|
+
kern: Kerning
|
|
57
|
+
lfbd: Left Bounds
|
|
58
|
+
liga: Standard Ligatures
|
|
59
|
+
ljmo: Leading Jamo Forms
|
|
60
|
+
lnum: Lining Figures
|
|
61
|
+
locl: Localized Forms
|
|
62
|
+
ltra: Left-to-right Alternates
|
|
63
|
+
ltrm: Left-to-right mirrored forms
|
|
64
|
+
mark: Mark Positioning
|
|
65
|
+
med2: Medial Forms #2
|
|
66
|
+
medi: Medial Forms
|
|
67
|
+
mgrk: Mathematical Greek
|
|
68
|
+
mkmk: Mark to Mark Positioning
|
|
69
|
+
mset: Mark Positioning via Substitution
|
|
70
|
+
nalt: Alternate Annotation Forms
|
|
71
|
+
nlck: NLC Kanji Forms
|
|
72
|
+
nukt: Nukta Forms
|
|
73
|
+
numr: Numerators
|
|
74
|
+
onum: Oldstyle Figures
|
|
75
|
+
opbd: Optical Bounds
|
|
76
|
+
ordn: Ordinals
|
|
77
|
+
ornm: Ornaments
|
|
78
|
+
palt: Proportional Alternate Widths
|
|
79
|
+
pcap: Petite Capitals
|
|
80
|
+
pkna: Proportional Kana
|
|
81
|
+
pnum: Proportional Figures
|
|
82
|
+
pref: Pre-Base Forms
|
|
83
|
+
pres: Pre-base Substitutions
|
|
84
|
+
pstf: Post-base Forms
|
|
85
|
+
psts: Post-base Substitutions
|
|
86
|
+
pwid: Proportional Widths
|
|
87
|
+
qwid: Quarter Widths
|
|
88
|
+
rand: Randomize
|
|
89
|
+
rclt: Required Contextual Alternates
|
|
90
|
+
rkrf: Rakar Forms
|
|
91
|
+
rlig: Required Ligatures
|
|
92
|
+
rphf: Reph Forms
|
|
93
|
+
rtbd: Right Bounds
|
|
94
|
+
rtla: Right-to-left Alternates
|
|
95
|
+
rtlm: Right-to-left mirrored forms
|
|
96
|
+
ruby: Ruby Notation Forms
|
|
97
|
+
rvrn: Required Variation Alternates
|
|
98
|
+
salt: Stylistic Alternates
|
|
99
|
+
sinf: Scientific Inferiors
|
|
100
|
+
size: Optical size
|
|
101
|
+
smcp: Small Capitals
|
|
102
|
+
smpl: Simplified Forms
|
|
103
|
+
ss01: Stylistic Set 1
|
|
104
|
+
ss02: Stylistic Set 2
|
|
105
|
+
ss03: Stylistic Set 3
|
|
106
|
+
ss04: Stylistic Set 4
|
|
107
|
+
ss05: Stylistic Set 5
|
|
108
|
+
ss06: Stylistic Set 6
|
|
109
|
+
ss07: Stylistic Set 7
|
|
110
|
+
ss08: Stylistic Set 8
|
|
111
|
+
ss09: Stylistic Set 9
|
|
112
|
+
ss10: Stylistic Set 10
|
|
113
|
+
ss11: Stylistic Set 11
|
|
114
|
+
ss12: Stylistic Set 12
|
|
115
|
+
ss13: Stylistic Set 13
|
|
116
|
+
ss14: Stylistic Set 14
|
|
117
|
+
ss15: Stylistic Set 15
|
|
118
|
+
ss16: Stylistic Set 16
|
|
119
|
+
ss17: Stylistic Set 17
|
|
120
|
+
ss18: Stylistic Set 18
|
|
121
|
+
ss19: Stylistic Set 19
|
|
122
|
+
ss20: Stylistic Set 20
|
|
123
|
+
subs: Subscript
|
|
124
|
+
sups: Superscript
|
|
125
|
+
swsh: Swash
|
|
126
|
+
titl: Titling
|
|
127
|
+
tjmo: Trailing Jamo Forms
|
|
128
|
+
tnam: Traditional Name Forms
|
|
129
|
+
tnum: Tabular Figures
|
|
130
|
+
trad: Traditional Forms
|
|
131
|
+
twid: Third Widths
|
|
132
|
+
unic: Unicase
|
|
133
|
+
valt: Alternate Vertical Metrics
|
|
134
|
+
vatu: Vattu Variants
|
|
135
|
+
vert: Vertical Writing
|
|
136
|
+
vhal: Alternate Vertical Half Metrics
|
|
137
|
+
vjmo: Vowel Jamo Forms
|
|
138
|
+
vkna: Vertical Kana Alternates
|
|
139
|
+
vkrn: Vertical Kerning
|
|
140
|
+
vpal: Proportional Alternate Vertical Metrics
|
|
141
|
+
vrt2: Vertical Alternates and Rotation
|
|
142
|
+
vrtr: Vertical Alternates for Rotation
|
|
143
|
+
zero: Slashed Zero
|