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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +94 -48
- data/README.adoc +293 -3
- data/Rakefile +20 -7
- data/lib/fontisan/base_collection.rb +296 -0
- 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 +156 -50
- 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 +106 -13
- data/lib/fontisan/font_loader.rb +109 -26
- data/lib/fontisan/formatters/text_formatter.rb +72 -19
- data/lib/fontisan/models/bitmap_glyph.rb +123 -0
- data/lib/fontisan/models/bitmap_strike.rb +94 -0
- data/lib/fontisan/models/collection_brief_info.rb +6 -0
- data/lib/fontisan/models/collection_info.rb +6 -1
- 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/open_type_collection.rb +17 -220
- 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/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/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/sbix.rb +379 -0
- data/lib/fontisan/tables/svg.rb +301 -0
- data/lib/fontisan/true_type_collection.rb +29 -113
- data/lib/fontisan/true_type_font.rb +6 -0
- data/lib/fontisan/validation/woff2_header_validator.rb +278 -0
- data/lib/fontisan/validation/woff2_table_validator.rb +270 -0
- data/lib/fontisan/validation/woff2_validator.rb +248 -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 +12 -0
- metadata +18 -2
|
@@ -58,30 +58,13 @@ module Fontisan
|
|
|
58
58
|
# Load the font using FontLoader.
|
|
59
59
|
#
|
|
60
60
|
# Uses FontLoader for automatic format detection and loading.
|
|
61
|
-
# Returns
|
|
61
|
+
# Returns TrueTypeFont, OpenTypeFont, WoffFont, or Woff2Font depending on file format.
|
|
62
62
|
#
|
|
63
|
-
# @return [TrueTypeFont, OpenTypeFont] The loaded font
|
|
63
|
+
# @return [TrueTypeFont, OpenTypeFont, WoffFont, Woff2Font] The loaded font
|
|
64
64
|
# @raise [Errno::ENOENT] if file does not exist
|
|
65
|
-
# @raise [UnsupportedFormatError] for WOFF/WOFF2 or other unsupported formats
|
|
66
65
|
# @raise [InvalidFontError] for corrupted or unknown formats
|
|
67
66
|
# @raise [Error] for other loading failures
|
|
68
67
|
def load_font
|
|
69
|
-
# BaseCommand is for inspection - reject compressed formats first
|
|
70
|
-
# Check file signature before attempting to load
|
|
71
|
-
File.open(@font_path, "rb") do |io|
|
|
72
|
-
signature = io.read(4)
|
|
73
|
-
|
|
74
|
-
if signature == "wOFF"
|
|
75
|
-
raise UnsupportedFormatError,
|
|
76
|
-
"Unsupported font format: WOFF files must be decompressed first. " \
|
|
77
|
-
"Use ConvertCommand to convert WOFF to TTF/OTF."
|
|
78
|
-
elsif signature == "wOF2"
|
|
79
|
-
raise UnsupportedFormatError,
|
|
80
|
-
"Unsupported font format: WOFF2 files must be decompressed first. " \
|
|
81
|
-
"Use ConvertCommand to convert WOFF2 to TTF/OTF."
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
68
|
# Brief mode uses metadata loading for 5x faster parsing
|
|
86
69
|
mode = @options[:brief] ? LoadingModes::METADATA : (@options[:mode] || LoadingModes::FULL)
|
|
87
70
|
|
|
@@ -51,22 +51,26 @@ module Fontisan
|
|
|
51
51
|
# @option options [Boolean] :verbose Verbose output
|
|
52
52
|
def initialize(font_path, options = {})
|
|
53
53
|
super(font_path, options)
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
# Convert string keys to symbols for Thor compatibility
|
|
56
|
+
opts = options.transform_keys(&:to_sym)
|
|
57
|
+
|
|
58
|
+
@output_path = opts[:output]
|
|
55
59
|
|
|
56
60
|
# Parse target format
|
|
57
|
-
@target_format = parse_target_format(
|
|
61
|
+
@target_format = parse_target_format(opts[:to])
|
|
58
62
|
|
|
59
63
|
# Parse coordinates if string provided
|
|
60
|
-
@coordinates = if
|
|
61
|
-
parse_coordinates(
|
|
62
|
-
elsif
|
|
63
|
-
|
|
64
|
+
@coordinates = if opts[:coordinates]
|
|
65
|
+
parse_coordinates(opts[:coordinates])
|
|
66
|
+
elsif opts[:instance_coordinates]
|
|
67
|
+
opts[:instance_coordinates]
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
@instance_index =
|
|
67
|
-
@preserve_variation =
|
|
68
|
-
@preserve_hints =
|
|
69
|
-
@validate = !
|
|
70
|
+
@instance_index = opts[:instance_index]
|
|
71
|
+
@preserve_variation = opts[:preserve_variation]
|
|
72
|
+
@preserve_hints = opts.fetch(:preserve_hints, false)
|
|
73
|
+
@validate = !opts[:no_validate]
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
# Execute the conversion
|
|
@@ -193,14 +197,13 @@ module Fontisan
|
|
|
193
197
|
when "svg"
|
|
194
198
|
:svg
|
|
195
199
|
when "woff"
|
|
196
|
-
|
|
197
|
-
"WOFF format conversion is not supported yet. Use woff2 instead."
|
|
200
|
+
:woff
|
|
198
201
|
when "woff2"
|
|
199
202
|
:woff2
|
|
200
203
|
else
|
|
201
204
|
raise ArgumentError,
|
|
202
205
|
"Unknown target format: #{format}. " \
|
|
203
|
-
"Supported: ttf, otf, svg, woff2"
|
|
206
|
+
"Supported: ttf, otf, svg, woff, woff2"
|
|
204
207
|
end
|
|
205
208
|
end
|
|
206
209
|
|
|
@@ -50,62 +50,80 @@ module Fontisan
|
|
|
50
50
|
# Brief mode: load each font and populate brief info
|
|
51
51
|
brief_info = Models::CollectionBriefInfo.new
|
|
52
52
|
brief_info.collection_path = @font_path
|
|
53
|
+
brief_info.collection_type = collection.class.collection_format
|
|
54
|
+
brief_info.collection_version = collection.version_string
|
|
53
55
|
brief_info.num_fonts = collection.num_fonts
|
|
54
|
-
brief_info.fonts =
|
|
55
|
-
|
|
56
|
-
collection.num_fonts.times do |index|
|
|
57
|
-
# Load individual font from collection
|
|
58
|
-
font = FontLoader.load(@font_path, font_index: index, mode: LoadingModes::METADATA)
|
|
59
|
-
|
|
60
|
-
# Populate brief info for this font
|
|
61
|
-
info = Models::FontInfo.new
|
|
62
|
-
|
|
63
|
-
# Font format and variable status
|
|
64
|
-
info.font_format = case font
|
|
65
|
-
when TrueTypeFont
|
|
66
|
-
"truetype"
|
|
67
|
-
when OpenTypeFont
|
|
68
|
-
"cff"
|
|
69
|
-
else
|
|
70
|
-
"unknown"
|
|
71
|
-
end
|
|
72
|
-
info.is_variable = font.has_table?(Constants::FVAR_TAG)
|
|
73
|
-
|
|
74
|
-
# Collection offset (only populated for fonts in collections)
|
|
75
|
-
info.collection_offset = collection.font_offsets[index]
|
|
76
|
-
|
|
77
|
-
# Essential names
|
|
78
|
-
if font.has_table?(Constants::NAME_TAG)
|
|
79
|
-
name_table = font.table(Constants::NAME_TAG)
|
|
80
|
-
info.family_name = name_table.english_name(Tables::Name::FAMILY)
|
|
81
|
-
info.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
|
|
82
|
-
info.full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
83
|
-
info.postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
84
|
-
info.version = name_table.english_name(Tables::Name::VERSION)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Essential metrics
|
|
88
|
-
if font.has_table?(Constants::HEAD_TAG)
|
|
89
|
-
head = font.table(Constants::HEAD_TAG)
|
|
90
|
-
info.font_revision = head.font_revision
|
|
91
|
-
info.units_per_em = head.units_per_em
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Vendor ID
|
|
95
|
-
if font.has_table?(Constants::OS2_TAG)
|
|
96
|
-
os2_table = font.table(Constants::OS2_TAG)
|
|
97
|
-
info.vendor_id = os2_table.vendor_id
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
brief_info.fonts << info
|
|
101
|
-
end
|
|
56
|
+
brief_info.fonts = load_collection_fonts(collection, @font_path)
|
|
102
57
|
|
|
103
58
|
brief_info
|
|
104
59
|
else
|
|
105
|
-
# Full mode: show detailed sharing statistics
|
|
106
|
-
collection.collection_info(io, @font_path)
|
|
60
|
+
# Full mode: show detailed sharing statistics AND font information
|
|
61
|
+
full_info = collection.collection_info(io, @font_path)
|
|
62
|
+
|
|
63
|
+
# Add font information to full mode
|
|
64
|
+
full_info.fonts = load_collection_fonts(collection, @font_path)
|
|
65
|
+
|
|
66
|
+
full_info
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Load font information for all fonts in a collection
|
|
72
|
+
#
|
|
73
|
+
# @param collection [TrueTypeCollection, OpenTypeCollection] The collection
|
|
74
|
+
# @param collection_path [String] Path to the collection file
|
|
75
|
+
# @return [Array<Models::FontInfo>] Array of font info objects
|
|
76
|
+
def load_collection_fonts(collection, collection_path)
|
|
77
|
+
fonts = []
|
|
78
|
+
|
|
79
|
+
collection.num_fonts.times do |index|
|
|
80
|
+
# Load individual font from collection
|
|
81
|
+
font = FontLoader.load(collection_path, font_index: index, mode: LoadingModes::METADATA)
|
|
82
|
+
|
|
83
|
+
# Populate font info
|
|
84
|
+
info = Models::FontInfo.new
|
|
85
|
+
|
|
86
|
+
# Font format and variable status
|
|
87
|
+
info.font_format = case font
|
|
88
|
+
when TrueTypeFont
|
|
89
|
+
"truetype"
|
|
90
|
+
when OpenTypeFont
|
|
91
|
+
"cff"
|
|
92
|
+
else
|
|
93
|
+
"unknown"
|
|
94
|
+
end
|
|
95
|
+
info.is_variable = font.has_table?(Constants::FVAR_TAG)
|
|
96
|
+
|
|
97
|
+
# Collection offset (only populated for fonts in collections)
|
|
98
|
+
info.collection_offset = collection.font_offsets[index]
|
|
99
|
+
|
|
100
|
+
# Essential names
|
|
101
|
+
if font.has_table?(Constants::NAME_TAG)
|
|
102
|
+
name_table = font.table(Constants::NAME_TAG)
|
|
103
|
+
info.family_name = name_table.english_name(Tables::Name::FAMILY)
|
|
104
|
+
info.subfamily_name = name_table.english_name(Tables::Name::SUBFAMILY)
|
|
105
|
+
info.full_name = name_table.english_name(Tables::Name::FULL_NAME)
|
|
106
|
+
info.postscript_name = name_table.english_name(Tables::Name::POSTSCRIPT_NAME)
|
|
107
|
+
info.version = name_table.english_name(Tables::Name::VERSION)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Essential metrics
|
|
111
|
+
if font.has_table?(Constants::HEAD_TAG)
|
|
112
|
+
head = font.table(Constants::HEAD_TAG)
|
|
113
|
+
info.font_revision = head.font_revision
|
|
114
|
+
info.units_per_em = head.units_per_em
|
|
107
115
|
end
|
|
116
|
+
|
|
117
|
+
# Vendor ID
|
|
118
|
+
if font.has_table?(Constants::OS2_TAG)
|
|
119
|
+
os2_table = font.table(Constants::OS2_TAG)
|
|
120
|
+
info.vendor_id = os2_table.vendor_id
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
fonts << info
|
|
108
124
|
end
|
|
125
|
+
|
|
126
|
+
fonts
|
|
109
127
|
end
|
|
110
128
|
|
|
111
129
|
# Get individual font information
|
|
@@ -184,6 +202,9 @@ module Fontisan
|
|
|
184
202
|
populate_from_name_table(info) if font.has_table?(Constants::NAME_TAG)
|
|
185
203
|
populate_from_os2_table(info) if font.has_table?(Constants::OS2_TAG)
|
|
186
204
|
populate_from_head_table(info) if font.has_table?(Constants::HEAD_TAG)
|
|
205
|
+
populate_color_info(info) if font.has_table?("COLR") && font.has_table?("CPAL")
|
|
206
|
+
populate_svg_info(info) if font.has_table?("SVG ")
|
|
207
|
+
populate_bitmap_info(info) if font.has_table?("CBLC") || font.has_table?("sbix")
|
|
187
208
|
end
|
|
188
209
|
|
|
189
210
|
# Populate FontInfo from the name table.
|
|
@@ -237,6 +258,91 @@ module Fontisan
|
|
|
237
258
|
info.units_per_em = head_table.units_per_em
|
|
238
259
|
end
|
|
239
260
|
|
|
261
|
+
# Populate FontInfo with color font information from COLR/CPAL tables
|
|
262
|
+
#
|
|
263
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
264
|
+
def populate_color_info(info)
|
|
265
|
+
colr_table = font.table("COLR")
|
|
266
|
+
cpal_table = font.table("CPAL")
|
|
267
|
+
|
|
268
|
+
return unless colr_table && cpal_table
|
|
269
|
+
|
|
270
|
+
info.is_color_font = true
|
|
271
|
+
info.color_glyphs = colr_table.num_color_glyphs
|
|
272
|
+
info.color_palettes = cpal_table.num_palettes
|
|
273
|
+
info.colors_per_palette = cpal_table.num_palette_entries
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
warn "Failed to populate color font info: #{e.message}"
|
|
276
|
+
info.is_color_font = false
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Populate FontInfo with SVG table information
|
|
280
|
+
#
|
|
281
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
282
|
+
def populate_svg_info(info)
|
|
283
|
+
svg_table = font.table("SVG ")
|
|
284
|
+
|
|
285
|
+
return unless svg_table
|
|
286
|
+
|
|
287
|
+
info.has_svg_table = true
|
|
288
|
+
info.svg_glyph_count = svg_table.glyph_ids_with_svg.length
|
|
289
|
+
rescue StandardError => e
|
|
290
|
+
warn "Failed to populate SVG info: #{e.message}"
|
|
291
|
+
info.has_svg_table = false
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Populate FontInfo with bitmap table information (CBDT/CBLC, sbix)
|
|
295
|
+
#
|
|
296
|
+
# @param info [Models::FontInfo] FontInfo instance to populate
|
|
297
|
+
def populate_bitmap_info(info)
|
|
298
|
+
bitmap_strikes = []
|
|
299
|
+
ppem_sizes = []
|
|
300
|
+
formats = []
|
|
301
|
+
|
|
302
|
+
# Check for CBDT/CBLC (Google format)
|
|
303
|
+
if font.has_table?("CBLC") && font.has_table?("CBDT")
|
|
304
|
+
cblc = font.table("CBLC")
|
|
305
|
+
info.has_bitmap_glyphs = true
|
|
306
|
+
ppem_sizes.concat(cblc.ppem_sizes)
|
|
307
|
+
|
|
308
|
+
cblc.strikes.each do |strike_rec|
|
|
309
|
+
bitmap_strikes << Models::BitmapStrike.new(
|
|
310
|
+
ppem: strike_rec.ppem,
|
|
311
|
+
start_glyph_id: strike_rec.start_glyph_index,
|
|
312
|
+
end_glyph_id: strike_rec.end_glyph_index,
|
|
313
|
+
bit_depth: strike_rec.bit_depth,
|
|
314
|
+
num_glyphs: strike_rec.glyph_range.size
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
formats << "PNG" # CBDT typically contains PNG data
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Check for sbix (Apple format)
|
|
321
|
+
if font.has_table?("sbix")
|
|
322
|
+
sbix = font.table("sbix")
|
|
323
|
+
info.has_bitmap_glyphs = true
|
|
324
|
+
ppem_sizes.concat(sbix.ppem_sizes)
|
|
325
|
+
formats.concat(sbix.supported_formats)
|
|
326
|
+
|
|
327
|
+
sbix.strikes.each do |strike|
|
|
328
|
+
bitmap_strikes << Models::BitmapStrike.new(
|
|
329
|
+
ppem: strike[:ppem],
|
|
330
|
+
start_glyph_id: 0,
|
|
331
|
+
end_glyph_id: strike[:num_glyphs] - 1,
|
|
332
|
+
bit_depth: 32, # sbix is typically 32-bit
|
|
333
|
+
num_glyphs: strike[:num_glyphs]
|
|
334
|
+
)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
info.bitmap_strikes = bitmap_strikes unless bitmap_strikes.empty?
|
|
339
|
+
info.bitmap_ppem_sizes = ppem_sizes.uniq.sort
|
|
340
|
+
info.bitmap_formats = formats.uniq
|
|
341
|
+
rescue StandardError => e
|
|
342
|
+
warn "Failed to populate bitmap info: #{e.message}"
|
|
343
|
+
info.has_bitmap_glyphs = false
|
|
344
|
+
end
|
|
345
|
+
|
|
240
346
|
# Format OS/2 embedding permission flags into a human-readable string.
|
|
241
347
|
#
|
|
242
348
|
# @param flags [Integer] OS/2 fsType flags
|
|
@@ -78,26 +78,34 @@ conversions:
|
|
|
78
78
|
are architectural placeholders for future optimization.
|
|
79
79
|
|
|
80
80
|
# Phase 2 conversions (Milestone 2.3) - WOFF
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
81
|
+
- from: ttf
|
|
82
|
+
to: woff
|
|
83
|
+
strategy: woff_writer
|
|
84
|
+
description: "Compress TrueType to WOFF format with zlib"
|
|
85
|
+
status: implemented
|
|
86
|
+
notes: >
|
|
87
|
+
WOFF 1.0 encoding with zlib compression. Supports optional metadata
|
|
88
|
+
and private data blocks. Individual table compression for optimal
|
|
89
|
+
file size reduction.
|
|
90
|
+
|
|
91
|
+
- from: otf
|
|
92
|
+
to: woff
|
|
93
|
+
strategy: woff_writer
|
|
94
|
+
description: "Compress OpenType/CFF to WOFF format with zlib"
|
|
95
|
+
status: implemented
|
|
96
|
+
notes: >
|
|
97
|
+
WOFF 1.0 encoding with zlib compression. Supports optional metadata
|
|
98
|
+
and private data blocks. Individual table compression for optimal
|
|
99
|
+
file size reduction.
|
|
100
|
+
|
|
101
|
+
- from: woff
|
|
102
|
+
to: woff
|
|
103
|
+
strategy: woff_recompress
|
|
104
|
+
description: "Copy WOFF font with re-compression"
|
|
105
|
+
status: implemented
|
|
106
|
+
notes: >
|
|
107
|
+
Same-format copy operation. Decompresses and re-compresses WOFF data,
|
|
108
|
+
useful for normalizing compression or updating metadata.
|
|
101
109
|
|
|
102
110
|
# Reverse WOFF conversions (WOFF → TTF/OTF)
|
|
103
111
|
- from: woff
|
|
@@ -139,6 +147,36 @@ conversions:
|
|
|
139
147
|
transformation. Note: SVG fonts are deprecated in favor of WOFF2, but
|
|
140
148
|
useful for fallback, conversion, and inspection purposes.
|
|
141
149
|
|
|
150
|
+
# WOFF2 decompression (reverse conversions)
|
|
151
|
+
- from: woff2
|
|
152
|
+
to: ttf
|
|
153
|
+
strategy: woff2_decoder
|
|
154
|
+
description: "Decompress WOFF2 to TrueType format"
|
|
155
|
+
status: implemented
|
|
156
|
+
notes: >
|
|
157
|
+
WOFF2 decompression with Brotli. Reverses table transformations
|
|
158
|
+
(glyf/loca, hmtx) if present. Preserves all font data and metadata.
|
|
159
|
+
Automatically detects font flavor from WOFF2 header.
|
|
160
|
+
|
|
161
|
+
- from: woff2
|
|
162
|
+
to: otf
|
|
163
|
+
strategy: woff2_decoder
|
|
164
|
+
description: "Decompress WOFF2 to OpenType/CFF format"
|
|
165
|
+
status: implemented
|
|
166
|
+
notes: >
|
|
167
|
+
WOFF2 decompression with Brotli. Reverses table transformations
|
|
168
|
+
if present. Preserves all font data and metadata. Automatically
|
|
169
|
+
detects font flavor from WOFF2 header.
|
|
170
|
+
|
|
171
|
+
- from: woff2
|
|
172
|
+
to: woff2
|
|
173
|
+
strategy: woff2_recompress
|
|
174
|
+
description: "Copy WOFF2 font with re-compression"
|
|
175
|
+
status: implemented
|
|
176
|
+
notes: >
|
|
177
|
+
Same-format copy operation. Decompresses and re-compresses WOFF2 data
|
|
178
|
+
with Brotli, useful for normalizing compression or updating metadata.
|
|
179
|
+
|
|
142
180
|
# Conversion compatibility matrix
|
|
143
181
|
#
|
|
144
182
|
# This section documents which source features are preserved in conversions.
|
|
@@ -415,11 +415,14 @@ module Fontisan
|
|
|
415
415
|
# If we have local subroutines, add Subrs offset
|
|
416
416
|
# Subrs offset is relative to Private DICT start
|
|
417
417
|
if local_subrs.any?
|
|
418
|
-
#
|
|
418
|
+
# Add a placeholder Subrs offset first to get accurate size
|
|
419
|
+
private_dict_hash[:subrs] = 0
|
|
420
|
+
|
|
421
|
+
# Calculate size of Private DICT with Subrs entry
|
|
419
422
|
temp_private_dict_data = Tables::Cff::DictBuilder.build(private_dict_hash)
|
|
420
423
|
subrs_offset = temp_private_dict_data.bytesize
|
|
421
424
|
|
|
422
|
-
#
|
|
425
|
+
# Update with actual Subrs offset
|
|
423
426
|
private_dict_hash[:subrs] = subrs_offset
|
|
424
427
|
end
|
|
425
428
|
|
|
@@ -729,7 +732,7 @@ module Fontisan
|
|
|
729
732
|
if glyph_patterns.empty?
|
|
730
733
|
charstring
|
|
731
734
|
else
|
|
732
|
-
rewriter.rewrite(charstring, glyph_patterns)
|
|
735
|
+
rewriter.rewrite(charstring, glyph_patterns, glyph_id)
|
|
733
736
|
end
|
|
734
737
|
end
|
|
735
738
|
|
|
@@ -99,6 +99,51 @@ module Fontisan
|
|
|
99
99
|
|
|
100
100
|
private
|
|
101
101
|
|
|
102
|
+
# Generate SVG for a color glyph from COLR/CPAL tables
|
|
103
|
+
#
|
|
104
|
+
# @param glyph_id [Integer] Glyph ID
|
|
105
|
+
# @param colr_table [Tables::Colr] COLR table
|
|
106
|
+
# @param cpal_table [Tables::Cpal] CPAL table
|
|
107
|
+
# @param extractor [OutlineExtractor] Outline extractor for layer glyphs
|
|
108
|
+
# @param palette_index [Integer] Palette index to use (default: 0)
|
|
109
|
+
# @return [String, nil] SVG path data with color layers, or nil if not color glyph
|
|
110
|
+
def generate_color_glyph(glyph_id, colr_table, cpal_table, extractor, palette_index: 0)
|
|
111
|
+
# Get layers for this glyph
|
|
112
|
+
layers = colr_table.layers_for_glyph(glyph_id)
|
|
113
|
+
return nil if layers.empty?
|
|
114
|
+
|
|
115
|
+
# Get palette colors
|
|
116
|
+
palette = cpal_table.palette(palette_index)
|
|
117
|
+
return nil unless palette
|
|
118
|
+
|
|
119
|
+
# Generate SVG for each layer
|
|
120
|
+
svg_paths = []
|
|
121
|
+
layers.each do |layer|
|
|
122
|
+
# Get outline for layer glyph
|
|
123
|
+
outline = extractor.extract(layer.glyph_id)
|
|
124
|
+
next unless outline
|
|
125
|
+
|
|
126
|
+
# Get color for this layer
|
|
127
|
+
color = if layer.uses_foreground_color?
|
|
128
|
+
"currentColor" # Use CSS currentColor for foreground
|
|
129
|
+
else
|
|
130
|
+
palette[layer.palette_index] || "#000000FF"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Convert outline to SVG path
|
|
134
|
+
path_data = outline.to_svg_path
|
|
135
|
+
next if path_data.empty?
|
|
136
|
+
|
|
137
|
+
# Create colored path element
|
|
138
|
+
svg_paths << %(<path d="#{path_data}" fill="#{color}" />)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
svg_paths.empty? ? nil : svg_paths.join("\n")
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
warn "Failed to generate color glyph #{glyph_id}: #{e.message}"
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
102
147
|
# Extract glyph data from font
|
|
103
148
|
#
|
|
104
149
|
# @param font [TrueTypeFont, OpenTypeFont] Source font
|