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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +221 -49
- data/README.adoc +519 -5
- data/Rakefile +20 -7
- data/lib/fontisan/cli.rb +67 -6
- data/lib/fontisan/commands/base_command.rb +2 -19
- data/lib/fontisan/commands/convert_command.rb +16 -13
- data/lib/fontisan/commands/info_command.rb +88 -0
- data/lib/fontisan/commands/validate_command.rb +107 -151
- data/lib/fontisan/config/conversion_matrix.yml +58 -20
- data/lib/fontisan/converters/outline_converter.rb +6 -3
- data/lib/fontisan/converters/svg_generator.rb +45 -0
- data/lib/fontisan/converters/woff2_encoder.rb +84 -13
- data/lib/fontisan/models/bitmap_glyph.rb +123 -0
- data/lib/fontisan/models/bitmap_strike.rb +94 -0
- data/lib/fontisan/models/color_glyph.rb +57 -0
- data/lib/fontisan/models/color_layer.rb +53 -0
- data/lib/fontisan/models/color_palette.rb +60 -0
- data/lib/fontisan/models/font_info.rb +26 -0
- data/lib/fontisan/models/svg_glyph.rb +89 -0
- data/lib/fontisan/models/validation_report.rb +227 -0
- data/lib/fontisan/open_type_font.rb +6 -0
- data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
- data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
- data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
- data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
- data/lib/fontisan/pipeline/output_writer.rb +2 -2
- data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
- data/lib/fontisan/tables/cbdt.rb +169 -0
- data/lib/fontisan/tables/cblc.rb +290 -0
- data/lib/fontisan/tables/cff.rb +6 -12
- data/lib/fontisan/tables/cmap.rb +82 -2
- data/lib/fontisan/tables/colr.rb +291 -0
- data/lib/fontisan/tables/cpal.rb +281 -0
- data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
- data/lib/fontisan/tables/glyf.rb +118 -0
- data/lib/fontisan/tables/head.rb +60 -0
- data/lib/fontisan/tables/hhea.rb +74 -0
- data/lib/fontisan/tables/maxp.rb +60 -0
- data/lib/fontisan/tables/name.rb +76 -0
- data/lib/fontisan/tables/os2.rb +113 -0
- data/lib/fontisan/tables/post.rb +57 -0
- data/lib/fontisan/tables/sbix.rb +379 -0
- data/lib/fontisan/tables/svg.rb +301 -0
- data/lib/fontisan/true_type_font.rb +6 -0
- data/lib/fontisan/validators/basic_validator.rb +85 -0
- data/lib/fontisan/validators/font_book_validator.rb +130 -0
- data/lib/fontisan/validators/opentype_validator.rb +112 -0
- data/lib/fontisan/validators/profile_loader.rb +139 -0
- data/lib/fontisan/validators/validator.rb +484 -0
- data/lib/fontisan/validators/web_font_validator.rb +102 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/directory.rb +40 -11
- data/lib/fontisan/woff2/table_transformer.rb +506 -73
- data/lib/fontisan/woff2_font.rb +29 -9
- data/lib/fontisan/woff_font.rb +17 -4
- data/lib/fontisan.rb +90 -6
- metadata +20 -9
- data/lib/fontisan/config/validation_rules.yml +0 -149
- data/lib/fontisan/validation/checksum_validator.rb +0 -170
- data/lib/fontisan/validation/consistency_validator.rb +0 -197
- data/lib/fontisan/validation/structure_validator.rb +0 -198
- data/lib/fontisan/validation/table_validator.rb +0 -158
- data/lib/fontisan/validation/validator.rb +0 -152
- 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
|
-
#
|
|
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
|
-
# @
|
|
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
|
-
#
|
|
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" =>
|
|
187
|
-
"glyf_loca" =>
|
|
188
|
-
"hmtx" =>
|
|
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
|
-
#
|
|
299
|
-
data =
|
|
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
|