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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +92 -40
  3. data/README.adoc +262 -3
  4. data/Rakefile +20 -7
  5. data/lib/fontisan/commands/base_command.rb +2 -19
  6. data/lib/fontisan/commands/convert_command.rb +16 -13
  7. data/lib/fontisan/commands/info_command.rb +88 -0
  8. data/lib/fontisan/config/conversion_matrix.yml +58 -20
  9. data/lib/fontisan/converters/outline_converter.rb +6 -3
  10. data/lib/fontisan/converters/svg_generator.rb +45 -0
  11. data/lib/fontisan/converters/woff2_encoder.rb +106 -13
  12. data/lib/fontisan/models/bitmap_glyph.rb +123 -0
  13. data/lib/fontisan/models/bitmap_strike.rb +94 -0
  14. data/lib/fontisan/models/color_glyph.rb +57 -0
  15. data/lib/fontisan/models/color_layer.rb +53 -0
  16. data/lib/fontisan/models/color_palette.rb +60 -0
  17. data/lib/fontisan/models/font_info.rb +26 -0
  18. data/lib/fontisan/models/svg_glyph.rb +89 -0
  19. data/lib/fontisan/open_type_font.rb +6 -0
  20. data/lib/fontisan/optimizers/charstring_rewriter.rb +19 -8
  21. data/lib/fontisan/optimizers/pattern_analyzer.rb +4 -2
  22. data/lib/fontisan/optimizers/subroutine_builder.rb +6 -5
  23. data/lib/fontisan/optimizers/subroutine_optimizer.rb +5 -2
  24. data/lib/fontisan/pipeline/output_writer.rb +2 -2
  25. data/lib/fontisan/tables/cbdt.rb +169 -0
  26. data/lib/fontisan/tables/cblc.rb +290 -0
  27. data/lib/fontisan/tables/cff.rb +6 -12
  28. data/lib/fontisan/tables/colr.rb +291 -0
  29. data/lib/fontisan/tables/cpal.rb +281 -0
  30. data/lib/fontisan/tables/glyf/glyph_builder.rb +5 -1
  31. data/lib/fontisan/tables/sbix.rb +379 -0
  32. data/lib/fontisan/tables/svg.rb +301 -0
  33. data/lib/fontisan/true_type_font.rb +6 -0
  34. data/lib/fontisan/validation/woff2_header_validator.rb +278 -0
  35. data/lib/fontisan/validation/woff2_table_validator.rb +270 -0
  36. data/lib/fontisan/validation/woff2_validator.rb +248 -0
  37. data/lib/fontisan/version.rb +1 -1
  38. data/lib/fontisan/woff2/directory.rb +40 -11
  39. data/lib/fontisan/woff2/table_transformer.rb +506 -73
  40. data/lib/fontisan/woff2_font.rb +29 -9
  41. data/lib/fontisan/woff_font.rb +17 -4
  42. data/lib/fontisan.rb +12 -0
  43. 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
- # Find all positions where this pattern occurs
129
- positions = find_pattern_positions(charstring, pattern)
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
- positions.each do |position|
132
- replacements << [position, pattern]
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
- sampled_glyphs = charstrings.keys.sample(sample_size)
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 [32 + num].pack("c")
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("c*")
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("c*")
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("c*")
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("c*")
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
- remaining = @patterns.sort_by { |p| -p.savings }
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
- subroutines.sort_by { |subr| -subr.frequency }
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
- result = writer.convert(font, @options)
99
+ woff_data = writer.convert(font, @options)
100
100
 
101
- File.binwrite(@output_path, result[:woff_data])
101
+ File.binwrite(@output_path, woff_data)
102
102
  end
103
103
 
104
104
  # Write WOFF2 format