fontisan 0.2.3 → 0.2.5

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +221 -49
  3. data/README.adoc +519 -5
  4. data/Rakefile +20 -7
  5. data/lib/fontisan/cli.rb +67 -6
  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 +88 -0
  9. data/lib/fontisan/commands/validate_command.rb +107 -151
  10. data/lib/fontisan/config/conversion_matrix.yml +58 -20
  11. data/lib/fontisan/converters/outline_converter.rb +6 -3
  12. data/lib/fontisan/converters/svg_generator.rb +45 -0
  13. data/lib/fontisan/converters/woff2_encoder.rb +84 -13
  14. data/lib/fontisan/models/bitmap_glyph.rb +123 -0
  15. data/lib/fontisan/models/bitmap_strike.rb +94 -0
  16. data/lib/fontisan/models/color_glyph.rb +57 -0
  17. data/lib/fontisan/models/color_layer.rb +53 -0
  18. data/lib/fontisan/models/color_palette.rb +60 -0
  19. data/lib/fontisan/models/font_info.rb +26 -0
  20. data/lib/fontisan/models/svg_glyph.rb +89 -0
  21. data/lib/fontisan/models/validation_report.rb +227 -0
  22. data/lib/fontisan/open_type_font.rb +6 -0
  23. data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
  24. data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
  25. data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
  26. data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
  27. data/lib/fontisan/pipeline/output_writer.rb +2 -2
  28. data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
  29. data/lib/fontisan/tables/cbdt.rb +169 -0
  30. data/lib/fontisan/tables/cblc.rb +290 -0
  31. data/lib/fontisan/tables/cff.rb +6 -12
  32. data/lib/fontisan/tables/cmap.rb +82 -2
  33. data/lib/fontisan/tables/colr.rb +291 -0
  34. data/lib/fontisan/tables/cpal.rb +281 -0
  35. data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
  36. data/lib/fontisan/tables/glyf.rb +118 -0
  37. data/lib/fontisan/tables/head.rb +60 -0
  38. data/lib/fontisan/tables/hhea.rb +74 -0
  39. data/lib/fontisan/tables/maxp.rb +60 -0
  40. data/lib/fontisan/tables/name.rb +76 -0
  41. data/lib/fontisan/tables/os2.rb +113 -0
  42. data/lib/fontisan/tables/post.rb +57 -0
  43. data/lib/fontisan/tables/sbix.rb +379 -0
  44. data/lib/fontisan/tables/svg.rb +301 -0
  45. data/lib/fontisan/true_type_font.rb +6 -0
  46. data/lib/fontisan/validators/basic_validator.rb +85 -0
  47. data/lib/fontisan/validators/font_book_validator.rb +130 -0
  48. data/lib/fontisan/validators/opentype_validator.rb +112 -0
  49. data/lib/fontisan/validators/profile_loader.rb +139 -0
  50. data/lib/fontisan/validators/validator.rb +484 -0
  51. data/lib/fontisan/validators/web_font_validator.rb +102 -0
  52. data/lib/fontisan/version.rb +1 -1
  53. data/lib/fontisan/woff2/directory.rb +40 -11
  54. data/lib/fontisan/woff2/table_transformer.rb +506 -73
  55. data/lib/fontisan/woff2_font.rb +29 -9
  56. data/lib/fontisan/woff_font.rb +17 -4
  57. data/lib/fontisan.rb +90 -6
  58. metadata +20 -9
  59. data/lib/fontisan/config/validation_rules.yml +0 -149
  60. data/lib/fontisan/validation/checksum_validator.rb +0 -170
  61. data/lib/fontisan/validation/consistency_validator.rb +0 -197
  62. data/lib/fontisan/validation/structure_validator.rb +0 -198
  63. data/lib/fontisan/validation/table_validator.rb +0 -158
  64. data/lib/fontisan/validation/validator.rb +0 -152
  65. data/lib/fontisan/validation/variable_font_validator.rb +0 -218
@@ -6,7 +6,10 @@ require_relative "../woff2/directory"
6
6
  require_relative "../woff2/table_transformer"
7
7
  require_relative "../utilities/brotli_wrapper"
8
8
  require_relative "../utilities/checksum_calculator"
9
+ # Validation temporarily disabled - will be reimplemented with new DSL framework in Week 3+
10
+ # require_relative "../validation/woff2_validator"
9
11
  require "yaml"
12
+ require "stringio"
10
13
 
11
14
  module Fontisan
12
15
  module Converters
@@ -24,6 +27,7 @@ module Fontisan
24
27
  # 5. Compress all tables with single Brotli stream
25
28
  # 6. Build WOFF2 header and table directory
26
29
  # 7. Assemble complete WOFF2 binary
30
+ # 8. (Optional) Validate encoded WOFF2
27
31
  #
28
32
  # For Phase 2 Milestone 2.1:
29
33
  # - Basic WOFF2 structure generation
@@ -33,8 +37,13 @@ module Fontisan
33
37
  #
34
38
  # @example Convert TTF to WOFF2
35
39
  # encoder = Woff2Encoder.new
36
- # woff2_binary = encoder.convert(font)
37
- # File.binwrite('font.woff2', woff2_binary)
40
+ # result = encoder.convert(font)
41
+ # File.binwrite('font.woff2', result[:woff2_binary])
42
+ #
43
+ # @example Convert with validation
44
+ # encoder = Woff2Encoder.new
45
+ # result = encoder.convert(font, validate: true)
46
+ # puts result[:validation_report].text_summary if result[:validation_report]
38
47
  class Woff2Encoder
39
48
  include ConversionStrategy
40
49
 
@@ -57,7 +66,9 @@ module Fontisan
57
66
  # @param options [Hash] Conversion options
58
67
  # @option options [Integer] :quality Brotli quality (0-11)
59
68
  # @option options [Boolean] :transform_tables Apply table transformations
60
- # @return [Hash] Hash with :woff2_binary key containing WOFF2 binary
69
+ # @option options [Boolean] :validate Run validation after encoding
70
+ # @option options [Symbol] :validation_level Validation level (:strict, :standard, :lenient)
71
+ # @return [Hash] Hash with :woff2_binary and optional :validation_report keys
61
72
  # @raise [Error] If encoding fails
62
73
  def convert(font, options = {})
63
74
  validate(font, :woff2)
@@ -97,8 +108,17 @@ module Fontisan
97
108
  # Assemble WOFF2 binary
98
109
  woff2_binary = assemble_woff2(header, entries, compressed_data)
99
110
 
100
- # Return in special format for ConvertCommand to handle
101
- { woff2_binary: woff2_binary }
111
+ # Prepare result
112
+ result = { woff2_binary: woff2_binary }
113
+
114
+ # Optional validation
115
+ # Temporarily disabled - will be reimplemented with new DSL framework
116
+ # if options[:validate]
117
+ # validation_report = validate_encoding(woff2_binary, options)
118
+ # result[:validation_report] = validation_report
119
+ # end
120
+
121
+ result
102
122
  end
103
123
 
104
124
  # Get list of supported conversions
@@ -144,6 +164,43 @@ module Fontisan
144
164
 
145
165
  private
146
166
 
167
+ # Helper method to load WOFF2 from StringIO
168
+ #
169
+ # This is added to Woff2Font to support in-memory validation
170
+ module Woff2FontMemoryLoader
171
+ def self.from_file_io(io, path_for_report)
172
+ io.rewind
173
+
174
+ woff2 = Woff2Font.new
175
+ woff2.io_source = Woff2Font::IOSource.new(path_for_report)
176
+
177
+ # Read header
178
+ woff2.header = Woff2::Woff2Header.read(io)
179
+
180
+ # Validate signature
181
+ unless woff2.header.signature == Woff2::Woff2Header::SIGNATURE
182
+ raise InvalidFontError,
183
+ "Invalid WOFF2 signature: expected 0x#{Woff2::Woff2Header::SIGNATURE.to_s(16)}, " \
184
+ "got 0x#{woff2.header.signature.to_i.to_s(16)}"
185
+ end
186
+
187
+ # Read table directory
188
+ woff2.table_entries = Woff2Font.read_table_directory_from_io(io, woff2.header)
189
+
190
+ # Decompress tables
191
+ woff2.decompressed_tables = Woff2Font.decompress_tables(io, woff2.header,
192
+ woff2.table_entries)
193
+
194
+ # Apply transformations
195
+ Woff2Font.apply_transformations!(woff2.table_entries, woff2.decompressed_tables)
196
+
197
+ woff2
198
+ end
199
+ end
200
+
201
+ # Extend Woff2Font with in-memory loading
202
+ Woff2Font.singleton_class.prepend(Woff2FontMemoryLoader)
203
+
147
204
  # Load configuration from YAML file
148
205
  #
149
206
  # @param path [String, nil] Path to config file
@@ -183,9 +240,9 @@ module Fontisan
183
240
  "mode" => "font",
184
241
  },
185
242
  "transformations" => {
186
- "enabled" => false, # Disabled for Milestone 2.1
187
- "glyf_loca" => false,
188
- "hmtx" => false,
243
+ "enabled" => true, # Enable transformations for better compression
244
+ "glyf_loca" => true,
245
+ "hmtx" => true,
189
246
  },
190
247
  "metadata" => {
191
248
  "include" => false,
@@ -255,11 +312,17 @@ module Fontisan
255
312
  # @return [Array<Woff2::Directory::Entry>] Table entries
256
313
  def build_table_entries(table_data, transformer, transform_enabled)
257
314
  entries = []
315
+ transformed_data = {}
258
316
 
259
317
  # Sort tables by tag for consistent output
260
318
  sorted_tags = table_data.keys.sort
261
319
 
262
320
  sorted_tags.each do |tag|
321
+ # Skip loca if we're transforming glyf (loca is combined with glyf)
322
+ if tag == "loca" && transform_enabled && transformer.transformable?("glyf")
323
+ next
324
+ end
325
+
263
326
  entry = Woff2::Directory::Entry.new
264
327
  entry.tag = tag
265
328
 
@@ -270,8 +333,10 @@ module Fontisan
270
333
  # Apply transformation if enabled and supported
271
334
  if transform_enabled && transformer.transformable?(tag)
272
335
  transformed = transformer.transform_table(tag)
273
- if transformed && transformed.bytesize < data.bytesize
336
+ if transformed&.bytesize&.positive? && transformed.bytesize < data.bytesize
337
+ # Transformation successful and reduces size
274
338
  entry.transform_length = transformed.bytesize
339
+ transformed_data[tag] = transformed
275
340
  end
276
341
  end
277
342
 
@@ -281,6 +346,9 @@ module Fontisan
281
346
  entries << entry
282
347
  end
283
348
 
349
+ # Store transformed data for compression
350
+ @transformed_data = transformed_data
351
+
284
352
  entries
285
353
  end
286
354
 
@@ -295,12 +363,15 @@ module Fontisan
295
363
  combined_data = String.new(encoding: Encoding::BINARY)
296
364
 
297
365
  entries.each do |entry|
298
- # Get table data
299
- data = table_data[entry.tag]
366
+ # Use transformed data if available, otherwise use original
367
+ data = if @transformed_data && @transformed_data[entry.tag]
368
+ @transformed_data[entry.tag]
369
+ else
370
+ table_data[entry.tag]
371
+ end
372
+
300
373
  next unless data
301
374
 
302
- # For this milestone, we don't have transformed data yet
303
- # Use original table data
304
375
  combined_data << data
305
376
  end
306
377
 
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Fontisan
6
+ module Models
7
+ # Bitmap glyph representation model
8
+ #
9
+ # Represents a bitmap glyph from the CBDT/CBLC tables. Each glyph contains
10
+ # bitmap image data at a specific ppem size.
11
+ #
12
+ # This model uses lutaml-model for structured serialization to YAML/JSON/XML.
13
+ #
14
+ # @example Creating a bitmap glyph
15
+ # glyph = BitmapGlyph.new
16
+ # glyph.glyph_id = 42
17
+ # glyph.ppem = 16
18
+ # glyph.format = "PNG"
19
+ # glyph.width = 16
20
+ # glyph.height = 16
21
+ # glyph.data_size = 256
22
+ #
23
+ # @example Serializing to JSON
24
+ # json = glyph.to_json
25
+ # # {
26
+ # # "glyph_id": 42,
27
+ # # "ppem": 16,
28
+ # # "format": "PNG",
29
+ # # "width": 16,
30
+ # # "height": 16,
31
+ # # "data_size": 256
32
+ # # }
33
+ class BitmapGlyph < Lutaml::Model::Serializable
34
+ # @!attribute glyph_id
35
+ # @return [Integer] Glyph ID
36
+ attribute :glyph_id, :integer
37
+
38
+ # @!attribute ppem
39
+ # @return [Integer] Pixels per em for this bitmap
40
+ attribute :ppem, :integer
41
+
42
+ # @!attribute format
43
+ # @return [String] Bitmap format (e.g., "PNG", "JPEG", "TIFF")
44
+ attribute :format, :string
45
+
46
+ # @!attribute width
47
+ # @return [Integer] Bitmap width in pixels
48
+ attribute :width, :integer
49
+
50
+ # @!attribute height
51
+ # @return [Integer] Bitmap height in pixels
52
+ attribute :height, :integer
53
+
54
+ # @!attribute bit_depth
55
+ # @return [Integer] Bit depth (1, 2, 4, 8, 32)
56
+ attribute :bit_depth, :integer
57
+
58
+ # @!attribute data_size
59
+ # @return [Integer] Size of bitmap data in bytes
60
+ attribute :data_size, :integer
61
+
62
+ # @!attribute data_offset
63
+ # @return [Integer] Offset to bitmap data in CBDT table
64
+ attribute :data_offset, :integer
65
+
66
+ # Check if this is a PNG bitmap
67
+ #
68
+ # @return [Boolean] True if format is PNG
69
+ def png?
70
+ format&.upcase == "PNG"
71
+ end
72
+
73
+ # Check if this is a JPEG bitmap
74
+ #
75
+ # @return [Boolean] True if format is JPEG
76
+ def jpeg?
77
+ format&.upcase == "JPEG"
78
+ end
79
+
80
+ # Check if this is a TIFF bitmap
81
+ #
82
+ # @return [Boolean] True if format is TIFF
83
+ def tiff?
84
+ format&.upcase == "TIFF"
85
+ end
86
+
87
+ # Check if this is a color bitmap (32-bit)
88
+ #
89
+ # @return [Boolean] True if 32-bit color
90
+ def color?
91
+ bit_depth == 32
92
+ end
93
+
94
+ # Check if this is a monochrome bitmap (1-bit)
95
+ #
96
+ # @return [Boolean] True if 1-bit monochrome
97
+ def monochrome?
98
+ bit_depth == 1
99
+ end
100
+
101
+ # Get the color depth description
102
+ #
103
+ # @return [String] Human-readable color depth
104
+ def color_depth
105
+ case bit_depth
106
+ when 1 then "1-bit (monochrome)"
107
+ when 2 then "2-bit (4 colors)"
108
+ when 4 then "4-bit (16 colors)"
109
+ when 8 then "8-bit (256 colors)"
110
+ when 32 then "32-bit (full color with alpha)"
111
+ else "#{bit_depth}-bit"
112
+ end
113
+ end
114
+
115
+ # Get bitmap dimensions as string
116
+ #
117
+ # @return [String] Dimensions in "WxH" format
118
+ def dimensions
119
+ "#{width}x#{height}"
120
+ end
121
+ end
122
+ end
123
+ end
@@ -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
@@ -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