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,42 @@
|
|
|
1
|
+
# OpenType script tags and descriptions
|
|
2
|
+
# Reference: OpenType specification
|
|
3
|
+
arab: Arabic
|
|
4
|
+
armn: Armenian
|
|
5
|
+
beng: Bengali
|
|
6
|
+
bopo: Bopomofo
|
|
7
|
+
brai: Braille
|
|
8
|
+
byzm: Byzantine Musical Symbols
|
|
9
|
+
cans: Canadian Aboriginal Syllabics
|
|
10
|
+
cher: Cherokee
|
|
11
|
+
cyrl: Cyrillic
|
|
12
|
+
DFLT: Default
|
|
13
|
+
deva: Devanagari
|
|
14
|
+
ethi: Ethiopic
|
|
15
|
+
geor: Georgian
|
|
16
|
+
goth: Gothic
|
|
17
|
+
grek: Greek
|
|
18
|
+
gujr: Gujarati
|
|
19
|
+
guru: Gurmukhi
|
|
20
|
+
hang: Hangul
|
|
21
|
+
hani: Han Ideographs
|
|
22
|
+
hebr: Hebrew
|
|
23
|
+
hira: Hiragana
|
|
24
|
+
kana: Katakana
|
|
25
|
+
khmr: Khmer
|
|
26
|
+
knda: Kannada
|
|
27
|
+
lao: Lao
|
|
28
|
+
latn: Latin
|
|
29
|
+
math: Mathematical Alphanumeric Symbols
|
|
30
|
+
mlym: Malayalam
|
|
31
|
+
mong: Mongolian
|
|
32
|
+
mymr: Myanmar
|
|
33
|
+
ogam: Ogham
|
|
34
|
+
orya: Oriya
|
|
35
|
+
runr: Runic
|
|
36
|
+
sinh: Sinhala
|
|
37
|
+
syrc: Syriac
|
|
38
|
+
taml: Tamil
|
|
39
|
+
telu: Telugu
|
|
40
|
+
thaa: Thaana
|
|
41
|
+
thai: Thai
|
|
42
|
+
tibt: Tibetan
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
# Constants module containing immutable constant definitions for font file operations.
|
|
5
|
+
#
|
|
6
|
+
# This module defines all magic numbers, version identifiers, and file format constants
|
|
7
|
+
# used throughout the fontisan gem. These values are based on the TrueType Collection,
|
|
8
|
+
# TrueType Font, and OpenType Font specifications.
|
|
9
|
+
module Constants
|
|
10
|
+
# TrueType Collection file signature tag.
|
|
11
|
+
# All valid TTC files must begin with this 4-byte tag.
|
|
12
|
+
TTC_TAG = "ttcf"
|
|
13
|
+
|
|
14
|
+
# TrueType Collection Version 1.0 identifier.
|
|
15
|
+
# Represents the original TTC format version.
|
|
16
|
+
TTC_VERSION_1 = 0x00010000
|
|
17
|
+
|
|
18
|
+
# TrueType Collection Version 2.0 identifier.
|
|
19
|
+
# Represents the extended TTC format with digital signature support.
|
|
20
|
+
TTC_VERSION_2 = 0x00020000
|
|
21
|
+
|
|
22
|
+
# SFNT version for TrueType fonts
|
|
23
|
+
SFNT_VERSION_TRUETYPE = 0x00010000
|
|
24
|
+
|
|
25
|
+
# SFNT version for OpenType fonts with CFF outlines ('OTTO')
|
|
26
|
+
SFNT_VERSION_OTTO = 0x4F54544F
|
|
27
|
+
|
|
28
|
+
# Head table tag identifier.
|
|
29
|
+
# The 'head' table contains global font header information including
|
|
30
|
+
# the checksum adjustment field.
|
|
31
|
+
HEAD_TAG = "head"
|
|
32
|
+
|
|
33
|
+
# Name table tag identifier
|
|
34
|
+
NAME_TAG = "name"
|
|
35
|
+
|
|
36
|
+
# OS/2 table tag identifier
|
|
37
|
+
OS2_TAG = "OS/2"
|
|
38
|
+
|
|
39
|
+
# Post table tag identifier
|
|
40
|
+
POST_TAG = "post"
|
|
41
|
+
|
|
42
|
+
# Cmap table tag identifier
|
|
43
|
+
CMAP_TAG = "cmap"
|
|
44
|
+
|
|
45
|
+
# Glyf table tag identifier (TrueType glyph data)
|
|
46
|
+
GLYF_TAG = "glyf"
|
|
47
|
+
|
|
48
|
+
# Loca table tag identifier (TrueType glyph index to location)
|
|
49
|
+
LOCA_TAG = "loca"
|
|
50
|
+
|
|
51
|
+
# CFF table tag identifier (OpenType CFF glyph data)
|
|
52
|
+
CFF_TAG = "CFF "
|
|
53
|
+
|
|
54
|
+
# GSUB table tag identifier (Glyph Substitution)
|
|
55
|
+
GSUB_TAG = "GSUB"
|
|
56
|
+
|
|
57
|
+
# GPOS table tag identifier (Glyph Positioning)
|
|
58
|
+
GPOS_TAG = "GPOS"
|
|
59
|
+
|
|
60
|
+
# Fvar table tag identifier (Font Variations)
|
|
61
|
+
FVAR_TAG = "fvar"
|
|
62
|
+
|
|
63
|
+
# Magic number used for font file checksum adjustment calculation.
|
|
64
|
+
# This constant is used in conjunction with the file checksum to compute
|
|
65
|
+
# the checksumAdjustment value stored in the 'head' table.
|
|
66
|
+
# Formula: checksumAdjustment = CHECKSUM_ADJUSTMENT_MAGIC - file_checksum
|
|
67
|
+
CHECKSUM_ADJUSTMENT_MAGIC = 0xB1B0AFBA
|
|
68
|
+
|
|
69
|
+
# Supported TTC version numbers.
|
|
70
|
+
# An array of valid version identifiers for TrueType Collection files.
|
|
71
|
+
SUPPORTED_VERSIONS = [TTC_VERSION_1, TTC_VERSION_2].freeze
|
|
72
|
+
|
|
73
|
+
# Table data alignment boundary in bytes.
|
|
74
|
+
# All table data in TTF files must be aligned to 4-byte boundaries,
|
|
75
|
+
# with padding added as necessary.
|
|
76
|
+
TABLE_ALIGNMENT = 4
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class InvalidFontError < Error; end
|
|
7
|
+
|
|
8
|
+
class UnsupportedFormatError < Error; end
|
|
9
|
+
|
|
10
|
+
class CorruptedTableError < Error; end
|
|
11
|
+
|
|
12
|
+
class MissingTableError < Error; end
|
|
13
|
+
|
|
14
|
+
class ParseError < Error; end
|
|
15
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
require_relative "true_type_font"
|
|
5
|
+
require_relative "open_type_font"
|
|
6
|
+
require_relative "true_type_collection"
|
|
7
|
+
require_relative "open_type_collection"
|
|
8
|
+
require_relative "error"
|
|
9
|
+
|
|
10
|
+
module Fontisan
|
|
11
|
+
# FontLoader provides unified font loading with automatic format detection.
|
|
12
|
+
#
|
|
13
|
+
# This class is the primary entry point for loading fonts in Fontisan.
|
|
14
|
+
# It automatically detects the font format and returns the appropriate
|
|
15
|
+
# domain object (TrueTypeFont, OpenTypeFont, TrueTypeCollection, or OpenTypeCollection).
|
|
16
|
+
#
|
|
17
|
+
# @example Load any font type
|
|
18
|
+
# font = FontLoader.load("font.ttf") # => TrueTypeFont
|
|
19
|
+
# font = FontLoader.load("font.otf") # => OpenTypeFont
|
|
20
|
+
# font = FontLoader.load("fonts.ttc") # => TrueTypeFont (first in collection)
|
|
21
|
+
# font = FontLoader.load("fonts.ttc", font_index: 2) # => TrueTypeFont (third in collection)
|
|
22
|
+
class FontLoader
|
|
23
|
+
# Load a font from file with automatic format detection
|
|
24
|
+
#
|
|
25
|
+
# @param path [String] Path to the font file
|
|
26
|
+
# @param font_index [Integer] Index of font in collection (0-based, default: 0)
|
|
27
|
+
# @return [TrueTypeFont, OpenTypeFont] The loaded font object
|
|
28
|
+
# @raise [Errno::ENOENT] if file does not exist
|
|
29
|
+
# @raise [UnsupportedFormatError] for WOFF/WOFF2 or other unsupported formats
|
|
30
|
+
# @raise [InvalidFontError] for corrupted or unknown formats
|
|
31
|
+
def self.load(path, font_index: 0)
|
|
32
|
+
raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
|
|
33
|
+
|
|
34
|
+
File.open(path, "rb") do |io|
|
|
35
|
+
signature = io.read(4)
|
|
36
|
+
io.rewind
|
|
37
|
+
|
|
38
|
+
case signature
|
|
39
|
+
when Constants::TTC_TAG
|
|
40
|
+
load_from_collection(io, path, font_index)
|
|
41
|
+
when pack_uint32(Constants::SFNT_VERSION_TRUETYPE)
|
|
42
|
+
TrueTypeFont.from_file(path)
|
|
43
|
+
when "OTTO"
|
|
44
|
+
OpenTypeFont.from_file(path)
|
|
45
|
+
when "wOFF"
|
|
46
|
+
raise UnsupportedFormatError,
|
|
47
|
+
"Unsupported font format: WOFF. Fontisan currently supports TTF, OTF, TTC, and OTC files."
|
|
48
|
+
when "wOF2"
|
|
49
|
+
raise UnsupportedFormatError,
|
|
50
|
+
"Unsupported font format: WOFF2. Fontisan currently supports TTF, OTF, TTC, and OTC files."
|
|
51
|
+
else
|
|
52
|
+
raise InvalidFontError,
|
|
53
|
+
"Unknown font format. Expected TTF, OTF, TTC, or OTC file."
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Load from a collection file (TTC or OTC)
|
|
59
|
+
#
|
|
60
|
+
# @param io [IO] Open file handle
|
|
61
|
+
# @param path [String] Path to the collection file
|
|
62
|
+
# @param font_index [Integer] Index of font to extract
|
|
63
|
+
# @return [TrueTypeFont, OpenTypeFont] The loaded font object
|
|
64
|
+
# @raise [InvalidFontError] if collection type cannot be determined
|
|
65
|
+
def self.load_from_collection(io, path, font_index)
|
|
66
|
+
# Read collection header to get font offsets
|
|
67
|
+
io.seek(12) # Skip tag (4) + major_version (2) + minor_version (2) + num_fonts marker (4)
|
|
68
|
+
num_fonts = io.read(4).unpack1("N")
|
|
69
|
+
|
|
70
|
+
if font_index >= num_fonts
|
|
71
|
+
raise InvalidFontError,
|
|
72
|
+
"Font index #{font_index} out of range (collection has #{num_fonts} fonts)"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Read first offset to detect collection type
|
|
76
|
+
first_offset = io.read(4).unpack1("N")
|
|
77
|
+
|
|
78
|
+
# Peek at first font's sfnt_version to determine TTC vs OTC
|
|
79
|
+
io.seek(first_offset)
|
|
80
|
+
sfnt_version = io.read(4).unpack1("N")
|
|
81
|
+
io.rewind
|
|
82
|
+
|
|
83
|
+
case sfnt_version
|
|
84
|
+
when Constants::SFNT_VERSION_TRUETYPE
|
|
85
|
+
# TrueType Collection
|
|
86
|
+
ttc = TrueTypeCollection.from_file(path)
|
|
87
|
+
File.open(path, "rb") { |f| ttc.font(font_index, f) }
|
|
88
|
+
when Constants::SFNT_VERSION_OTTO
|
|
89
|
+
# OpenType Collection
|
|
90
|
+
otc = OpenTypeCollection.from_file(path)
|
|
91
|
+
File.open(path, "rb") { |f| otc.font(font_index, f) }
|
|
92
|
+
else
|
|
93
|
+
raise InvalidFontError,
|
|
94
|
+
"Unknown font type in collection (sfnt version: 0x#{sfnt_version.to_s(16)})"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Pack uint32 value to big-endian bytes
|
|
99
|
+
#
|
|
100
|
+
# @param value [Integer] The uint32 value
|
|
101
|
+
# @return [String] 4-byte binary string
|
|
102
|
+
# @api private
|
|
103
|
+
def self.pack_uint32(value)
|
|
104
|
+
[value].pack("N")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private_class_method :load_from_collection, :pack_uint32
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Formatters
|
|
5
|
+
# TextFormatter formats model objects into human-readable text output.
|
|
6
|
+
#
|
|
7
|
+
# This formatter handles Models::FontInfo and Models::TableInfo objects,
|
|
8
|
+
# presenting them with proper alignment and spacing for terminal display.
|
|
9
|
+
#
|
|
10
|
+
# @example Format font information
|
|
11
|
+
# formatter = TextFormatter.new
|
|
12
|
+
# text = formatter.format(font_info)
|
|
13
|
+
# puts text
|
|
14
|
+
class TextFormatter
|
|
15
|
+
# Format a model object into human-readable text.
|
|
16
|
+
#
|
|
17
|
+
# @param model [Object] The model to format (FontInfo, TableInfo, etc.)
|
|
18
|
+
# @return [String] Formatted text representation
|
|
19
|
+
def format(model)
|
|
20
|
+
case model
|
|
21
|
+
when Models::FontInfo
|
|
22
|
+
format_font_info(model)
|
|
23
|
+
when Models::TableInfo
|
|
24
|
+
format_table_info(model)
|
|
25
|
+
when Models::GlyphInfo
|
|
26
|
+
format_glyph_info(model)
|
|
27
|
+
when Models::UnicodeMappings
|
|
28
|
+
format_unicode_mappings(model)
|
|
29
|
+
when Models::VariableFontInfo
|
|
30
|
+
format_variable_font_info(model)
|
|
31
|
+
when Models::OpticalSizeInfo
|
|
32
|
+
format_optical_size_info(model)
|
|
33
|
+
when Models::ScriptsInfo
|
|
34
|
+
format_scripts_info(model)
|
|
35
|
+
when Models::AllScriptsFeaturesInfo
|
|
36
|
+
format_all_scripts_features_info(model)
|
|
37
|
+
when Models::FeaturesInfo
|
|
38
|
+
format_features_info(model)
|
|
39
|
+
else
|
|
40
|
+
model.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Format FontInfo as human-readable text.
|
|
47
|
+
#
|
|
48
|
+
# @param info [Models::FontInfo] Font information to format
|
|
49
|
+
# @return [String] Formatted text with aligned labels and values
|
|
50
|
+
def format_font_info(info)
|
|
51
|
+
lines = []
|
|
52
|
+
|
|
53
|
+
# Font type should be first (formatted for display)
|
|
54
|
+
font_type_display = format_font_type_display(info.font_format,
|
|
55
|
+
info.is_variable)
|
|
56
|
+
add_line(lines, "Font type", font_type_display)
|
|
57
|
+
|
|
58
|
+
add_line(lines, "Family", info.family_name)
|
|
59
|
+
add_line(lines, "Subfamily", info.subfamily_name)
|
|
60
|
+
add_line(lines, "Full name", info.full_name)
|
|
61
|
+
add_line(lines, "PostScript name", info.postscript_name)
|
|
62
|
+
add_line(lines, "PostScript CID name", info.postscript_cid_name)
|
|
63
|
+
add_line(lines, "Preferred family", info.preferred_family)
|
|
64
|
+
add_line(lines, "Preferred subfamily", info.preferred_subfamily)
|
|
65
|
+
add_line(lines, "Mac font menu name", info.mac_font_menu_name)
|
|
66
|
+
add_line(lines, "Version", info.version)
|
|
67
|
+
add_line(lines, "Unique ID", info.unique_id)
|
|
68
|
+
add_line(lines, "Description", info.description)
|
|
69
|
+
add_line(lines, "Designer", info.designer)
|
|
70
|
+
add_line(lines, "Designer URL", info.designer_url)
|
|
71
|
+
add_line(lines, "Manufacturer", info.manufacturer)
|
|
72
|
+
add_line(lines, "Vendor URL", info.vendor_url)
|
|
73
|
+
add_line(lines, "Vendor ID", info.vendor_id)
|
|
74
|
+
add_line(lines, "Trademark", info.trademark)
|
|
75
|
+
add_line(lines, "Copyright", info.copyright)
|
|
76
|
+
add_line(lines, "License Description", info.license_description)
|
|
77
|
+
add_line(lines, "License URL", info.license_url)
|
|
78
|
+
add_line(lines, "Sample text", info.sample_text)
|
|
79
|
+
add_line(lines, "Font revision", format_float(info.font_revision))
|
|
80
|
+
add_line(lines, "Permissions", info.permissions)
|
|
81
|
+
add_line(lines, "Units per em", info.units_per_em)
|
|
82
|
+
|
|
83
|
+
lines.join("\n")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Format TableInfo as human-readable text.
|
|
87
|
+
#
|
|
88
|
+
# @param info [Models::TableInfo] Table information to format
|
|
89
|
+
# @return [String] Formatted text with table directory listing
|
|
90
|
+
def format_table_info(info)
|
|
91
|
+
lines = []
|
|
92
|
+
lines << "SFNT Version: #{info.sfnt_version}"
|
|
93
|
+
lines << "Number of tables: #{info.num_tables}"
|
|
94
|
+
lines << ""
|
|
95
|
+
lines << "Tables:"
|
|
96
|
+
|
|
97
|
+
# Find max tag length for alignment
|
|
98
|
+
max_tag_len = info.tables.map { |t| t.tag.length }.max || 4
|
|
99
|
+
|
|
100
|
+
info.tables.each do |table|
|
|
101
|
+
tag = table.tag.ljust(max_tag_len)
|
|
102
|
+
lines << Kernel.format(" %<tag>s %<length>10d bytes (offset: %<offset>d, checksum: 0x%<checksum>08X)",
|
|
103
|
+
tag: tag, length: table.length, offset: table.offset, checksum: table.checksum)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
lines.join("\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Format GlyphInfo as human-readable text.
|
|
110
|
+
#
|
|
111
|
+
# @param info [Models::GlyphInfo] Glyph information to format
|
|
112
|
+
# @return [String] Formatted text with glyph names
|
|
113
|
+
def format_glyph_info(info)
|
|
114
|
+
lines = []
|
|
115
|
+
|
|
116
|
+
if info.glyph_names.empty?
|
|
117
|
+
lines << "No glyph name information available"
|
|
118
|
+
lines << "Source: #{info.source}"
|
|
119
|
+
else
|
|
120
|
+
lines << "Glyph count: #{info.glyph_count}"
|
|
121
|
+
lines << "Source: #{info.source}"
|
|
122
|
+
lines << ""
|
|
123
|
+
lines << "Glyph names:"
|
|
124
|
+
|
|
125
|
+
info.glyph_names.each_with_index do |name, index|
|
|
126
|
+
lines << Kernel.format(" %5d %s", index, name)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
lines.join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Format UnicodeMappings as human-readable text.
|
|
134
|
+
#
|
|
135
|
+
# @param mappings [Models::UnicodeMappings] Unicode mappings to format
|
|
136
|
+
# @return [String] Formatted text with Unicode to glyph mappings
|
|
137
|
+
def format_unicode_mappings(mappings)
|
|
138
|
+
lines = []
|
|
139
|
+
|
|
140
|
+
if mappings.mappings.empty?
|
|
141
|
+
lines << "No Unicode mappings available"
|
|
142
|
+
else
|
|
143
|
+
lines << "Unicode mappings: #{mappings.count}"
|
|
144
|
+
lines << ""
|
|
145
|
+
|
|
146
|
+
mappings.mappings.each do |mapping|
|
|
147
|
+
lines << if mapping.glyph_name
|
|
148
|
+
"#{mapping.codepoint} glyph #{mapping.glyph_index} #{mapping.glyph_name}"
|
|
149
|
+
else
|
|
150
|
+
"#{mapping.codepoint} glyph #{mapping.glyph_index}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
lines.join("\n")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Format VariableFontInfo as human-readable text.
|
|
159
|
+
#
|
|
160
|
+
# @param info [Models::VariableFontInfo] Variable font information to format
|
|
161
|
+
# @return [String] Formatted text with axes and instances
|
|
162
|
+
def format_variable_font_info(info)
|
|
163
|
+
lines = []
|
|
164
|
+
|
|
165
|
+
unless info.is_variable
|
|
166
|
+
lines << "Not a variable font"
|
|
167
|
+
return lines.join("\n")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
info.axes.each_with_index do |axis, i|
|
|
171
|
+
lines << "Axis #{i}: #{axis.tag}"
|
|
172
|
+
lines << "Axis #{i} name: #{axis.name}" if axis.name
|
|
173
|
+
lines << "Axis #{i} range: #{format_float(axis.min_value)} #{format_float(axis.max_value)}"
|
|
174
|
+
lines << "Axis #{i} default: #{format_float(axis.default_value)}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
info.instances.each_with_index do |instance, i|
|
|
178
|
+
lines << "Instance #{i} name: #{instance.name}"
|
|
179
|
+
coordinates = instance.coordinates.map do |c|
|
|
180
|
+
format_float(c)
|
|
181
|
+
end.join(" ")
|
|
182
|
+
lines << "Instance #{i} position: #{coordinates}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
lines.join("\n")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Format OpticalSizeInfo as human-readable text.
|
|
189
|
+
#
|
|
190
|
+
# @param info [Models::OpticalSizeInfo] Optical size information to format
|
|
191
|
+
# @return [String] Formatted text with optical size range
|
|
192
|
+
def format_optical_size_info(info)
|
|
193
|
+
return "No optical size information" unless info.has_optical_size
|
|
194
|
+
|
|
195
|
+
"Size range: [#{format_float(info.lower_point_size)}, #{format_float(info.upper_point_size)}) pt (source: #{info.source})"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Format ScriptsInfo as human-readable text.
|
|
199
|
+
#
|
|
200
|
+
# @param info [Models::ScriptsInfo] Scripts information to format
|
|
201
|
+
# @return [String] Formatted text with script tags and descriptions
|
|
202
|
+
def format_scripts_info(info)
|
|
203
|
+
lines = []
|
|
204
|
+
|
|
205
|
+
if info.scripts.empty?
|
|
206
|
+
lines << "No scripts found"
|
|
207
|
+
else
|
|
208
|
+
lines << "Script count: #{info.script_count}"
|
|
209
|
+
lines << ""
|
|
210
|
+
|
|
211
|
+
info.scripts.each do |script|
|
|
212
|
+
lines << "#{script.tag} #{script.description}"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
lines.join("\n")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Format AllScriptsFeaturesInfo as human-readable text.
|
|
220
|
+
#
|
|
221
|
+
# @param info [Models::AllScriptsFeaturesInfo] All scripts features information to format
|
|
222
|
+
# @return [String] Formatted text with features for all scripts
|
|
223
|
+
def format_all_scripts_features_info(info)
|
|
224
|
+
lines = []
|
|
225
|
+
|
|
226
|
+
info.scripts_features.each_with_index do |script_features, index|
|
|
227
|
+
lines << "" if index.positive? # Add blank line between scripts
|
|
228
|
+
lines << "Script: #{script_features.script}"
|
|
229
|
+
lines << "Feature count: #{script_features.feature_count}"
|
|
230
|
+
lines << ""
|
|
231
|
+
|
|
232
|
+
if script_features.features.empty?
|
|
233
|
+
lines << " No features found"
|
|
234
|
+
else
|
|
235
|
+
script_features.features.each do |feature|
|
|
236
|
+
lines << " #{feature.tag} #{feature.description}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
lines.join("\n")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Format FeaturesInfo as human-readable text.
|
|
245
|
+
#
|
|
246
|
+
# @param info [Models::FeaturesInfo] Features information to format
|
|
247
|
+
# @return [String] Formatted text with feature tags and descriptions
|
|
248
|
+
def format_features_info(info)
|
|
249
|
+
lines = []
|
|
250
|
+
|
|
251
|
+
if info.features.empty?
|
|
252
|
+
lines << "No features found for script '#{info.script}'"
|
|
253
|
+
else
|
|
254
|
+
lines << "Script: #{info.script}"
|
|
255
|
+
lines << "Feature count: #{info.feature_count}"
|
|
256
|
+
lines << ""
|
|
257
|
+
|
|
258
|
+
info.features.each do |feature|
|
|
259
|
+
lines << "#{feature.tag} #{feature.description}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
lines.join("\n")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Add a formatted line to the output if the value is present.
|
|
267
|
+
#
|
|
268
|
+
# @param lines [Array<String>] Output lines array
|
|
269
|
+
# @param label [String] Field label
|
|
270
|
+
# @param value [Object] Field value (skipped if nil or empty string)
|
|
271
|
+
def add_line(lines, label, value)
|
|
272
|
+
return if value.nil? || (value.is_a?(String) && value.empty?)
|
|
273
|
+
|
|
274
|
+
formatted_label = "#{label}:".ljust(25)
|
|
275
|
+
lines << "#{formatted_label} #{value}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Format a float value for display.
|
|
279
|
+
#
|
|
280
|
+
# @param value [Float, nil] Float value to format
|
|
281
|
+
# @return [String, nil] Formatted float or nil if input is nil
|
|
282
|
+
def format_float(value)
|
|
283
|
+
return nil if value.nil?
|
|
284
|
+
|
|
285
|
+
# Format to 5 decimal places, remove trailing zeros
|
|
286
|
+
formatted = Kernel.format("%<value>.5f", value: value)
|
|
287
|
+
formatted.sub(/\.?0+$/, "")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Format font type for human-readable display.
|
|
291
|
+
#
|
|
292
|
+
# @param font_format [String] Enumerated font format code
|
|
293
|
+
# @param is_variable [Boolean] Whether font is variable
|
|
294
|
+
# @return [String, nil] Formatted font type or nil if font_format is nil
|
|
295
|
+
def format_font_type_display(font_format, is_variable)
|
|
296
|
+
return nil if font_format.nil?
|
|
297
|
+
|
|
298
|
+
type = case font_format
|
|
299
|
+
when "truetype"
|
|
300
|
+
"TrueType"
|
|
301
|
+
when "cff"
|
|
302
|
+
"OpenType (CFF)"
|
|
303
|
+
when "unknown"
|
|
304
|
+
"Unknown"
|
|
305
|
+
else
|
|
306
|
+
font_format
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
type += " (Variable)" if is_variable
|
|
310
|
+
type
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require_relative "features_info"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Models
|
|
8
|
+
# Represents features information for all scripts from GSUB/GPOS tables
|
|
9
|
+
class AllScriptsFeaturesInfo < Lutaml::Model::Serializable
|
|
10
|
+
attribute :scripts_features, FeaturesInfo, collection: true
|
|
11
|
+
|
|
12
|
+
json do
|
|
13
|
+
map "scripts_features", to: :scripts_features
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
yaml do
|
|
17
|
+
map "scripts_features", to: :scripts_features
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Models
|
|
7
|
+
# Represents a single feature record
|
|
8
|
+
class FeatureRecord < Lutaml::Model::Serializable
|
|
9
|
+
attribute :tag, :string
|
|
10
|
+
attribute :description, :string
|
|
11
|
+
|
|
12
|
+
json do
|
|
13
|
+
map "tag", to: :tag
|
|
14
|
+
map "description", to: :description
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
yaml do
|
|
18
|
+
map "tag", to: :tag
|
|
19
|
+
map "description", to: :description
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Represents features information from GSUB/GPOS tables
|
|
24
|
+
class FeaturesInfo < Lutaml::Model::Serializable
|
|
25
|
+
attribute :script, :string
|
|
26
|
+
attribute :feature_count, :integer
|
|
27
|
+
attribute :features, FeatureRecord, collection: true
|
|
28
|
+
|
|
29
|
+
json do
|
|
30
|
+
map "script", to: :script
|
|
31
|
+
map "feature_count", to: :feature_count
|
|
32
|
+
map "features", to: :features
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
yaml do
|
|
36
|
+
map "script", to: :script
|
|
37
|
+
map "feature_count", to: :feature_count
|
|
38
|
+
map "features", to: :features
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|