fontisan 0.4.23 → 0.4.24

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 (71) 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/benchmark/compile_benchmark.rb +13 -5
  6. data/docs/.vitepress/config.ts +1 -0
  7. data/docs/STITCHER_GUIDE.adoc +47 -3
  8. data/docs/api/layer.md +89 -0
  9. data/docs/cli/subset.md +25 -6
  10. data/docs/guide/cli/subset.md +31 -0
  11. data/docs/guide/color-fonts/bitmaps.md +17 -0
  12. data/lib/fontisan/cli.rb +2 -1
  13. data/lib/fontisan/collection/writer.rb +2 -1
  14. data/lib/fontisan/commands/validate_collection_command.rb +7 -1
  15. data/lib/fontisan/commands.rb +2 -1
  16. data/lib/fontisan/converters/collection_converter.rb +6 -1
  17. data/lib/fontisan/converters/svg_generator.rb +3 -1
  18. data/lib/fontisan/stitcher/collection_result.rb +2 -1
  19. data/lib/fontisan/stitcher/glyph_copier.rb +2 -1
  20. data/lib/fontisan/stitcher/partition_strategy/by_block.rb +3 -1
  21. data/lib/fontisan/stitcher/partition_strategy/by_script.rb +6 -2
  22. data/lib/fontisan/stitcher/source.rb +2 -1
  23. data/lib/fontisan/stitcher.rb +2 -1
  24. data/lib/fontisan/subset/table_strategy/glyf_loca_builder.rb +2 -1
  25. data/lib/fontisan/svg/standalone_glyph.rb +4 -1
  26. data/lib/fontisan/svg_to_glyf/assembler.rb +6 -3
  27. data/lib/fontisan/svg_to_glyf/geometry/transform_parser.rb +11 -4
  28. data/lib/fontisan/svg_to_glyf/geometry.rb +4 -2
  29. data/lib/fontisan/tables/cff/cff2_charstring_builder.rb +11 -5
  30. data/lib/fontisan/tables/cff.rb +2 -1
  31. data/lib/fontisan/tables/cff2/index_builder.rb +2 -1
  32. data/lib/fontisan/tables.rb +2 -1
  33. data/lib/fontisan/ufo/cli.rb +4 -1
  34. data/lib/fontisan/ufo/compile/cbdt_cblc.rb +2 -1
  35. data/lib/fontisan/ufo/compile/cff.rb +6 -2
  36. data/lib/fontisan/ufo/compile/cmap.rb +11 -5
  37. data/lib/fontisan/ufo/compile/cpal.rb +2 -1
  38. data/lib/fontisan/ufo/compile/feature_writers/gdef.rb +3 -1
  39. data/lib/fontisan/ufo/compile/feature_writers/mark_family_base.rb +3 -1
  40. data/lib/fontisan/ufo/compile/feature_writers.rb +2 -1
  41. data/lib/fontisan/ufo/compile/filters/cubic_to_quadratic.rb +4 -2
  42. data/lib/fontisan/ufo/compile/filters/remove_overlaps.rb +2 -1
  43. data/lib/fontisan/ufo/compile/fvar.rb +7 -3
  44. data/lib/fontisan/ufo/compile/glyf_loca.rb +4 -2
  45. data/lib/fontisan/ufo/compile/gvar.rb +5 -2
  46. data/lib/fontisan/ufo/compile/head.rb +2 -1
  47. data/lib/fontisan/ufo/compile/item_variation_store.rb +2 -1
  48. data/lib/fontisan/ufo/compile/name.rb +3 -1
  49. data/lib/fontisan/ufo/compile/sbix.rb +2 -1
  50. data/lib/fontisan/ufo/compile/stat.rb +6 -3
  51. data/lib/fontisan/ufo/convert/from_bin_data.rb +6 -2
  52. data/lib/fontisan/ufo/convert/to_dfont.rb +6 -2
  53. data/lib/fontisan/ufo/convert/to_otc.rb +4 -1
  54. data/lib/fontisan/ufo/convert/to_postscript.rb +8 -3
  55. data/lib/fontisan/ufo/convert/to_woff.rb +8 -3
  56. data/lib/fontisan/ufo/convert/to_woff2.rb +8 -3
  57. data/lib/fontisan/ufo/convert.rb +4 -1
  58. data/lib/fontisan/ufo/info.rb +4 -2
  59. data/lib/fontisan/ufo/plist.rb +7 -2
  60. data/lib/fontisan/ufo/reader.rb +3 -1
  61. data/lib/fontisan/ufo/writer.rb +13 -5
  62. data/lib/fontisan/version.rb +1 -1
  63. data/lib/fontisan/woff2/collection_decoder.rb +4 -1
  64. data/lib/fontisan/woff2/collection_encoder.rb +4 -1
  65. data/lib/fontisan/woff2/glyf_canonicalizer.rb +2 -1
  66. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +5 -2
  67. data/lib/fontisan/woff2/sfnt_checksum.rb +2 -1
  68. data/lib/fontisan.rb +0 -1
  69. metadata +2 -3
  70. data/lib/fontisan/tasks/fixture_downloader.rb +0 -162
  71. data/lib/fontisan/tasks.rb +0 -11
@@ -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