fontisan 0.2.2 → 0.2.4

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +94 -48
  3. data/README.adoc +293 -3
  4. data/Rakefile +20 -7
  5. data/lib/fontisan/base_collection.rb +296 -0
  6. data/lib/fontisan/commands/base_command.rb +2 -19
  7. data/lib/fontisan/commands/convert_command.rb +16 -13
  8. data/lib/fontisan/commands/info_command.rb +156 -50
  9. data/lib/fontisan/config/conversion_matrix.yml +58 -20
  10. data/lib/fontisan/converters/outline_converter.rb +6 -3
  11. data/lib/fontisan/converters/svg_generator.rb +45 -0
  12. data/lib/fontisan/converters/woff2_encoder.rb +106 -13
  13. data/lib/fontisan/font_loader.rb +109 -26
  14. data/lib/fontisan/formatters/text_formatter.rb +72 -19
  15. data/lib/fontisan/models/bitmap_glyph.rb +123 -0
  16. data/lib/fontisan/models/bitmap_strike.rb +94 -0
  17. data/lib/fontisan/models/collection_brief_info.rb +6 -0
  18. data/lib/fontisan/models/collection_info.rb +6 -1
  19. data/lib/fontisan/models/color_glyph.rb +57 -0
  20. data/lib/fontisan/models/color_layer.rb +53 -0
  21. data/lib/fontisan/models/color_palette.rb +60 -0
  22. data/lib/fontisan/models/font_info.rb +26 -0
  23. data/lib/fontisan/models/svg_glyph.rb +89 -0
  24. data/lib/fontisan/open_type_collection.rb +17 -220
  25. data/lib/fontisan/open_type_font.rb +6 -0
  26. data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
  27. data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
  28. data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
  29. data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
  30. data/lib/fontisan/pipeline/output_writer.rb +2 -2
  31. data/lib/fontisan/tables/cbdt.rb +169 -0
  32. data/lib/fontisan/tables/cblc.rb +290 -0
  33. data/lib/fontisan/tables/cff.rb +6 -12
  34. data/lib/fontisan/tables/colr.rb +291 -0
  35. data/lib/fontisan/tables/cpal.rb +281 -0
  36. data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
  37. data/lib/fontisan/tables/sbix.rb +379 -0
  38. data/lib/fontisan/tables/svg.rb +301 -0
  39. data/lib/fontisan/true_type_collection.rb +29 -113
  40. data/lib/fontisan/true_type_font.rb +6 -0
  41. data/lib/fontisan/validation/woff2_header_validator.rb +278 -0
  42. data/lib/fontisan/validation/woff2_table_validator.rb +270 -0
  43. data/lib/fontisan/validation/woff2_validator.rb +248 -0
  44. data/lib/fontisan/version.rb +1 -1
  45. data/lib/fontisan/woff2/directory.rb +40 -11
  46. data/lib/fontisan/woff2/table_transformer.rb +506 -73
  47. data/lib/fontisan/woff2_font.rb +29 -9
  48. data/lib/fontisan/woff_font.rb +17 -4
  49. data/lib/fontisan.rb +12 -0
  50. metadata +18 -2
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Bitmap strike representation model
8
+ #
9
+ # Represents a bitmap strike (size) from the CBLC table. Each strike contains
10
+ # bitmap glyphs at a specific ppem (pixels per em) size.
11
+ #
12
+ # This model uses lutaml-model for structured serialization to YAML/JSON/XML.
13
+ #
14
+ # @example Creating a bitmap strike
15
+ # strike = BitmapStrike.new
16
+ # strike.ppem = 16
17
+ # strike.start_glyph_id = 10
18
+ # strike.end_glyph_id = 100
19
+ # strike.bit_depth = 32
20
+ #
21
+ # @example Serializing to JSON
22
+ # json = strike.to_json
23
+ # # {
24
+ # # "ppem": 16,
25
+ # # "start_glyph_id": 10,
26
+ # # "end_glyph_id": 100,
27
+ # # "bit_depth": 32
28
+ # # }
29
+ class BitmapStrike < Lutaml::Model::Serializable
30
+ # @!attribute ppem
31
+ # @return [Integer] Pixels per em (square pixels)
32
+ attribute :ppem, :integer
33
+
34
+ # @!attribute start_glyph_id
35
+ # @return [Integer] First glyph ID in this strike
36
+ attribute :start_glyph_id, :integer
37
+
38
+ # @!attribute end_glyph_id
39
+ # @return [Integer] Last glyph ID in this strike
40
+ attribute :end_glyph_id, :integer
41
+
42
+ # @!attribute bit_depth
43
+ # @return [Integer] Bit depth (1, 2, 4, 8, or 32)
44
+ attribute :bit_depth, :integer
45
+
46
+ # @!attribute num_glyphs
47
+ # @return [Integer] Number of glyphs in this strike
48
+ attribute :num_glyphs, :integer
49
+
50
+ # Get glyph IDs covered by this strike
51
+ #
52
+ # @return [Range] Range of glyph IDs
53
+ def glyph_range
54
+ start_glyph_id..end_glyph_id
55
+ end
56
+
57
+ # Check if this strike covers a specific glyph ID
58
+ #
59
+ # @param glyph_id [Integer] Glyph ID to check
60
+ # @return [Boolean] True if glyph is in range
61
+ def includes_glyph?(glyph_id)
62
+ glyph_range.include?(glyph_id)
63
+ end
64
+
65
+ # Get the color depth description
66
+ #
67
+ # @return [String] Human-readable color depth
68
+ def color_depth
69
+ case bit_depth
70
+ when 1 then "1-bit (monochrome)"
71
+ when 2 then "2-bit (4 colors)"
72
+ when 4 then "4-bit (16 colors)"
73
+ when 8 then "8-bit (256 colors)"
74
+ when 32 then "32-bit (full color with alpha)"
75
+ else "#{bit_depth}-bit"
76
+ end
77
+ end
78
+
79
+ # Check if this is a color strike (32-bit)
80
+ #
81
+ # @return [Boolean] True if 32-bit color
82
+ def color?
83
+ bit_depth == 32
84
+ end
85
+
86
+ # Check if this is a monochrome strike (1-bit)
87
+ #
88
+ # @return [Boolean] True if 1-bit monochrome
89
+ def monochrome?
90
+ bit_depth == 1
91
+ end
92
+ end
93
+ end
94
+ end
@@ -13,16 +13,22 @@ module Fontisan
13
13
  # @example Creating collection brief info
14
14
  # info = CollectionBriefInfo.new(
15
15
  # collection_path: "fonts.ttc",
16
+ # collection_type: "TTC",
17
+ # collection_version: "1.0",
16
18
  # num_fonts: 3,
17
19
  # fonts: [font_info1, font_info2, font_info3]
18
20
  # )
19
21
  class CollectionBriefInfo < Lutaml::Model::Serializable
20
22
  attribute :collection_path, :string
23
+ attribute :collection_type, :string
24
+ attribute :collection_version, :string
21
25
  attribute :num_fonts, :integer
22
26
  attribute :fonts, FontInfo, collection: true
23
27
 
24
28
  key_value do
25
29
  map "collection_path", to: :collection_path
30
+ map "collection_type", to: :collection_type
31
+ map "collection_version", to: :collection_version
26
32
  map "num_fonts", to: :num_fonts
27
33
  map "fonts", to: :fonts
28
34
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "lutaml/model"
4
4
  require_relative "table_sharing_info"
5
+ require_relative "font_info"
5
6
 
6
7
  module Fontisan
7
8
  module Models
@@ -20,7 +21,8 @@ module Fontisan
20
21
  # num_fonts: 6,
21
22
  # font_offsets: [48, 380, 712, 1044, 1376, 1676],
22
23
  # file_size_bytes: 2240000,
23
- # table_sharing: table_sharing_obj
24
+ # table_sharing: table_sharing_obj,
25
+ # fonts: [font_info1, font_info2, ...]
24
26
  # )
25
27
  class CollectionInfo < Lutaml::Model::Serializable
26
28
  attribute :collection_path, :string
@@ -32,6 +34,7 @@ module Fontisan
32
34
  attribute :font_offsets, :integer, collection: true
33
35
  attribute :file_size_bytes, :integer
34
36
  attribute :table_sharing, TableSharingInfo
37
+ attribute :fonts, FontInfo, collection: true
35
38
 
36
39
  yaml do
37
40
  map "collection_path", to: :collection_path
@@ -43,6 +46,7 @@ module Fontisan
43
46
  map "font_offsets", to: :font_offsets
44
47
  map "file_size_bytes", to: :file_size_bytes
45
48
  map "table_sharing", to: :table_sharing
49
+ map "fonts", to: :fonts
46
50
  end
47
51
 
48
52
  json do
@@ -55,6 +59,7 @@ module Fontisan
55
59
  map "font_offsets", to: :font_offsets
56
60
  map "file_size_bytes", to: :file_size_bytes
57
61
  map "table_sharing", to: :table_sharing
62
+ map "fonts", to: :fonts
58
63
  end
59
64
 
60
65
  # Get version as a formatted string
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+ require_relative "color_layer"
5
+
6
+ module Fontisan
7
+ module Models
8
+ # Color glyph information model
9
+ #
10
+ # Represents a complete color glyph from the COLR table, containing
11
+ # multiple layers that are rendered in order to create the final
12
+ # multi-colored glyph.
13
+ #
14
+ # This model uses lutaml-model for structured serialization to YAML/JSON/XML.
15
+ #
16
+ # @example Creating a color glyph
17
+ # glyph = ColorGlyph.new
18
+ # glyph.glyph_id = 100
19
+ # glyph.num_layers = 3
20
+ # glyph.layers = [layer1, layer2, layer3]
21
+ #
22
+ # @example Serializing to JSON
23
+ # json = glyph.to_json
24
+ # # {
25
+ # # "glyph_id": 100,
26
+ # # "num_layers": 3,
27
+ # # "layers": [...]
28
+ # # }
29
+ class ColorGlyph < Lutaml::Model::Serializable
30
+ # @!attribute glyph_id
31
+ # @return [Integer] Base glyph ID
32
+ attribute :glyph_id, :integer
33
+
34
+ # @!attribute num_layers
35
+ # @return [Integer] Number of color layers
36
+ attribute :num_layers, :integer
37
+
38
+ # @!attribute layers
39
+ # @return [Array<ColorLayer>] Array of color layers
40
+ attribute :layers, ColorLayer, collection: true
41
+
42
+ # Check if glyph has color layers
43
+ #
44
+ # @return [Boolean] True if glyph has layers
45
+ def has_layers?
46
+ num_layers&.positive? || false
47
+ end
48
+
49
+ # Check if glyph is empty
50
+ #
51
+ # @return [Boolean] True if no layers
52
+ def empty?
53
+ !has_layers?
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Color layer information model
8
+ #
9
+ # Represents a single color layer in a COLR glyph. Each layer specifies
10
+ # a glyph ID to render and the palette index for its color.
11
+ #
12
+ # This model uses lutaml-model for structured serialization to YAML/JSON/XML.
13
+ #
14
+ # @example Creating a color layer
15
+ # layer = ColorLayer.new
16
+ # layer.glyph_id = 42
17
+ # layer.palette_index = 2
18
+ # layer.color = "#FF0000FF"
19
+ #
20
+ # @example Serializing to YAML
21
+ # yaml = layer.to_yaml
22
+ # # glyph_id: 42
23
+ # # palette_index: 2
24
+ # # color: "#FF0000FF"
25
+ class ColorLayer < Lutaml::Model::Serializable
26
+ # @!attribute glyph_id
27
+ # @return [Integer] Glyph ID of the layer
28
+ attribute :glyph_id, :integer
29
+
30
+ # @!attribute palette_index
31
+ # @return [Integer] Index into CPAL palette (0xFFFF = foreground)
32
+ attribute :palette_index, :integer
33
+
34
+ # @!attribute color
35
+ # @return [String, nil] Hex color from palette (#RRGGBBAA), nil if foreground
36
+ attribute :color, :string
37
+
38
+ # Check if this layer uses the foreground color
39
+ #
40
+ # @return [Boolean] True if using text foreground color
41
+ def uses_foreground_color?
42
+ palette_index == 0xFFFF
43
+ end
44
+
45
+ # Check if this layer uses a palette color
46
+ #
47
+ # @return [Boolean] True if using CPAL palette color
48
+ def uses_palette_color?
49
+ !uses_foreground_color?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Color palette information model
8
+ #
9
+ # Represents a color palette from the CPAL table. Each palette contains
10
+ # an array of RGBA colors in hex format that can be referenced by
11
+ # COLR layer palette indices.
12
+ #
13
+ # This model uses lutaml-model for structured serialization to YAML/JSON/XML.
14
+ #
15
+ # @example Creating a color palette
16
+ # palette = ColorPalette.new
17
+ # palette.index = 0
18
+ # palette.num_colors = 3
19
+ # palette.colors = ["#FF0000FF", "#00FF00FF", "#0000FFFF"]
20
+ #
21
+ # @example Serializing to YAML
22
+ # yaml = palette.to_yaml
23
+ # # index: 0
24
+ # # num_colors: 3
25
+ # # colors:
26
+ # # - "#FF0000FF"
27
+ # # - "#00FF00FF"
28
+ # # - "#0000FFFF"
29
+ class ColorPalette < Lutaml::Model::Serializable
30
+ # @!attribute index
31
+ # @return [Integer] Palette index (0-based)
32
+ attribute :index, :integer
33
+
34
+ # @!attribute num_colors
35
+ # @return [Integer] Number of colors in this palette
36
+ attribute :num_colors, :integer
37
+
38
+ # @!attribute colors
39
+ # @return [Array<String>] Array of hex color strings (#RRGGBBAA)
40
+ attribute :colors, :string, collection: true
41
+
42
+ # Get a color by index
43
+ #
44
+ # @param color_index [Integer] Color index within palette
45
+ # @return [String, nil] Hex color string or nil if invalid index
46
+ def color_at(color_index)
47
+ return nil if color_index.negative? || color_index >= colors.length
48
+
49
+ colors[color_index]
50
+ end
51
+
52
+ # Check if palette is empty
53
+ #
54
+ # @return [Boolean] True if no colors
55
+ def empty?
56
+ colors.nil? || colors.empty?
57
+ end
58
+ end
59
+ end
60
+ end
@@ -38,6 +38,22 @@ module Fontisan
38
38
  attribute :units_per_em, :integer
39
39
  attribute :collection_offset, :integer
40
40
 
41
+ # Color font information (from COLR/CPAL tables)
42
+ attribute :is_color_font, Lutaml::Model::Type::Boolean
43
+ attribute :color_glyphs, :integer
44
+ attribute :color_palettes, :integer
45
+ attribute :colors_per_palette, :integer
46
+
47
+ # SVG table information
48
+ attribute :has_svg_table, Lutaml::Model::Type::Boolean
49
+ attribute :svg_glyph_count, :integer
50
+
51
+ # Bitmap table information (CBDT/CBLC, sbix)
52
+ attribute :has_bitmap_glyphs, Lutaml::Model::Type::Boolean
53
+ attribute :bitmap_strikes, Models::BitmapStrike, collection: true
54
+ attribute :bitmap_ppem_sizes, :integer, collection: true
55
+ attribute :bitmap_formats, :string, collection: true
56
+
41
57
  key_value do
42
58
  map "font_format", to: :font_format
43
59
  map "is_variable", to: :is_variable
@@ -66,6 +82,16 @@ module Fontisan
66
82
  map "permissions", to: :permissions
67
83
  map "units_per_em", to: :units_per_em
68
84
  map "collection_offset", to: :collection_offset
85
+ map "is_color_font", to: :is_color_font
86
+ map "color_glyphs", to: :color_glyphs
87
+ map "color_palettes", to: :color_palettes
88
+ map "colors_per_palette", to: :colors_per_palette
89
+ map "has_svg_table", to: :has_svg_table
90
+ map "svg_glyph_count", to: :svg_glyph_count
91
+ map "has_bitmap_glyphs", to: :has_bitmap_glyphs
92
+ map "bitmap_strikes", to: :bitmap_strikes
93
+ map "bitmap_ppem_sizes", to: :bitmap_ppem_sizes
94
+ map "bitmap_formats", to: :bitmap_formats
69
95
  end
70
96
  end
71
97
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # SVG glyph representation model
8
+ #
9
+ # Represents an SVG document for a glyph or range of glyphs from the SVG table.
10
+ # Each SVG document can cover multiple glyph IDs and may be compressed.
11
+ #
12
+ # This model uses lutaml-model for structured serialization to YAML/JSON/XML.
13
+ #
14
+ # @example Creating an SVG glyph
15
+ # svg_glyph = SvgGlyph.new
16
+ # svg_glyph.glyph_id = 100
17
+ # svg_glyph.start_glyph_id = 100
18
+ # svg_glyph.end_glyph_id = 105
19
+ # svg_glyph.svg_content = '<svg>...</svg>'
20
+ # svg_glyph.compressed = false
21
+ #
22
+ # @example Serializing to JSON
23
+ # json = svg_glyph.to_json
24
+ # # {
25
+ # # "glyph_id": 100,
26
+ # # "start_glyph_id": 100,
27
+ # # "end_glyph_id": 105,
28
+ # # "svg_content": "<svg>...</svg>",
29
+ # # "compressed": false
30
+ # # }
31
+ class SvgGlyph < Lutaml::Model::Serializable
32
+ # @!attribute glyph_id
33
+ # @return [Integer] Primary glyph ID (usually same as start_glyph_id)
34
+ attribute :glyph_id, :integer
35
+
36
+ # @!attribute start_glyph_id
37
+ # @return [Integer] First glyph ID in range covered by this SVG
38
+ attribute :start_glyph_id, :integer
39
+
40
+ # @!attribute end_glyph_id
41
+ # @return [Integer] Last glyph ID in range covered by this SVG
42
+ attribute :end_glyph_id, :integer
43
+
44
+ # @!attribute svg_content
45
+ # @return [String] SVG XML content (decompressed)
46
+ attribute :svg_content, :string
47
+
48
+ # @!attribute compressed
49
+ # @return [Boolean] Whether the original data was gzip compressed
50
+ attribute :compressed, :boolean, default: -> { false }
51
+
52
+ # Get glyph IDs covered by this SVG document
53
+ #
54
+ # @return [Range] Range of glyph IDs
55
+ def glyph_range
56
+ start_glyph_id..end_glyph_id
57
+ end
58
+
59
+ # Check if this SVG covers a specific glyph ID
60
+ #
61
+ # @param glyph_id [Integer] Glyph ID to check
62
+ # @return [Boolean] True if glyph is in range
63
+ def includes_glyph?(glyph_id)
64
+ glyph_range.include?(glyph_id)
65
+ end
66
+
67
+ # Check if this SVG covers multiple glyphs
68
+ #
69
+ # @return [Boolean] True if range includes more than one glyph
70
+ def covers_multiple_glyphs?
71
+ start_glyph_id != end_glyph_id
72
+ end
73
+
74
+ # Get the number of glyphs covered by this SVG
75
+ #
76
+ # @return [Integer] Number of glyphs in range
77
+ def num_glyphs
78
+ end_glyph_id - start_glyph_id + 1
79
+ end
80
+
81
+ # Check if SVG content is present
82
+ #
83
+ # @return [Boolean] True if svg_content is not nil or empty
84
+ def has_content?
85
+ !svg_content.nil? && !svg_content.empty?
86
+ end
87
+ end
88
+ end
89
+ end