fontisan 0.2.3 → 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 +92 -40
- data/README.adoc +262 -3
- data/Rakefile +20 -7
- 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/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/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/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_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 +17 -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
|
|
@@ -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
|
|
@@ -512,6 +512,12 @@ module Fontisan
|
|
|
512
512
|
Constants::GPOS_TAG => Tables::Gpos,
|
|
513
513
|
Constants::GLYF_TAG => Tables::Glyf,
|
|
514
514
|
Constants::LOCA_TAG => Tables::Loca,
|
|
515
|
+
"SVG " => Tables::Svg,
|
|
516
|
+
"COLR" => Tables::Colr,
|
|
517
|
+
"CPAL" => Tables::Cpal,
|
|
518
|
+
"CBDT" => Tables::Cbdt,
|
|
519
|
+
"CBLC" => Tables::Cblc,
|
|
520
|
+
"sbix" => Tables::Sbix,
|
|
515
521
|
}[tag]
|
|
516
522
|
end
|
|
517
523
|
|
|
@@ -30,12 +30,13 @@ module Fontisan
|
|
|
30
30
|
#
|
|
31
31
|
# @param charstring [String] original CharString bytes
|
|
32
32
|
# @param patterns [Array<Pattern>] patterns to replace in this CharString
|
|
33
|
+
# @param glyph_id [Integer, nil] glyph ID being rewritten (for position filtering)
|
|
33
34
|
# @return [String] rewritten CharString with subroutine calls
|
|
34
|
-
def rewrite(charstring, patterns)
|
|
35
|
+
def rewrite(charstring, patterns, glyph_id = nil)
|
|
35
36
|
return charstring if patterns.empty?
|
|
36
37
|
|
|
37
38
|
# Build list of all replacements: [position, pattern]
|
|
38
|
-
replacements = build_replacement_list(charstring, patterns)
|
|
39
|
+
replacements = build_replacement_list(charstring, patterns, glyph_id)
|
|
39
40
|
|
|
40
41
|
# Remove overlapping replacements
|
|
41
42
|
replacements = remove_overlaps(replacements)
|
|
@@ -120,16 +121,26 @@ module Fontisan
|
|
|
120
121
|
# Build list of all pattern replacements with their positions
|
|
121
122
|
# @param charstring [String] CharString being rewritten
|
|
122
123
|
# @param patterns [Array<Pattern>] patterns to find
|
|
124
|
+
# @param glyph_id [Integer, nil] glyph ID being rewritten (for position filtering)
|
|
123
125
|
# @return [Array<Array>] array of [position, pattern] pairs
|
|
124
|
-
def build_replacement_list(charstring, patterns)
|
|
126
|
+
def build_replacement_list(charstring, patterns, glyph_id = nil)
|
|
125
127
|
replacements = []
|
|
126
128
|
|
|
127
129
|
patterns.each do |pattern|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
if glyph_id && pattern.respond_to?(:positions) && pattern.positions.is_a?(Hash)
|
|
131
|
+
# Use exact positions from pattern analysis for this glyph
|
|
132
|
+
glyph_positions = pattern.positions[glyph_id] || []
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
glyph_positions.each do |position|
|
|
135
|
+
replacements << [position, pattern]
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
# Fallback for backward compatibility (unit tests without glyph_id)
|
|
139
|
+
positions = find_pattern_positions(charstring, pattern)
|
|
140
|
+
|
|
141
|
+
positions.each do |position|
|
|
142
|
+
replacements << [position, pattern]
|
|
143
|
+
end
|
|
133
144
|
end
|
|
134
145
|
end
|
|
135
146
|
|
|
@@ -140,7 +151,7 @@ module Fontisan
|
|
|
140
151
|
# @param charstring [String] CharString to search
|
|
141
152
|
# @param pattern [Pattern] pattern to find
|
|
142
153
|
# @return [Array<Integer>] array of start positions
|
|
143
|
-
def find_pattern_positions(charstring, pattern)
|
|
154
|
+
def find_pattern_positions(charstring, pattern, glyph_id = nil)
|
|
144
155
|
positions = []
|
|
145
156
|
offset = 0
|
|
146
157
|
|
|
@@ -160,7 +160,9 @@ module Fontisan
|
|
|
160
160
|
charstrings.length
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
# Use deterministic selection instead of random sampling
|
|
164
|
+
# Sort keys first to ensure consistent ordering across platforms
|
|
165
|
+
sampled_glyphs = charstrings.keys.sort.take(sample_size)
|
|
164
166
|
|
|
165
167
|
# NEW: Pre-compute boundaries for sampled glyphs
|
|
166
168
|
# Check if boundaries are useful (more than just start position)
|
|
@@ -249,7 +251,7 @@ module Fontisan
|
|
|
249
251
|
# Build positions hash
|
|
250
252
|
positions = {}
|
|
251
253
|
by_glyph.each do |glyph_id, glyph_occurrences|
|
|
252
|
-
positions[glyph_id] = glyph_occurrences.map(&:last)
|
|
254
|
+
positions[glyph_id] = glyph_occurrences.map(&:last).uniq
|
|
253
255
|
end
|
|
254
256
|
|
|
255
257
|
@patterns[bytes] = Pattern.new(
|
|
@@ -95,22 +95,23 @@ module Fontisan
|
|
|
95
95
|
# @return [String] encoded bytes
|
|
96
96
|
def encode_integer(num)
|
|
97
97
|
# Range 1: -107 to 107 (single byte)
|
|
98
|
+
# CFF spec: byte value = 139 + number
|
|
98
99
|
if num >= -107 && num <= 107
|
|
99
|
-
return [
|
|
100
|
+
return [139 + num].pack("C")
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
# Range 2: 108 to 1131 (two bytes)
|
|
103
104
|
if num >= 108 && num <= 1131
|
|
104
105
|
b0 = 247 + ((num - 108) >> 8)
|
|
105
106
|
b1 = (num - 108) & 0xff
|
|
106
|
-
return [b0, b1].pack("
|
|
107
|
+
return [b0, b1].pack("C*")
|
|
107
108
|
end
|
|
108
109
|
|
|
109
110
|
# Range 3: -1131 to -108 (two bytes)
|
|
110
111
|
if num >= -1131 && num <= -108
|
|
111
112
|
b0 = 251 - ((num + 108) >> 8)
|
|
112
113
|
b1 = -(num + 108) & 0xff
|
|
113
|
-
return [b0, b1].pack("
|
|
114
|
+
return [b0, b1].pack("C*")
|
|
114
115
|
end
|
|
115
116
|
|
|
116
117
|
# Range 4: -32768 to 32767 (three bytes)
|
|
@@ -118,7 +119,7 @@ module Fontisan
|
|
|
118
119
|
b0 = 29
|
|
119
120
|
b1 = (num >> 8) & 0xff
|
|
120
121
|
b2 = num & 0xff
|
|
121
|
-
return [b0, b1, b2].pack("
|
|
122
|
+
return [b0, b1, b2].pack("C*")
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
# Range 5: Larger numbers (five bytes)
|
|
@@ -127,7 +128,7 @@ module Fontisan
|
|
|
127
128
|
b2 = (num >> 16) & 0xff
|
|
128
129
|
b3 = (num >> 8) & 0xff
|
|
129
130
|
b4 = num & 0xff
|
|
130
|
-
[b0, b1, b2, b3, b4].pack("
|
|
131
|
+
[b0, b1, b2, b3, b4].pack("C*")
|
|
131
132
|
end
|
|
132
133
|
end
|
|
133
134
|
end
|
|
@@ -30,7 +30,9 @@ module Fontisan
|
|
|
30
30
|
# @return [Array<Pattern>] selected patterns
|
|
31
31
|
def optimize_selection
|
|
32
32
|
selected = []
|
|
33
|
-
|
|
33
|
+
# Sort by savings (descending), then by length (descending), then by min glyph ID,
|
|
34
|
+
# then by byte values for complete determinism across platforms
|
|
35
|
+
remaining = @patterns.sort_by { |p| [-p.savings, -p.length, p.glyphs.min, p.bytes.bytes] }
|
|
34
36
|
|
|
35
37
|
remaining.each do |pattern|
|
|
36
38
|
break if selected.length >= @max_subrs
|
|
@@ -50,7 +52,8 @@ module Fontisan
|
|
|
50
52
|
# @return [Array<Pattern>] ordered subroutines
|
|
51
53
|
def optimize_ordering(subroutines)
|
|
52
54
|
# Higher frequency = lower ID (shorter encoding)
|
|
53
|
-
|
|
55
|
+
# Use same comprehensive sort keys as optimize_selection for consistency
|
|
56
|
+
subroutines.sort_by { |subr| [-subr.frequency, -subr.length, subr.glyphs.min, subr.bytes.bytes] }
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
# Check if nesting would be beneficial
|
|
@@ -96,9 +96,9 @@ module Fontisan
|
|
|
96
96
|
|
|
97
97
|
writer = Converters::WoffWriter.new
|
|
98
98
|
font = build_font_from_tables(tables)
|
|
99
|
-
|
|
99
|
+
woff_data = writer.convert(font, @options)
|
|
100
100
|
|
|
101
|
-
File.binwrite(@output_path,
|
|
101
|
+
File.binwrite(@output_path, woff_data)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
# Write WOFF2 format
|