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,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require_relative "../binary/base_record"
|
|
5
|
+
|
|
6
|
+
module Fontisan
|
|
7
|
+
module Tables
|
|
8
|
+
# BinData structure for variation axis record
|
|
9
|
+
#
|
|
10
|
+
# Each axis defines a design dimension along which the font can vary,
|
|
11
|
+
# such as weight (wght), width (wdth), italic (ital), or slant (slnt).
|
|
12
|
+
class VariationAxisRecord < Binary::BaseRecord
|
|
13
|
+
string :axis_tag, length: 4
|
|
14
|
+
int32 :min_value_raw
|
|
15
|
+
int32 :default_value_raw
|
|
16
|
+
int32 :max_value_raw
|
|
17
|
+
uint16 :flags
|
|
18
|
+
uint16 :axis_name_id
|
|
19
|
+
|
|
20
|
+
# Convert minimum value from fixed-point to float
|
|
21
|
+
#
|
|
22
|
+
# @return [Float] Minimum value for this axis
|
|
23
|
+
def min_value
|
|
24
|
+
fixed_to_float(min_value_raw)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Convert default value from fixed-point to float
|
|
28
|
+
#
|
|
29
|
+
# @return [Float] Default value for this axis
|
|
30
|
+
def default_value
|
|
31
|
+
fixed_to_float(default_value_raw)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Convert maximum value from fixed-point to float
|
|
35
|
+
#
|
|
36
|
+
# @return [Float] Maximum value for this axis
|
|
37
|
+
def max_value
|
|
38
|
+
fixed_to_float(max_value_raw)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# BinData structure for instance record
|
|
43
|
+
#
|
|
44
|
+
# Each instance defines a predefined combination of axis values,
|
|
45
|
+
# representing a named style/weight/width combination.
|
|
46
|
+
class InstanceRecord < Binary::BaseRecord
|
|
47
|
+
uint16 :subfamily_name_id
|
|
48
|
+
uint16 :flags
|
|
49
|
+
# Coordinates are read based on axis_count from parent fvar table
|
|
50
|
+
# This needs to be handled by the parent
|
|
51
|
+
|
|
52
|
+
# Get the instance name ID
|
|
53
|
+
#
|
|
54
|
+
# @return [Integer] Name table ID for this instance
|
|
55
|
+
def name_id
|
|
56
|
+
subfamily_name_id
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Parser for the 'fvar' (Font Variations) table
|
|
61
|
+
#
|
|
62
|
+
# The fvar table contains information about variation axes and named
|
|
63
|
+
# instances for variable fonts. This table is present only in variable
|
|
64
|
+
# fonts (OpenType Font Variations).
|
|
65
|
+
#
|
|
66
|
+
# Reference: OpenType specification, fvar table
|
|
67
|
+
#
|
|
68
|
+
# @example Reading an fvar table
|
|
69
|
+
# data = font.table_data("fvar")
|
|
70
|
+
# fvar = Fontisan::Tables::Fvar.read(data)
|
|
71
|
+
# fvar.axes.each do |axis|
|
|
72
|
+
# puts "#{axis.axis_tag}: #{axis.min_value} - #{axis.max_value}"
|
|
73
|
+
# end
|
|
74
|
+
class Fvar < Binary::BaseRecord
|
|
75
|
+
uint16 :major_version
|
|
76
|
+
uint16 :minor_version
|
|
77
|
+
uint16 :axes_array_offset
|
|
78
|
+
uint16 :reserved
|
|
79
|
+
uint16 :axis_count
|
|
80
|
+
uint16 :axis_size
|
|
81
|
+
uint16 :instance_count
|
|
82
|
+
uint16 :instance_size
|
|
83
|
+
|
|
84
|
+
# Parse variation axes from the table data
|
|
85
|
+
#
|
|
86
|
+
# @return [Array<VariationAxisRecord>] Array of axis records
|
|
87
|
+
def axes
|
|
88
|
+
return @axes if @axes
|
|
89
|
+
return @axes = [] if axis_count.zero?
|
|
90
|
+
|
|
91
|
+
# Get the full data buffer as binary string
|
|
92
|
+
data = to_binary_s
|
|
93
|
+
|
|
94
|
+
@axes = Array.new(axis_count) do |i|
|
|
95
|
+
offset = axes_array_offset + (i * axis_size)
|
|
96
|
+
axis_data = data.byteslice(offset, axis_size)
|
|
97
|
+
VariationAxisRecord.read(axis_data)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Parse instance records from the table data
|
|
102
|
+
#
|
|
103
|
+
# @return [Array<Hash>] Array of instance information hashes
|
|
104
|
+
def instances
|
|
105
|
+
return @instances if @instances
|
|
106
|
+
return @instances = [] if instance_count.zero?
|
|
107
|
+
|
|
108
|
+
# Get the full data buffer as binary string
|
|
109
|
+
data = to_binary_s
|
|
110
|
+
|
|
111
|
+
# Calculate instance data offset (after all axes)
|
|
112
|
+
instance_offset = axes_array_offset + (axis_count * axis_size)
|
|
113
|
+
|
|
114
|
+
@instances = Array.new(instance_count) do |i|
|
|
115
|
+
offset = instance_offset + (i * instance_size)
|
|
116
|
+
|
|
117
|
+
# Check bounds
|
|
118
|
+
next nil if offset + instance_size > data.bytesize
|
|
119
|
+
|
|
120
|
+
instance_data = data.byteslice(offset, instance_size)
|
|
121
|
+
next nil if instance_data.nil? || instance_data.empty?
|
|
122
|
+
|
|
123
|
+
# Parse instance data manually
|
|
124
|
+
io = StringIO.new(instance_data)
|
|
125
|
+
io.set_encoding(Encoding::BINARY)
|
|
126
|
+
|
|
127
|
+
# Read subfamily name ID and flags
|
|
128
|
+
subfamily_name_id = io.read(2).unpack1("n")
|
|
129
|
+
flags = io.read(2).unpack1("n")
|
|
130
|
+
|
|
131
|
+
# Read coordinates for each axis (as int32 fixed-point values)
|
|
132
|
+
coordinates = Array.new(axis_count) do
|
|
133
|
+
fixed_to_float(io.read(4).unpack1("N"))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Read optional postScriptNameID if present
|
|
137
|
+
postscript_name_id = nil
|
|
138
|
+
postscript_name_id = io.read(2).unpack1("n") if instance_size >= (4 + (axis_count * 4) + 2)
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
name_id: subfamily_name_id,
|
|
142
|
+
flags: flags,
|
|
143
|
+
coordinates: coordinates,
|
|
144
|
+
postscript_name_id: postscript_name_id,
|
|
145
|
+
}
|
|
146
|
+
end.compact
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get version as a float
|
|
150
|
+
#
|
|
151
|
+
# @return [Float] Version number (e.g., 1.0)
|
|
152
|
+
def version
|
|
153
|
+
major_version + (minor_version / 10.0)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "layout_common"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
# GPOS (Glyph Positioning) table parser
|
|
8
|
+
# Parses OpenType GPOS table to extract scripts and features
|
|
9
|
+
class Gpos < Binary::BaseRecord
|
|
10
|
+
uint16 :major_version
|
|
11
|
+
uint16 :minor_version
|
|
12
|
+
uint16 :script_list_offset
|
|
13
|
+
uint16 :feature_list_offset
|
|
14
|
+
uint16 :lookup_list_offset
|
|
15
|
+
rest :table_data
|
|
16
|
+
|
|
17
|
+
# Get all script tags supported by this font
|
|
18
|
+
# @return [Array<String>] Array of 4-character script tags
|
|
19
|
+
def scripts
|
|
20
|
+
return [] if script_list_offset.zero?
|
|
21
|
+
|
|
22
|
+
script_list_data = table_data[(script_list_offset - 10)..]
|
|
23
|
+
return [] if script_list_data.nil? || script_list_data.empty?
|
|
24
|
+
|
|
25
|
+
script_list = LayoutCommon::ScriptList.read(script_list_data)
|
|
26
|
+
script_list.script_tags
|
|
27
|
+
rescue StandardError
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get all feature tags for a given script
|
|
32
|
+
# @param script_tag [String] 4-character script tag (e.g., 'latn')
|
|
33
|
+
# @return [Array<String>] Array of 4-character feature tags
|
|
34
|
+
def features(script_tag: "latn")
|
|
35
|
+
return [] if script_list_offset.zero? || feature_list_offset.zero?
|
|
36
|
+
|
|
37
|
+
# Get feature indices from the script's LangSys
|
|
38
|
+
feature_indices = feature_indices_for_script(script_tag)
|
|
39
|
+
return [] if feature_indices.empty?
|
|
40
|
+
|
|
41
|
+
# Get feature list
|
|
42
|
+
feature_list_data = table_data[(feature_list_offset - 10)..]
|
|
43
|
+
return [] if feature_list_data.nil? || feature_list_data.empty?
|
|
44
|
+
|
|
45
|
+
feature_list = LayoutCommon::FeatureList.read(feature_list_data)
|
|
46
|
+
|
|
47
|
+
# Collect features referenced by the script
|
|
48
|
+
features = []
|
|
49
|
+
feature_indices.each do |idx|
|
|
50
|
+
next if idx >= feature_list.feature_count
|
|
51
|
+
|
|
52
|
+
features << feature_list.feature_records[idx].feature_tag
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
features.uniq
|
|
56
|
+
rescue StandardError
|
|
57
|
+
[]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Get feature indices for a given script
|
|
63
|
+
# @param script_tag [String] 4-character script tag
|
|
64
|
+
# @return [Array<Integer>] Array of feature indices
|
|
65
|
+
def feature_indices_for_script(script_tag)
|
|
66
|
+
return [] if script_list_offset.zero?
|
|
67
|
+
|
|
68
|
+
script_list_data = table_data[(script_list_offset - 10)..]
|
|
69
|
+
return [] if script_list_data.nil? || script_list_data.empty?
|
|
70
|
+
|
|
71
|
+
script_list = LayoutCommon::ScriptList.read(script_list_data)
|
|
72
|
+
|
|
73
|
+
# Find the script record
|
|
74
|
+
script_record = script_list.script_records.find do |rec|
|
|
75
|
+
rec.script_tag == script_tag
|
|
76
|
+
end
|
|
77
|
+
return [] unless script_record
|
|
78
|
+
|
|
79
|
+
# Parse the script table at the offset
|
|
80
|
+
script_offset = script_record.script_offset
|
|
81
|
+
script_data = script_list_data[script_offset..]
|
|
82
|
+
return [] if script_data.nil? || script_data.empty?
|
|
83
|
+
|
|
84
|
+
script = LayoutCommon::Script.read(script_data)
|
|
85
|
+
|
|
86
|
+
# Get the default LangSys if it exists
|
|
87
|
+
feature_indices = []
|
|
88
|
+
if script.default_lang_sys_offset != 0
|
|
89
|
+
lang_sys_data = script_data[script.default_lang_sys_offset..]
|
|
90
|
+
if lang_sys_data && !lang_sys_data.empty?
|
|
91
|
+
lang_sys = LayoutCommon::LangSys.read(lang_sys_data)
|
|
92
|
+
feature_indices.concat(lang_sys.feature_indices)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Also collect from all LangSys records
|
|
97
|
+
script.lang_sys_records.each do |lang_sys_rec|
|
|
98
|
+
lang_sys_data = script_data[lang_sys_rec.lang_sys_offset..]
|
|
99
|
+
next if lang_sys_data.nil? || lang_sys_data.empty?
|
|
100
|
+
|
|
101
|
+
lang_sys = LayoutCommon::LangSys.read(lang_sys_data)
|
|
102
|
+
feature_indices.concat(lang_sys.feature_indices)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
feature_indices.uniq
|
|
106
|
+
rescue StandardError
|
|
107
|
+
[]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "layout_common"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
# GSUB (Glyph Substitution) table parser
|
|
8
|
+
# Parses OpenType GSUB table to extract scripts and features
|
|
9
|
+
class Gsub < Binary::BaseRecord
|
|
10
|
+
uint16 :major_version
|
|
11
|
+
uint16 :minor_version
|
|
12
|
+
uint16 :script_list_offset
|
|
13
|
+
uint16 :feature_list_offset
|
|
14
|
+
uint16 :lookup_list_offset
|
|
15
|
+
rest :table_data
|
|
16
|
+
|
|
17
|
+
# Get all script tags supported by this font
|
|
18
|
+
# @return [Array<String>] Array of 4-character script tags
|
|
19
|
+
def scripts
|
|
20
|
+
return [] if script_list_offset.zero?
|
|
21
|
+
|
|
22
|
+
script_list_data = table_data[(script_list_offset - 10)..]
|
|
23
|
+
return [] if script_list_data.nil? || script_list_data.empty?
|
|
24
|
+
|
|
25
|
+
script_list = LayoutCommon::ScriptList.read(script_list_data)
|
|
26
|
+
script_list.script_tags
|
|
27
|
+
rescue StandardError
|
|
28
|
+
[]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get all feature tags for a given script
|
|
32
|
+
# @param script_tag [String] 4-character script tag (e.g., 'latn')
|
|
33
|
+
# @return [Array<String>] Array of 4-character feature tags
|
|
34
|
+
def features(script_tag: "latn")
|
|
35
|
+
return [] if script_list_offset.zero? || feature_list_offset.zero?
|
|
36
|
+
|
|
37
|
+
# Get feature indices from the script's LangSys
|
|
38
|
+
feature_indices = feature_indices_for_script(script_tag)
|
|
39
|
+
return [] if feature_indices.empty?
|
|
40
|
+
|
|
41
|
+
# Get feature list
|
|
42
|
+
feature_list_data = table_data[(feature_list_offset - 10)..]
|
|
43
|
+
return [] if feature_list_data.nil? || feature_list_data.empty?
|
|
44
|
+
|
|
45
|
+
feature_list = LayoutCommon::FeatureList.read(feature_list_data)
|
|
46
|
+
|
|
47
|
+
# Collect features referenced by the script
|
|
48
|
+
features = []
|
|
49
|
+
feature_indices.each do |idx|
|
|
50
|
+
next if idx >= feature_list.feature_count
|
|
51
|
+
|
|
52
|
+
features << feature_list.feature_records[idx].feature_tag
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
features.uniq
|
|
56
|
+
rescue StandardError
|
|
57
|
+
[]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Get feature indices for a given script
|
|
63
|
+
# @param script_tag [String] 4-character script tag
|
|
64
|
+
# @return [Array<Integer>] Array of feature indices
|
|
65
|
+
def feature_indices_for_script(script_tag)
|
|
66
|
+
return [] if script_list_offset.zero?
|
|
67
|
+
|
|
68
|
+
script_list_data = table_data[(script_list_offset - 10)..]
|
|
69
|
+
return [] if script_list_data.nil? || script_list_data.empty?
|
|
70
|
+
|
|
71
|
+
script_list = LayoutCommon::ScriptList.read(script_list_data)
|
|
72
|
+
|
|
73
|
+
# Find the script record
|
|
74
|
+
script_record = script_list.script_records.find do |rec|
|
|
75
|
+
rec.script_tag == script_tag
|
|
76
|
+
end
|
|
77
|
+
return [] unless script_record
|
|
78
|
+
|
|
79
|
+
# Parse the script table at the offset
|
|
80
|
+
script_offset = script_record.script_offset
|
|
81
|
+
script_data = script_list_data[script_offset..]
|
|
82
|
+
return [] if script_data.nil? || script_data.empty?
|
|
83
|
+
|
|
84
|
+
script = LayoutCommon::Script.read(script_data)
|
|
85
|
+
|
|
86
|
+
# Get the default LangSys if it exists
|
|
87
|
+
feature_indices = []
|
|
88
|
+
if script.default_lang_sys_offset != 0
|
|
89
|
+
lang_sys_data = script_data[script.default_lang_sys_offset..]
|
|
90
|
+
if lang_sys_data && !lang_sys_data.empty?
|
|
91
|
+
lang_sys = LayoutCommon::LangSys.read(lang_sys_data)
|
|
92
|
+
feature_indices.concat(lang_sys.feature_indices)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Also collect from all LangSys records
|
|
97
|
+
script.lang_sys_records.each do |lang_sys_rec|
|
|
98
|
+
lang_sys_data = script_data[lang_sys_rec.lang_sys_offset..]
|
|
99
|
+
next if lang_sys_data.nil? || lang_sys_data.empty?
|
|
100
|
+
|
|
101
|
+
lang_sys = LayoutCommon::LangSys.read(lang_sys_data)
|
|
102
|
+
feature_indices.concat(lang_sys.feature_indices)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
feature_indices.uniq
|
|
106
|
+
rescue StandardError
|
|
107
|
+
[]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../binary/base_record"
|
|
4
|
+
|
|
5
|
+
module Fontisan
|
|
6
|
+
module Tables
|
|
7
|
+
# BinData structure for the 'head' (Font Header) table
|
|
8
|
+
#
|
|
9
|
+
# The head table contains global information about the font, including
|
|
10
|
+
# metadata about the font file, bounding box, and indexing information.
|
|
11
|
+
#
|
|
12
|
+
# Reference: OpenType specification, head table
|
|
13
|
+
#
|
|
14
|
+
# @example Reading a head table
|
|
15
|
+
# data = File.binread("font.ttf", 54, head_offset)
|
|
16
|
+
# head = Fontisan::Tables::Head.read(data)
|
|
17
|
+
# puts head.units_per_em # => 2048
|
|
18
|
+
# puts head.version_number # => 1.0
|
|
19
|
+
class Head < Binary::BaseRecord
|
|
20
|
+
# Magic number that must be present in the head table
|
|
21
|
+
MAGIC_NUMBER = 0x5F0F3CF5
|
|
22
|
+
|
|
23
|
+
# Version as 16.16 fixed-point (stored as int32)
|
|
24
|
+
int32 :version_raw
|
|
25
|
+
|
|
26
|
+
# Font revision as 16.16 fixed-point (stored as int32)
|
|
27
|
+
int32 :font_revision_raw
|
|
28
|
+
|
|
29
|
+
uint32 :checksum_adjustment
|
|
30
|
+
uint32 :magic_number
|
|
31
|
+
uint16 :flags
|
|
32
|
+
uint16 :units_per_em
|
|
33
|
+
|
|
34
|
+
# Created date as 64-bit signed integer (seconds since 1904-01-01)
|
|
35
|
+
int64 :created_raw
|
|
36
|
+
|
|
37
|
+
# Modified date as 64-bit signed integer (seconds since 1904-01-01)
|
|
38
|
+
int64 :modified_raw
|
|
39
|
+
|
|
40
|
+
int16 :x_min
|
|
41
|
+
int16 :y_min
|
|
42
|
+
int16 :x_max
|
|
43
|
+
int16 :y_max
|
|
44
|
+
uint16 :mac_style
|
|
45
|
+
uint16 :lowest_rec_ppem
|
|
46
|
+
int16 :font_direction_hint
|
|
47
|
+
int16 :index_to_loc_format
|
|
48
|
+
int16 :glyph_data_format
|
|
49
|
+
|
|
50
|
+
# Convert version from fixed-point to float
|
|
51
|
+
#
|
|
52
|
+
# @return [Float] Version number (e.g., 1.0)
|
|
53
|
+
def version
|
|
54
|
+
fixed_to_float(version_raw)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Convert font revision from fixed-point to float
|
|
58
|
+
#
|
|
59
|
+
# @return [Float] Font revision number
|
|
60
|
+
def font_revision
|
|
61
|
+
fixed_to_float(font_revision_raw)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Convert created timestamp to Time object
|
|
65
|
+
#
|
|
66
|
+
# @return [Time] Creation time
|
|
67
|
+
def created
|
|
68
|
+
longdatetime_to_time(created_raw)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Convert modified timestamp to Time object
|
|
72
|
+
#
|
|
73
|
+
# @return [Time] Modification time
|
|
74
|
+
def modified
|
|
75
|
+
longdatetime_to_time(modified_raw)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Validate that the magic number is correct
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean] True if magic number is valid
|
|
81
|
+
def valid?
|
|
82
|
+
magic_number == MAGIC_NUMBER
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Validate magic number and raise error if invalid
|
|
86
|
+
#
|
|
87
|
+
# @raise [Fontisan::CorruptedTableError] If magic number is invalid
|
|
88
|
+
def validate_magic_number!
|
|
89
|
+
return if valid?
|
|
90
|
+
|
|
91
|
+
message = "Invalid magic number in head table: " \
|
|
92
|
+
"expected 0x#{MAGIC_NUMBER.to_s(16).upcase}, " \
|
|
93
|
+
"got 0x#{magic_number.to_s(16).upcase}"
|
|
94
|
+
error = Fontisan::CorruptedTableError.new(message)
|
|
95
|
+
error.set_backtrace(caller)
|
|
96
|
+
Kernel.raise(error)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Alias for backward compatibility
|
|
100
|
+
alias validate! validate_magic_number!
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Convert LONGDATETIME to Ruby Time
|
|
105
|
+
#
|
|
106
|
+
# @param seconds [Integer] Seconds since 1904-01-01 00:00:00
|
|
107
|
+
# @return [Time] Ruby Time object
|
|
108
|
+
def longdatetime_to_time(seconds)
|
|
109
|
+
# Difference between 1904 and 1970 (Unix epoch) is 2082844800 seconds
|
|
110
|
+
Time.at(seconds - 2_082_844_800)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontisan
|
|
4
|
+
module Tables
|
|
5
|
+
# Common structures shared between GSUB and GPOS tables
|
|
6
|
+
module LayoutCommon
|
|
7
|
+
# ScriptRecord structure
|
|
8
|
+
class ScriptRecord < Binary::BaseRecord
|
|
9
|
+
string :script_tag, length: 4
|
|
10
|
+
uint16 :script_offset
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# ScriptList table
|
|
14
|
+
class ScriptList < Binary::BaseRecord
|
|
15
|
+
uint16 :script_count
|
|
16
|
+
array :script_records, type: ScriptRecord,
|
|
17
|
+
initial_length: -> { script_count }
|
|
18
|
+
|
|
19
|
+
def script_tags
|
|
20
|
+
script_records.map(&:script_tag)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# LangSysRecord structure
|
|
25
|
+
class LangSysRecord < Binary::BaseRecord
|
|
26
|
+
string :lang_sys_tag, length: 4
|
|
27
|
+
uint16 :lang_sys_offset
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Script table
|
|
31
|
+
class Script < Binary::BaseRecord
|
|
32
|
+
uint16 :default_lang_sys_offset
|
|
33
|
+
uint16 :lang_sys_count
|
|
34
|
+
array :lang_sys_records, type: LangSysRecord,
|
|
35
|
+
initial_length: -> { lang_sys_count }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# LangSys table
|
|
39
|
+
class LangSys < Binary::BaseRecord
|
|
40
|
+
uint16 :lookup_order_offset # Reserved, set to NULL
|
|
41
|
+
uint16 :required_feature_index
|
|
42
|
+
uint16 :feature_index_count
|
|
43
|
+
array :feature_indices, type: :uint16,
|
|
44
|
+
initial_length: -> { feature_index_count }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# FeatureRecord structure
|
|
48
|
+
class FeatureRecord < Binary::BaseRecord
|
|
49
|
+
string :feature_tag, length: 4
|
|
50
|
+
uint16 :feature_offset
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# FeatureList table
|
|
54
|
+
class FeatureList < Binary::BaseRecord
|
|
55
|
+
uint16 :feature_count
|
|
56
|
+
array :feature_records, type: FeatureRecord,
|
|
57
|
+
initial_length: -> { feature_count }
|
|
58
|
+
|
|
59
|
+
def feature_tags
|
|
60
|
+
feature_records.map(&:feature_tag)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Feature table
|
|
65
|
+
class Feature < Binary::BaseRecord
|
|
66
|
+
uint16 :feature_params_offset # Reserved, set to NULL
|
|
67
|
+
uint16 :lookup_index_count
|
|
68
|
+
array :lookup_list_indices, type: :uint16,
|
|
69
|
+
initial_length: -> { lookup_index_count }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|