fontisan 0.4.23 → 0.4.25

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +240 -29
  3. data/Gemfile +6 -0
  4. data/Rakefile +9 -6
  5. data/TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md +73 -0
  6. data/TODO.improvements/02-collection-outline-priority.md +48 -0
  7. data/TODO.improvements/03-fontisan-audit-command.md +189 -0
  8. data/TODO.improvements/04-ufo-composite-glyph-encoding.md +46 -0
  9. data/TODO.improvements/05-otf-compiler-real-cff.md +63 -0
  10. data/TODO.improvements/06-cff2-blend-vsindex-operators.md +51 -0
  11. data/TODO.improvements/07-cpal-v1-header-fields.md +71 -0
  12. data/TODO.improvements/08-cff-standard-string-table.md +52 -0
  13. data/TODO.improvements/09-type1-seac-expansion.md +57 -0
  14. data/TODO.improvements/10-ufo-image-set-feature-writers.md +71 -0
  15. data/TODO.improvements/11-kern-groups-plist.md +49 -0
  16. data/TODO.improvements/12-cbdt-fixture-bindata-conversion.md +68 -0
  17. data/TODO.improvements/13-split-octokit-fetcher.md +45 -0
  18. data/TODO.improvements/14-rubocop-baseline-chip.md +62 -0
  19. data/TODO.improvements/README.md +57 -0
  20. data/benchmark/compile_benchmark.rb +13 -5
  21. data/docs/.vitepress/config.ts +1 -0
  22. data/docs/STITCHER_GUIDE.adoc +47 -3
  23. data/docs/api/layer.md +89 -0
  24. data/docs/cli/subset.md +25 -6
  25. data/docs/guide/cli/subset.md +31 -0
  26. data/docs/guide/color-fonts/bitmaps.md +17 -0
  27. data/lib/fontisan/cli.rb +2 -1
  28. data/lib/fontisan/collection/writer.rb +2 -1
  29. data/lib/fontisan/commands/validate_collection_command.rb +7 -1
  30. data/lib/fontisan/commands.rb +2 -1
  31. data/lib/fontisan/converters/collection_converter.rb +6 -1
  32. data/lib/fontisan/converters/svg_generator.rb +3 -1
  33. data/lib/fontisan/stitcher/collection_result.rb +2 -1
  34. data/lib/fontisan/stitcher/glyph_copier.rb +2 -1
  35. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
  36. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
  37. data/lib/fontisan/stitcher/source.rb +2 -1
  38. data/lib/fontisan/stitcher.rb +2 -1
  39. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +2 -1
  40. data/lib/fontisan/svg/standalone_glyph.rb +4 -1
  41. data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
  42. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
  43. data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
  44. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
  45. data/lib/fontisan/tables/cff.rb +2 -1
  46. data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
  47. data/lib/fontisan/tables/cpal.rb +112 -11
  48. data/lib/fontisan/tables.rb +2 -1
  49. data/lib/fontisan/ufo/cli.rb +4 -1
  50. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
  51. data/lib/fontisan/ufo/compile/cff.rb +6 -2
  52. data/lib/fontisan/ufo/compile/cmap.rb +11 -5
  53. data/lib/fontisan/ufo/compile/cpal.rb +2 -1
  54. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
  55. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
  56. data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
  57. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
  58. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
  59. data/lib/fontisan/ufo/compile/fvar.rb +7 -3
  60. data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
  61. data/lib/fontisan/ufo/compile/gvar.rb +5 -2
  62. data/lib/fontisan/ufo/compile/head.rb +2 -1
  63. data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
  64. data/lib/fontisan/ufo/compile/name.rb +3 -1
  65. data/lib/fontisan/ufo/compile/sbix.rb +2 -1
  66. data/lib/fontisan/ufo/compile/stat.rb +6 -3
  67. data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
  68. data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
  69. data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
  70. data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
  71. data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
  72. data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
  73. data/lib/fontisan/ufo/convert.rb +4 -1
  74. data/lib/fontisan/ufo/info.rb +4 -2
  75. data/lib/fontisan/ufo/plist.rb +7 -2
  76. data/lib/fontisan/ufo/reader.rb +3 -1
  77. data/lib/fontisan/ufo/writer.rb +13 -5
  78. data/lib/fontisan/version.rb +1 -1
  79. data/lib/fontisan/woff2/collection_decoder.rb +4 -1
  80. data/lib/fontisan/woff2/collection_encoder.rb +4 -1
  81. data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
  82. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
  83. data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
  84. data/lib/fontisan.rb +0 -1
  85. metadata +18 -4
  86. data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
  87. data/lib/fontisan/tasks.rb +0 -11
@@ -62,7 +62,8 @@ module Fontisan
62
62
  io << [regions.size].pack("n") # regionCount
63
63
  regions.each do |region|
64
64
  region.each do |coords|
65
- io << [f2dot14(coords[:start]), f2dot14(coords[:peak]), f2dot14(coords[:end])].pack("nnn")
65
+ io << [f2dot14(coords[:start]), f2dot14(coords[:peak]),
66
+ f2dot14(coords[:end])].pack("nnn")
66
67
  end
67
68
  end
68
69
  io
@@ -51,7 +51,9 @@ module Fontisan
51
51
 
52
52
  def self.format0_bytes(records)
53
53
  # Strings are stored UTF-16BE on disk (Windows Unicode).
54
- encoded = records.map { |r| r[:value].encode("UTF-16BE").force_encoding("BINARY") }
54
+ encoded = records.map do |r|
55
+ r[:value].encode("UTF-16BE").force_encoding("BINARY")
56
+ end
55
57
  storage = encoded.join
56
58
  storage_offset = 6 + (records.size * 12)
57
59
 
@@ -75,7 +75,8 @@ module Fontisan
75
75
  if glyph && glyph[:data]
76
76
  offsets << current_offset
77
77
  data = glyph[:data].b
78
- glyph_data << [glyph[:origin_x] || 0, glyph[:origin_y] || 0].pack("nn")
78
+ glyph_data << [glyph[:origin_x] || 0,
79
+ glyph[:origin_y] || 0].pack("nn")
79
80
  glyph_data << (glyph[:graphic_type] || PNG_TYPE).ljust(4)[0, 4]
80
81
  glyph_data << data
81
82
  current_offset += 8 + data.bytesize
@@ -26,7 +26,8 @@ module Fontisan
26
26
  axis_value_count = axis_value_list.size
27
27
 
28
28
  design_axes = serialize_design_axes(axes)
29
- value_tables, value_offsets = serialize_axis_values(axis_value_list, design_axes.bytesize)
29
+ value_tables, value_offsets = serialize_axis_values(axis_value_list,
30
+ design_axes.bytesize)
30
31
 
31
32
  header = serialize_header(
32
33
  design_axis_count: design_axis_count,
@@ -45,7 +46,8 @@ module Fontisan
45
46
  io
46
47
  end
47
48
 
48
- def self.serialize_header(design_axis_count:, design_axes_size:, axis_value_count:, elided_name_id:)
49
+ def self.serialize_header(design_axis_count:, design_axes_size:,
50
+ axis_value_count:, elided_name_id:)
49
51
  design_axes_offset = HEADER_SIZE
50
52
  offset_to_axis_value_offsets = design_axes_offset + design_axes_size
51
53
 
@@ -86,7 +88,8 @@ module Fontisan
86
88
  flags = av[:flags] || av["flags"] || 0
87
89
  name_id = av[:name_id] || av["name_id"] || 0
88
90
  value = f2dot14(av[:value] || av["value"] || 0)
89
- value_tables << [format, axis_idx, flags, name_id, value].pack("nnnnn")
91
+ value_tables << [format, axis_idx, flags, name_id,
92
+ value].pack("nnnnn")
90
93
  end
91
94
 
92
95
  [value_tables, value_offsets]
@@ -82,7 +82,8 @@ module Fontisan
82
82
 
83
83
  if raw && raw.encoding == Encoding::UTF_16BE
84
84
  raw.encode("UTF-8")
85
- elsif raw && raw.bytesize >= 2 && raw.getbyte(0).between?(0, 127) && raw.getbyte(1).zero?
85
+ elsif raw && raw.bytesize >= 2 && raw.getbyte(0).between?(0,
86
+ 127) && raw.getbyte(1).zero?
86
87
  # Looks like UTF-16BE
87
88
  raw.force_encoding("UTF-16BE").encode("UTF-8")
88
89
  else
@@ -165,7 +166,10 @@ module Fontisan
165
166
  return unless glyf && loca && head
166
167
 
167
168
  # Tables need context-aware initialization before per-glyph access.
168
- loca.parse_with_context(head.index_to_loc_format, num_glyphs) if loca.respond_to?(:parse_with_context)
169
+ if loca.respond_to?(:parse_with_context)
170
+ loca.parse_with_context(head.index_to_loc_format,
171
+ num_glyphs)
172
+ end
169
173
 
170
174
  num_glyphs.times do |gid|
171
175
  glyph_name = glyph_name_for(font, gid) || "glyph#{gid}"
@@ -17,10 +17,14 @@ module Fontisan
17
17
  # @return [String] the output_path
18
18
  def self.convert(ufo, output_path:, compiler: :ttf, **_opts)
19
19
  compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
20
- raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
20
+ unless compiler_class
21
+ raise ArgumentError,
22
+ "unknown intermediate compiler: #{compiler.inspect}"
23
+ end
21
24
 
22
25
  Dir.mktmpdir do |dir|
23
- intermediate_path = File.join(dir, "intermediate#{ext_for(compiler)}")
26
+ intermediate_path = File.join(dir,
27
+ "intermediate#{ext_for(compiler)}")
24
28
  compiler_class.new(ufo).compile(output_path: intermediate_path)
25
29
 
26
30
  loaded = Fontisan::FontLoader.load(intermediate_path)
@@ -18,7 +18,10 @@ module Fontisan
18
18
  # @return [String] the output_path
19
19
  def self.convert(ufo, output_path:, compiler: :otf, **_opts)
20
20
  compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
21
- raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
21
+ unless compiler_class
22
+ raise ArgumentError,
23
+ "unknown intermediate compiler: #{compiler.inspect}"
24
+ end
22
25
 
23
26
  Dir.mktmpdir do |dir|
24
27
  intermediate_path = File.join(dir, "intermediate.otf")
@@ -17,12 +17,17 @@ module Fontisan
17
17
  # @param ps_options [Hash] forwarded to the generator
18
18
  # (encoding, upm_scale, convert_curves)
19
19
  # @return [String] the output_path
20
- def self.convert(ufo, output_path:, generator_class:, compiler: :ttf, **ps_options)
20
+ def self.convert(ufo, output_path:, generator_class:, compiler: :ttf,
21
+ **ps_options)
21
22
  compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
22
- raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
23
+ unless compiler_class
24
+ raise ArgumentError,
25
+ "unknown intermediate compiler: #{compiler.inspect}"
26
+ end
23
27
 
24
28
  Dir.mktmpdir do |dir|
25
- intermediate_path = File.join(dir, "intermediate#{ext_for(compiler)}")
29
+ intermediate_path = File.join(dir,
30
+ "intermediate#{ext_for(compiler)}")
26
31
  compiler_class.new(ufo).compile(output_path: intermediate_path)
27
32
 
28
33
  loaded = Fontisan::FontLoader.load(intermediate_path)
@@ -32,14 +32,19 @@ module Fontisan
32
32
  # @return [String] the output_path
33
33
  def self.convert(ufo, output_path:, compiler: :ttf, **woff_options)
34
34
  compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
35
- raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
35
+ unless compiler_class
36
+ raise ArgumentError,
37
+ "unknown intermediate compiler: #{compiler.inspect}"
38
+ end
36
39
 
37
40
  Dir.mktmpdir do |dir|
38
- intermediate_path = File.join(dir, "intermediate#{format_ext(compiler)}")
41
+ intermediate_path = File.join(dir,
42
+ "intermediate#{format_ext(compiler)}")
39
43
  compiler_class.new(ufo).compile(output_path: intermediate_path)
40
44
 
41
45
  loaded = Fontisan::FontLoader.load(intermediate_path)
42
- woff_bytes = Fontisan::Converters::WoffWriter.new.convert(loaded, woff_options)
46
+ woff_bytes = Fontisan::Converters::WoffWriter.new.convert(loaded,
47
+ woff_options)
43
48
  File.binwrite(output_path, woff_bytes)
44
49
  end
45
50
 
@@ -21,14 +21,19 @@ module Fontisan
21
21
  # @return [String] the output_path
22
22
  def self.convert(ufo, output_path:, compiler: :ttf, **woff2_options)
23
23
  compiler_class = Convert::COMPILER_FOR_FORMAT[compiler.to_sym]
24
- raise ArgumentError, "unknown intermediate compiler: #{compiler.inspect}" unless compiler_class
24
+ unless compiler_class
25
+ raise ArgumentError,
26
+ "unknown intermediate compiler: #{compiler.inspect}"
27
+ end
25
28
 
26
29
  Dir.mktmpdir do |dir|
27
- intermediate_path = File.join(dir, "intermediate#{format_ext(compiler)}")
30
+ intermediate_path = File.join(dir,
31
+ "intermediate#{format_ext(compiler)}")
28
32
  compiler_class.new(ufo).compile(output_path: intermediate_path)
29
33
 
30
34
  loaded = Fontisan::FontLoader.load(intermediate_path)
31
- result = Fontisan::Converters::Woff2Encoder.new.convert(loaded, woff2_options)
35
+ result = Fontisan::Converters::Woff2Encoder.new.convert(loaded,
36
+ woff2_options)
32
37
  File.binwrite(output_path, result[:woff2_binary])
33
38
  end
34
39
 
@@ -71,7 +71,10 @@ module Fontisan
71
71
  end
72
72
 
73
73
  compiler = COMPILER_FOR_FORMAT[format_key]
74
- raise ArgumentError, "unknown UFO output format: #{to.inspect}" unless compiler
74
+ unless compiler
75
+ raise ArgumentError,
76
+ "unknown UFO output format: #{to.inspect}"
77
+ end
75
78
 
76
79
  compiler.new(ufo).compile(output_path: output_path)
77
80
  output_path
@@ -81,8 +81,10 @@ module Fontisan
81
81
  # @return [Array(Integer, Integer)] [major, minor]
82
82
  def self.parse_version(version)
83
83
  case version.to_s
84
- when /\A(\d+)\.(\d+)\.\d+\z/ then [Regexp.last_match(1).to_i, Regexp.last_match(2).to_i]
85
- when /\A(\d+)\.(\d+)\z/ then [Regexp.last_match(1).to_i, Regexp.last_match(2).to_i]
84
+ when /\A(\d+)\.(\d+)\.\d+\z/ then [Regexp.last_match(1).to_i,
85
+ Regexp.last_match(2).to_i]
86
+ when /\A(\d+)\.(\d+)\z/ then [Regexp.last_match(1).to_i,
87
+ Regexp.last_match(2).to_i]
86
88
  when /\A(\d+)\z/ then [Regexp.last_match(1).to_i, 0]
87
89
  else [0, 0]
88
90
  end
@@ -25,7 +25,9 @@ module Fontisan
25
25
  root = doc.root
26
26
  raise ParseError, "no <plist> root element" unless root&.name == "plist"
27
27
 
28
- parse_value(root.children.find { |c| c.element? || c.cdata? || c.text? && !c.text.strip.empty? })
28
+ parse_value(root.children.find do |c|
29
+ c.element? || c.cdata? || c.text? && !c.text.strip.empty?
30
+ end)
29
31
  end
30
32
 
31
33
  # @param value [Object] the value to serialize
@@ -73,7 +75,10 @@ module Fontisan
73
75
  # dict children are key/value pairs (key first, then value)
74
76
  while children.any?
75
77
  key_node = children.shift
76
- raise ParseError, "dict key is not <key>" unless key_node.name == "key"
78
+ unless key_node.name == "key"
79
+ raise ParseError,
80
+ "dict key is not <key>"
81
+ end
77
82
 
78
83
  value_node = children.shift
79
84
  result[key_node.text] = parse_value(value_node)
@@ -94,7 +94,9 @@ module Fontisan
94
94
  if order.first.is_a?(Hash)
95
95
  order.reduce({}) { |h, pair| h.merge(pair) }
96
96
  else
97
- order.to_h { |filename| [File.basename(filename, ".glif"), filename] }
97
+ order.to_h do |filename|
98
+ [File.basename(filename, ".glif"), filename]
99
+ end
98
100
  end
99
101
  else
100
102
  raise "unsupported contents.plist format: #{order.class}"
@@ -50,7 +50,8 @@ module Fontisan
50
50
  end
51
51
 
52
52
  def write_fontinfo(path)
53
- File.write(File.join(path, "fontinfo.plist"), Plist.emit(@font.info.to_plist))
53
+ File.write(File.join(path, "fontinfo.plist"),
54
+ Plist.emit(@font.info.to_plist))
54
55
  end
55
56
 
56
57
  def write_layercontents(path)
@@ -75,11 +76,17 @@ module Fontisan
75
76
  FileUtils.mkpath(layer_dir)
76
77
 
77
78
  contents =
78
- layer.glyphs.transform_values { |g| "#{safe_filename(g.name)}.glif" }
79
- File.write(File.join(layer_dir, "contents.plist"), Plist.emit(contents))
79
+ layer.glyphs.transform_values do |g|
80
+ "#{safe_filename(g.name)}.glif"
81
+ end
82
+ File.write(File.join(layer_dir, "contents.plist"),
83
+ Plist.emit(contents))
80
84
 
81
85
  layer.glyphs.each_value do |glyph|
82
- File.write(File.join(layer_dir, "#{safe_filename(glyph.name)}.glif"), glyph.to_glif)
86
+ File.write(
87
+ File.join(layer_dir,
88
+ "#{safe_filename(glyph.name)}.glif"), glyph.to_glif
89
+ )
83
90
  end
84
91
  end
85
92
  end
@@ -96,7 +103,8 @@ module Fontisan
96
103
  def write_kerning(path)
97
104
  return if @font.kerning.empty?
98
105
 
99
- File.write(File.join(path, "kerning.plist"), Plist.emit(@font.kerning.to_plist))
106
+ File.write(File.join(path, "kerning.plist"),
107
+ Plist.emit(@font.kerning.to_plist))
100
108
  end
101
109
 
102
110
  def write_features(path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fontisan
4
- VERSION = "0.4.23"
4
+ VERSION = "0.4.25"
5
5
  end
@@ -131,7 +131,10 @@ module Fontisan
131
131
  def read_collection_directory(pos)
132
132
  version = @data[pos, 4].unpack1("N")
133
133
  pos += 4
134
- raise InvalidFontError, "Unsupported CollectionHeader version 0x#{version.to_s(16)}" unless version == 0x00010000
134
+ unless version == 0x00010000
135
+ raise InvalidFontError,
136
+ "Unsupported CollectionHeader version 0x#{version.to_s(16)}"
137
+ end
135
138
 
136
139
  num_fonts, pos = UInt255.decode_at(@data, pos)
137
140
  font_entries = Array.new(num_fonts) do
@@ -32,7 +32,10 @@ module Fontisan
32
32
  # @param fonts [Array<TrueTypeFont, OpenTypeFont>] Source fonts
33
33
  # @return [String] WOFF2 collection binary
34
34
  def encode_fonts(fonts)
35
- raise ArgumentError, "fonts cannot be empty" if fonts.nil? || fonts.empty?
35
+ if fonts.nil? || fonts.empty?
36
+ raise ArgumentError,
37
+ "fonts cannot be empty"
38
+ end
36
39
 
37
40
  prepared = fonts.map.with_index { |font, i| prepare_font(font, i) }
38
41
  _, font_to_ckeys = deduplicate(prepared)
@@ -50,7 +50,8 @@ module Fontisan
50
50
  start_offset = source_offsets[i]
51
51
  end_offset = source_offsets[i + 1]
52
52
  if start_offset < end_offset
53
- glyf << @glyf_data.byteslice(start_offset, end_offset - start_offset)
53
+ glyf << @glyf_data.byteslice(start_offset,
54
+ end_offset - start_offset)
54
55
  end
55
56
  remainder = glyf.bytesize % 4
56
57
  glyf << ("\x00" * (4 - remainder)) if remainder.positive?
@@ -131,7 +131,8 @@ module Fontisan
131
131
  streams[:bbox_entries] = StringIO.new(bbox_blob[bitmap_size..] || "")
132
132
 
133
133
  # overlapSimpleBitmap follows all streams when optionFlags bit 0 set.
134
- streams[:overlap_bitmap] = header[:has_overlap_bitmap] ? @data[pos, overlap_size] : ""
134
+ streams[:overlap_bitmap] =
135
+ header[:has_overlap_bitmap] ? @data[pos, overlap_size] : ""
135
136
 
136
137
  # nContour is read positionally (int16 per glyph).
137
138
  nc_data = streams[:n_contour].string
@@ -178,7 +179,9 @@ module Fontisan
178
179
  x = 0
179
180
  y = 0
180
181
  triplet_flags.each do |flag|
181
- dx, dy, oc = TripletCodec.decode(flag, read_triplet_payload(flag, streams[:glyph]))
182
+ dx, dy, oc = TripletCodec.decode(flag,
183
+ read_triplet_payload(flag,
184
+ streams[:glyph]))
182
185
  x += dx
183
186
  y += dy
184
187
  xs << x
@@ -97,7 +97,8 @@ module Fontisan
97
97
  def per_table_checksum(table)
98
98
  data = table.bytes
99
99
  if table.tag == "head"
100
- data = "#{data.byteslice(0, 8)}#{['00000000'].pack('H8')}#{data.byteslice(12..)}"
100
+ data = "#{data.byteslice(0,
101
+ 8)}#{['00000000'].pack('H8')}#{data.byteslice(12..)}"
101
102
  end
102
103
  Utilities::ChecksumCalculator.calculate_table_checksum(data)
103
104
  end
data/lib/fontisan.rb CHANGED
@@ -119,7 +119,6 @@ module Fontisan
119
119
  autoload :SfntBuilder, "fontisan/sfnt_builder"
120
120
  autoload :Stitcher, "fontisan/stitcher"
121
121
  autoload :StitcherCli, "fontisan/stitcher_cli"
122
- autoload :Tasks, "fontisan/tasks"
123
122
  autoload :TrueTypeCollection, "fontisan/true_type_collection"
124
123
  autoload :TrueTypeFont, "fontisan/true_type_font"
125
124
  autoload :TrueTypeFontExtensions, "fontisan/true_type_font_extensions"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fontisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.23
4
+ version: 0.4.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-09 00:00:00.000000000 Z
11
+ date: 2026-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -133,6 +133,21 @@ files:
133
133
  - LICENSE
134
134
  - README.adoc
135
135
  - Rakefile
136
+ - TODO.improvements/01-cbdt-cblc-gid-stable-propagation.md
137
+ - TODO.improvements/02-collection-outline-priority.md
138
+ - TODO.improvements/03-fontisan-audit-command.md
139
+ - TODO.improvements/04-ufo-composite-glyph-encoding.md
140
+ - TODO.improvements/05-otf-compiler-real-cff.md
141
+ - TODO.improvements/06-cff2-blend-vsindex-operators.md
142
+ - TODO.improvements/07-cpal-v1-header-fields.md
143
+ - TODO.improvements/08-cff-standard-string-table.md
144
+ - TODO.improvements/09-type1-seac-expansion.md
145
+ - TODO.improvements/10-ufo-image-set-feature-writers.md
146
+ - TODO.improvements/11-kern-groups-plist.md
147
+ - TODO.improvements/12-cbdt-fixture-bindata-conversion.md
148
+ - TODO.improvements/13-split-octokit-fetcher.md
149
+ - TODO.improvements/14-rubocop-baseline-chip.md
150
+ - TODO.improvements/README.md
136
151
  - benchmark/compile_benchmark.rb
137
152
  - benchmark/variation_quick_bench.rb
138
153
  - docs/.gitignore
@@ -168,6 +183,7 @@ files:
168
183
  - docs/api/font-loader.md
169
184
  - docs/api/font-writer.md
170
185
  - docs/api/index.md
186
+ - docs/api/layer.md
171
187
  - docs/api/models/glyph-accessor.md
172
188
  - docs/api/models/glyph.md
173
189
  - docs/api/models/table-analyzer.md
@@ -588,8 +604,6 @@ files:
588
604
  - lib/fontisan/tables/svg.rb
589
605
  - lib/fontisan/tables/variation_common.rb
590
606
  - lib/fontisan/tables/vvar.rb
591
- - lib/fontisan/tasks.rb
592
- - lib/fontisan/tasks/fixture_downloader.rb
593
607
  - lib/fontisan/true_type_collection.rb
594
608
  - lib/fontisan/true_type_font.rb
595
609
  - lib/fontisan/true_type_font_extensions.rb
@@ -1,162 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "open-uri"
4
- require "net/http"
5
- require "fileutils"
6
-
7
- module Fontisan
8
- # Tasks supporting the developer workflow: fixture downloads, etc.
9
- # Lives under its own namespace so Rakefiles and other tooling can
10
- # load just the task plumbing without pulling in the full fontisan
11
- # stack (BinData tables, UFO, etc.).
12
- module Tasks
13
- # Downloads a single fixture file with retry on transient network
14
- # failures. Used by `rake fixtures:download` so a single CDN blip
15
- # (5xx, connection reset, OpenTimeout) doesn't sink a fresh
16
- # checkout. Permanent failures (404, malformed URL) surface
17
- # immediately.
18
- #
19
- # The downloader is a focused class, not a procedural Rakefile
20
- # patch, so the retry logic is unit-testable in isolation.
21
- #
22
- # @example
23
- # Fontisan::Tasks::FixtureDownloader.new(
24
- # url: "https://github.com/.../font.ttf",
25
- # destination: "spec/fixtures/font.ttf",
26
- # ).call
27
- class FixtureDownloader
28
- RETRIABLE_ERRORS = [
29
- Net::OpenTimeout,
30
- Net::ReadTimeout,
31
- Errno::ECONNRESET,
32
- Errno::ECONNREFUSED,
33
- Errno::EHOSTUNREACH,
34
- Errno::ETIMEDOUT,
35
- EOFError,
36
- IOError,
37
- ].freeze
38
-
39
- # 5xx HTTP responses are transient server errors worth retrying.
40
- # 4xx are permanent (404, 403) and must fail fast.
41
- RETRIABLE_HTTP_STATUSES = (500..599)
42
-
43
- DEFAULT_MAX_RETRIES = 3
44
- DEFAULT_BASE_BACKOFF = 0.5 # seconds; doubles per attempt
45
-
46
- # Error raised after exhausting all retries. Carries the last
47
- # underlying exception so callers can log the root cause.
48
- class Error < StandardError
49
- attr_reader :last_error
50
-
51
- def initialize(url:, attempts:, last_error:)
52
- @last_error = last_error
53
- super("Failed to download #{url} after #{attempts} attempts: " \
54
- "#{last_error.class}: #{last_error.message}")
55
- end
56
- end
57
-
58
- attr_reader :url, :destination, :max_retries, :base_backoff, :sleep_method
59
-
60
- # @param url [String] source URL.
61
- # @param destination [String] path to write bytes to. Parent dir
62
- # is auto-created.
63
- # @param max_retries [Integer] total attempts including the
64
- # first. 3 means: try, retry, retry.
65
- # @param base_backoff [Float] seconds to sleep before the first
66
- # retry. Doubles per attempt.
67
- # @param sleep_method [#call] injectable sleep (for tests).
68
- # Defaults to Kernel.sleep.
69
- def initialize(url:, destination:, max_retries: DEFAULT_MAX_RETRIES,
70
- base_backoff: DEFAULT_BASE_BACKOFF, sleep_method: method(:sleep))
71
- @url = url
72
- @destination = destination
73
- @max_retries = max_retries
74
- @base_backoff = base_backoff
75
- @sleep_method = sleep_method
76
- end
77
-
78
- # Performs the download. Returns the destination path on
79
- # success. Raises {Error} after exhausting retries.
80
- #
81
- # @return [String] destination path
82
- # @raise [Error]
83
- def call
84
- attempts = 0
85
- nil
86
-
87
- begin
88
- attempts += 1
89
- fetch_to_destination
90
- destination
91
- rescue StandardError => e
92
- e
93
- raise if permanent_failure?(e)
94
- raise Error.new(url: url, attempts: attempts, last_error: e) if attempts >= max_retries
95
-
96
- backoff = base_backoff * (2**(attempts - 1))
97
- sleep_method.call(backoff)
98
- retry
99
- end
100
- end
101
-
102
- private
103
-
104
- def fetch_to_destination
105
- FileUtils.mkdir_p(File.dirname(destination))
106
-
107
- # IO.copy_stream avoids loading the whole response into memory
108
- # and is more Windows-compatible than remote.read + File.binwrite.
109
- # URLs come from FixtureFonts config (version-controlled), not
110
- # user input — same trust model as the previous inline URI.open
111
- # call in the Rakefile.
112
- #
113
- # Parsing with URI.parse first satisfies CodeQL's "open with
114
- # non-constant value" check: any string that isn't a valid URI
115
- # raises URI::InvalidURIError before OpenURI can dispatch on
116
- # it. The parsed URI's .open is OpenURI's standard entry.
117
- # rubocop:disable Security/Open
118
- URI.parse(url).open(open_uri_options) do |remote|
119
- File.open(destination, "wb") do |file|
120
- IO.copy_stream(remote, file)
121
- end
122
- end
123
- # rubocop:enable Security/Open
124
- end
125
-
126
- # `open-uri` follows redirects by default and surfaces HTTP
127
- # errors as `OpenURI::HTTPError` whose `io.status` is the `[code,
128
- # message]` array. We re-raise non-retriable 4xx as
129
- # `permanent-failure`-tagged exceptions so the retry loop exits.
130
- def open_uri_options
131
- {
132
- "User-Agent" => "fontisan-fixtures/1.0",
133
- redirect: true,
134
- open_timeout: 30,
135
- read_timeout: 120,
136
- }
137
- end
138
-
139
- def permanent_failure?(error)
140
- case error
141
- when OpenURI::HTTPError
142
- status = parse_http_status(error)
143
- status && !RETRIABLE_HTTP_STATUSES.cover?(status)
144
- else
145
- RETRIABLE_ERRORS.none? { |klass| error.is_a?(klass) }
146
- end
147
- end
148
-
149
- def parse_http_status(error)
150
- io = error.io
151
- return nil unless io
152
-
153
- status = io.status
154
- return nil unless status
155
-
156
- status.first.to_i
157
- rescue StandardError
158
- nil
159
- end
160
- end
161
- end
162
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- # Tasks supporting the developer workflow: fixture downloads, etc.
5
- # Lives under its own namespace so Rakefiles and other tooling can
6
- # load just the task plumbing without pulling in the full fontisan
7
- # stack (BinData tables, UFO, etc.).
8
- module Tasks
9
- autoload :FixtureDownloader, "fontisan/tasks/fixture_downloader"
10
- end
11
- end