fontisan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/.rubocop_todo.yml +217 -0
  5. data/Gemfile +15 -0
  6. data/LICENSE +24 -0
  7. data/README.adoc +984 -0
  8. data/Rakefile +95 -0
  9. data/exe/fontisan +7 -0
  10. data/fontisan.gemspec +44 -0
  11. data/lib/fontisan/binary/base_record.rb +57 -0
  12. data/lib/fontisan/binary/structures.rb +84 -0
  13. data/lib/fontisan/cli.rb +192 -0
  14. data/lib/fontisan/commands/base_command.rb +82 -0
  15. data/lib/fontisan/commands/dump_table_command.rb +71 -0
  16. data/lib/fontisan/commands/features_command.rb +94 -0
  17. data/lib/fontisan/commands/glyphs_command.rb +50 -0
  18. data/lib/fontisan/commands/info_command.rb +120 -0
  19. data/lib/fontisan/commands/optical_size_command.rb +41 -0
  20. data/lib/fontisan/commands/scripts_command.rb +59 -0
  21. data/lib/fontisan/commands/tables_command.rb +52 -0
  22. data/lib/fontisan/commands/unicode_command.rb +76 -0
  23. data/lib/fontisan/commands/variable_command.rb +61 -0
  24. data/lib/fontisan/config/features.yml +143 -0
  25. data/lib/fontisan/config/scripts.yml +42 -0
  26. data/lib/fontisan/constants.rb +78 -0
  27. data/lib/fontisan/error.rb +15 -0
  28. data/lib/fontisan/font_loader.rb +109 -0
  29. data/lib/fontisan/formatters/text_formatter.rb +314 -0
  30. data/lib/fontisan/models/all_scripts_features_info.rb +21 -0
  31. data/lib/fontisan/models/features_info.rb +42 -0
  32. data/lib/fontisan/models/font_info.rb +99 -0
  33. data/lib/fontisan/models/glyph_info.rb +26 -0
  34. data/lib/fontisan/models/optical_size_info.rb +33 -0
  35. data/lib/fontisan/models/scripts_info.rb +39 -0
  36. data/lib/fontisan/models/table_info.rb +55 -0
  37. data/lib/fontisan/models/unicode_mappings.rb +42 -0
  38. data/lib/fontisan/models/variable_font_info.rb +82 -0
  39. data/lib/fontisan/open_type_collection.rb +97 -0
  40. data/lib/fontisan/open_type_font.rb +292 -0
  41. data/lib/fontisan/parsers/tag.rb +77 -0
  42. data/lib/fontisan/tables/cmap.rb +284 -0
  43. data/lib/fontisan/tables/fvar.rb +157 -0
  44. data/lib/fontisan/tables/gpos.rb +111 -0
  45. data/lib/fontisan/tables/gsub.rb +111 -0
  46. data/lib/fontisan/tables/head.rb +114 -0
  47. data/lib/fontisan/tables/layout_common.rb +73 -0
  48. data/lib/fontisan/tables/name.rb +188 -0
  49. data/lib/fontisan/tables/os2.rb +175 -0
  50. data/lib/fontisan/tables/post.rb +148 -0
  51. data/lib/fontisan/true_type_collection.rb +98 -0
  52. data/lib/fontisan/true_type_font.rb +313 -0
  53. data/lib/fontisan/utilities/checksum_calculator.rb +89 -0
  54. data/lib/fontisan/version.rb +5 -0
  55. data/lib/fontisan.rb +80 -0
  56. metadata +150 -0
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # FontInfo model represents comprehensive font metadata
8
+ # extracted from various font tables (name, head, OS/2, etc.)
9
+ #
10
+ # This model provides a unified interface for accessing font information
11
+ # and supports serialization to YAML and JSON formats through lutaml-model.
12
+ class FontInfo < Lutaml::Model::Serializable
13
+ attribute :font_format, :string
14
+ attribute :is_variable, Lutaml::Model::Type::Boolean
15
+ attribute :family_name, :string
16
+ attribute :subfamily_name, :string
17
+ attribute :full_name, :string
18
+ attribute :postscript_name, :string
19
+ attribute :postscript_cid_name, :string
20
+ attribute :preferred_family, :string
21
+ attribute :preferred_subfamily, :string
22
+ attribute :mac_font_menu_name, :string
23
+ attribute :version, :string
24
+ attribute :unique_id, :string
25
+ attribute :description, :string
26
+ attribute :designer, :string
27
+ attribute :designer_url, :string
28
+ attribute :manufacturer, :string
29
+ attribute :vendor_url, :string
30
+ attribute :vendor_id, :string
31
+ attribute :trademark, :string
32
+ attribute :copyright, :string
33
+ attribute :license_description, :string
34
+ attribute :license_url, :string
35
+ attribute :sample_text, :string
36
+ attribute :font_revision, :float
37
+ attribute :permissions, :string
38
+ attribute :units_per_em, :integer
39
+
40
+ json do
41
+ map "font_format", to: :font_format
42
+ map "is_variable", to: :is_variable
43
+ map "family_name", to: :family_name
44
+ map "subfamily_name", to: :subfamily_name
45
+ map "full_name", to: :full_name
46
+ map "postscript_name", to: :postscript_name
47
+ map "postscript_cid_name", to: :postscript_cid_name
48
+ map "preferred_family", to: :preferred_family
49
+ map "preferred_subfamily", to: :preferred_subfamily
50
+ map "mac_font_menu_name", to: :mac_font_menu_name
51
+ map "version", to: :version
52
+ map "unique_id", to: :unique_id
53
+ map "description", to: :description
54
+ map "designer", to: :designer
55
+ map "designer_url", to: :designer_url
56
+ map "manufacturer", to: :manufacturer
57
+ map "vendor_url", to: :vendor_url
58
+ map "vendor_id", to: :vendor_id
59
+ map "trademark", to: :trademark
60
+ map "copyright", to: :copyright
61
+ map "license_description", to: :license_description
62
+ map "license_url", to: :license_url
63
+ map "sample_text", to: :sample_text
64
+ map "font_revision", to: :font_revision
65
+ map "permissions", to: :permissions
66
+ map "units_per_em", to: :units_per_em
67
+ end
68
+
69
+ yaml do
70
+ map "font_format", to: :font_format
71
+ map "is_variable", to: :is_variable
72
+ map "family_name", to: :family_name
73
+ map "subfamily_name", to: :subfamily_name
74
+ map "full_name", to: :full_name
75
+ map "postscript_name", to: :postscript_name
76
+ map "postscript_cid_name", to: :postscript_cid_name
77
+ map "preferred_family", to: :preferred_family
78
+ map "preferred_subfamily", to: :preferred_subfamily
79
+ map "mac_font_menu_name", to: :mac_font_menu_name
80
+ map "version", to: :version
81
+ map "unique_id", to: :unique_id
82
+ map "description", to: :description
83
+ map "designer", to: :designer
84
+ map "designer_url", to: :designer_url
85
+ map "manufacturer", to: :manufacturer
86
+ map "vendor_url", to: :vendor_url
87
+ map "vendor_id", to: :vendor_id
88
+ map "trademark", to: :trademark
89
+ map "copyright", to: :copyright
90
+ map "license_description", to: :license_description
91
+ map "license_url", to: :license_url
92
+ map "sample_text", to: :sample_text
93
+ map "font_revision", to: :font_revision
94
+ map "permissions", to: :permissions
95
+ map "units_per_em", to: :units_per_em
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Model for glyph information
8
+ class GlyphInfo < Lutaml::Model::Serializable
9
+ attribute :glyph_count, :integer
10
+ attribute :glyph_names, :string, collection: true
11
+ attribute :source, :string
12
+
13
+ json do
14
+ map "glyph_count", to: :glyph_count
15
+ map "glyph_names", to: :glyph_names
16
+ map "source", to: :source
17
+ end
18
+
19
+ yaml do
20
+ map "glyph_count", to: :glyph_count
21
+ map "glyph_names", to: :glyph_names
22
+ map "source", to: :source
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # OpticalSizeInfo model represents optical size information from a font
8
+ #
9
+ # Optical size information indicates the design size range for which a font
10
+ # is optimized. This can come from the OS/2 table (version 5+) or from the
11
+ # GPOS 'size' feature.
12
+ class OpticalSizeInfo < Lutaml::Model::Serializable
13
+ attribute :has_optical_size, Lutaml::Model::Type::Boolean
14
+ attribute :source, :string
15
+ attribute :lower_point_size, :float
16
+ attribute :upper_point_size, :float
17
+
18
+ json do
19
+ map "has_optical_size", to: :has_optical_size
20
+ map "source", to: :source
21
+ map "lower_point_size", to: :lower_point_size
22
+ map "upper_point_size", to: :upper_point_size
23
+ end
24
+
25
+ yaml do
26
+ map "has_optical_size", to: :has_optical_size
27
+ map "source", to: :source
28
+ map "lower_point_size", to: :lower_point_size
29
+ map "upper_point_size", to: :upper_point_size
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Represents a single script record
8
+ class ScriptRecord < 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 scripts information from GSUB/GPOS tables
24
+ class ScriptsInfo < Lutaml::Model::Serializable
25
+ attribute :script_count, :integer
26
+ attribute :scripts, ScriptRecord, collection: true
27
+
28
+ json do
29
+ map "script_count", to: :script_count
30
+ map "scripts", to: :scripts
31
+ end
32
+
33
+ yaml do
34
+ map "script_count", to: :script_count
35
+ map "scripts", to: :scripts
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # TableEntry represents a single table directory entry in a font file
8
+ #
9
+ # Each entry contains metadata about a font table including its tag,
10
+ # length, offset within the file, and checksum for validation.
11
+ class TableEntry < Lutaml::Model::Serializable
12
+ attribute :tag, :string
13
+ attribute :length, :integer
14
+ attribute :offset, :integer
15
+ attribute :checksum, :integer
16
+
17
+ json do
18
+ map "tag", to: :tag
19
+ map "length", to: :length
20
+ map "offset", to: :offset
21
+ map "checksum", to: :checksum
22
+ end
23
+
24
+ yaml do
25
+ map "tag", to: :tag
26
+ map "length", to: :length
27
+ map "offset", to: :offset
28
+ map "checksum", to: :checksum
29
+ end
30
+ end
31
+
32
+ # TableInfo represents the table directory information from a font file
33
+ #
34
+ # This model contains the SFNT version identifier, the number of tables,
35
+ # and a collection of TableEntry objects representing each table in the font.
36
+ # It supports serialization to YAML and JSON formats through lutaml-model.
37
+ class TableInfo < Lutaml::Model::Serializable
38
+ attribute :sfnt_version, :string
39
+ attribute :num_tables, :integer
40
+ attribute :tables, TableEntry, collection: true
41
+
42
+ json do
43
+ map "sfnt_version", to: :sfnt_version
44
+ map "num_tables", to: :num_tables
45
+ map "tables", to: :tables
46
+ end
47
+
48
+ yaml do
49
+ map "sfnt_version", to: :sfnt_version
50
+ map "num_tables", to: :num_tables
51
+ map "tables", to: :tables
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Model for a single Unicode to glyph mapping
8
+ class UnicodeMapping < Lutaml::Model::Serializable
9
+ attribute :codepoint, :string
10
+ attribute :glyph_index, :integer
11
+ attribute :glyph_name, :string
12
+
13
+ json do
14
+ map "codepoint", to: :codepoint
15
+ map "glyph_index", to: :glyph_index
16
+ map "glyph_name", to: :glyph_name
17
+ end
18
+
19
+ yaml do
20
+ map "codepoint", to: :codepoint
21
+ map "glyph_index", to: :glyph_index
22
+ map "glyph_name", to: :glyph_name
23
+ end
24
+ end
25
+
26
+ # Model for collection of Unicode mappings
27
+ class UnicodeMappings < Lutaml::Model::Serializable
28
+ attribute :count, :integer
29
+ attribute :mappings, UnicodeMapping, collection: true
30
+
31
+ json do
32
+ map "count", to: :count
33
+ map "mappings", to: :mappings
34
+ end
35
+
36
+ yaml do
37
+ map "count", to: :count
38
+ map "mappings", to: :mappings
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # AxisInfo model represents a single variation axis in a variable font
8
+ #
9
+ # Each axis defines a design dimension along which the font can vary,
10
+ # such as weight (wght), width (wdth), italic (ital), or slant (slnt).
11
+ class AxisInfo < Lutaml::Model::Serializable
12
+ attribute :tag, :string
13
+ attribute :name, :string
14
+ attribute :min_value, :float
15
+ attribute :default_value, :float
16
+ attribute :max_value, :float
17
+
18
+ json do
19
+ map "tag", to: :tag
20
+ map "name", to: :name
21
+ map "min_value", to: :min_value
22
+ map "default_value", to: :default_value
23
+ map "max_value", to: :max_value
24
+ end
25
+
26
+ yaml do
27
+ map "tag", to: :tag
28
+ map "name", to: :name
29
+ map "min_value", to: :min_value
30
+ map "default_value", to: :default_value
31
+ map "max_value", to: :max_value
32
+ end
33
+ end
34
+
35
+ # InstanceInfo model represents a named instance in a variable font
36
+ #
37
+ # Each instance defines a predefined combination of axis values,
38
+ # representing a named style/weight/width combination.
39
+ class InstanceInfo < Lutaml::Model::Serializable
40
+ attribute :name, :string
41
+ attribute :coordinates, :float, collection: true
42
+
43
+ json do
44
+ map "name", to: :name
45
+ map "coordinates", to: :coordinates
46
+ end
47
+
48
+ yaml do
49
+ map "name", to: :name
50
+ map "coordinates", to: :coordinates
51
+ end
52
+ end
53
+
54
+ # VariableFontInfo model represents comprehensive variable font metadata
55
+ #
56
+ # This model provides information about variation axes and named instances
57
+ # for variable fonts (OpenType Font Variations).
58
+ class VariableFontInfo < Lutaml::Model::Serializable
59
+ attribute :is_variable, Lutaml::Model::Type::Boolean
60
+ attribute :axis_count, :integer
61
+ attribute :instance_count, :integer
62
+ attribute :axes, AxisInfo, collection: true
63
+ attribute :instances, InstanceInfo, collection: true
64
+
65
+ json do
66
+ map "is_variable", to: :is_variable
67
+ map "axis_count", to: :axis_count
68
+ map "instance_count", to: :instance_count
69
+ map "axes", to: :axes
70
+ map "instances", to: :instances
71
+ end
72
+
73
+ yaml do
74
+ map "is_variable", to: :is_variable
75
+ map "axis_count", to: :axis_count
76
+ map "instance_count", to: :instance_count
77
+ map "axes", to: :axes
78
+ map "instances", to: :instances
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bindata"
4
+ require_relative "constants"
5
+
6
+ module Fontisan
7
+ # OpenType Collection domain object using BinData
8
+ #
9
+ # Represents a complete OpenType Collection file (OTC) using BinData's declarative
10
+ # DSL for binary structure definition. Parallel to TrueTypeCollection but for OpenType fonts.
11
+ #
12
+ # @example Reading and extracting fonts
13
+ # File.open("fonts.otc", "rb") do |io|
14
+ # otc = OpenTypeCollection.read(io)
15
+ # puts otc.num_fonts # => 4
16
+ # fonts = otc.extract_fonts(io) # => [OpenTypeFont, OpenTypeFont, ...]
17
+ # end
18
+ class OpenTypeCollection < BinData::Record
19
+ endian :big
20
+
21
+ string :tag, length: 4, assert: "ttcf"
22
+ uint16 :major_version
23
+ uint16 :minor_version
24
+ uint32 :num_fonts
25
+ array :font_offsets, type: :uint32, initial_length: :num_fonts
26
+
27
+ # Read OpenType Collection from a file
28
+ #
29
+ # @param path [String] Path to the OTC file
30
+ # @return [OpenTypeCollection] A new instance
31
+ # @raise [ArgumentError] if path is nil or empty
32
+ # @raise [Errno::ENOENT] if file does not exist
33
+ # @raise [RuntimeError] if file format is invalid
34
+ def self.from_file(path)
35
+ if path.nil? || path.to_s.empty?
36
+ raise ArgumentError,
37
+ "path cannot be nil or empty"
38
+ end
39
+ raise Errno::ENOENT, "File not found: #{path}" unless File.exist?(path)
40
+
41
+ File.open(path, "rb") { |io| read(io) }
42
+ rescue BinData::ValidityError => e
43
+ raise "Invalid OTC file: #{e.message}"
44
+ rescue EOFError => e
45
+ raise "Invalid OTC file: unexpected end of file - #{e.message}"
46
+ end
47
+
48
+ # Extract fonts as OpenTypeFont objects
49
+ #
50
+ # Reads each font from the OTC file and returns them as OpenTypeFont objects.
51
+ #
52
+ # @param io [IO] Open file handle to read fonts from
53
+ # @return [Array<OpenTypeFont>] Array of font objects
54
+ def extract_fonts(io)
55
+ require_relative "open_type_font"
56
+
57
+ font_offsets.map do |offset|
58
+ OpenTypeFont.from_collection(io, offset)
59
+ end
60
+ end
61
+
62
+ # Get a single font from the collection
63
+ #
64
+ # @param index [Integer] Index of the font (0-based)
65
+ # @param io [IO] Open file handle
66
+ # @return [OpenTypeFont, nil] Font object or nil if index out of range
67
+ def font(index, io)
68
+ return nil if index >= num_fonts
69
+
70
+ require_relative "open_type_font"
71
+ OpenTypeFont.from_collection(io, font_offsets[index])
72
+ end
73
+
74
+ # Get font count
75
+ #
76
+ # @return [Integer] Number of fonts in collection
77
+ def font_count
78
+ num_fonts
79
+ end
80
+
81
+ # Validate format correctness
82
+ #
83
+ # @return [Boolean] true if the format is valid, false otherwise
84
+ def valid?
85
+ tag == Constants::TTC_TAG && num_fonts.positive? && font_offsets.length == num_fonts
86
+ rescue StandardError
87
+ false
88
+ end
89
+
90
+ # Get the OTC version as a single integer
91
+ #
92
+ # @return [Integer] Version number (e.g., 0x00010000 for version 1.0)
93
+ def version
94
+ (major_version << 16) | minor_version
95
+ end
96
+ end
97
+ end