fontisan 0.4.46 → 0.4.48

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/BENCHMARKS.md +56 -0
  3. data/CHANGELOG.md +12 -0
  4. data/Cargo.lock +550 -0
  5. data/Cargo.toml +12 -0
  6. data/Gemfile +10 -0
  7. data/README.adoc +52 -0
  8. data/Rakefile +5 -1
  9. data/benchmark/baseline.json +5 -0
  10. data/benchmark/ci_gate.rb +217 -0
  11. data/benchmark/corpus/harness.rb +175 -0
  12. data/benchmark/corpus/ops.rb +150 -0
  13. data/benchmark/corpus/run.rb +182 -0
  14. data/benchmark/corpus/scan_child.rb +8 -0
  15. data/benchmark/corpus/tiers.rb +176 -0
  16. data/benchmark/tier_baseline.json +12 -0
  17. data/docs/.vitepress/config.ts +2 -1
  18. data/docs/guide/engines.md +117 -0
  19. data/docs/reference-facade.md +130 -0
  20. data/ext/fontisan_cdylib/Cargo.lock +313 -0
  21. data/ext/fontisan_cdylib/Cargo.toml +18 -0
  22. data/ext/fontisan_cdylib/build.rs +22 -0
  23. data/ext/fontisan_cdylib/extconf.rb +36 -0
  24. data/ext/fontisan_cdylib/src/lib.rs +13 -0
  25. data/ext/fontisan_native/Cargo.toml +21 -0
  26. data/ext/fontisan_native/build.rs +22 -0
  27. data/ext/fontisan_native/clippy.toml +10 -0
  28. data/ext/fontisan_native/extconf.rb +59 -0
  29. data/ext/fontisan_native/src/lib.rs +463 -0
  30. data/lib/fontisan/audit/checks/cmap_check.rb +7 -0
  31. data/lib/fontisan/audit/checks/hinting_check.rb +1 -1
  32. data/lib/fontisan/audit/checks/maxp_check.rb +1 -1
  33. data/lib/fontisan/audit/checks/opentype_conformance_check.rb +5 -2
  34. data/lib/fontisan/base_collection.rb +30 -0
  35. data/lib/fontisan/byte_ops.rb +219 -0
  36. data/lib/fontisan/cli.rb +5 -5
  37. data/lib/fontisan/commands/audit_command.rb +38 -43
  38. data/lib/fontisan/commands/convert_command.rb +53 -0
  39. data/lib/fontisan/commands/instance_command.rb +75 -0
  40. data/lib/fontisan/commands/subset_command.rb +43 -0
  41. data/lib/fontisan/commands/unpack_command.rb +56 -0
  42. data/lib/fontisan/converters/cff_table_builder.rb +1 -1
  43. data/lib/fontisan/converters/collection_converter.rb +13 -5
  44. data/lib/fontisan/converters/format_converter.rb +1 -1
  45. data/lib/fontisan/converters/woff2_encoder.rb +8 -6
  46. data/lib/fontisan/converters/woff_writer.rb +6 -5
  47. data/lib/fontisan/engine.rb +350 -0
  48. data/lib/fontisan/ffi/client.rb +121 -0
  49. data/lib/fontisan/ffi.rb +343 -0
  50. data/lib/fontisan/ffi_platform.rb +24 -0
  51. data/lib/fontisan/font_loader.rb +5 -1
  52. data/lib/fontisan/font_writer.rb +13 -1
  53. data/lib/fontisan/native.rb +231 -0
  54. data/lib/fontisan/pipeline/transformation_pipeline.rb +10 -8
  55. data/lib/fontisan/pipeline.rb +1 -1
  56. data/lib/fontisan/reference/table_digests.rb +251 -0
  57. data/lib/fontisan/reference.rb +619 -0
  58. data/lib/fontisan/scan.rb +372 -0
  59. data/lib/fontisan/sfnt_directory.rb +41 -0
  60. data/lib/fontisan/sfnt_font.rb +16 -6
  61. data/lib/fontisan/subset/builder.rb +2 -1
  62. data/lib/fontisan/tables/cff/charstring.rb +16 -0
  63. data/lib/fontisan/tables/cff.rb +15 -5
  64. data/lib/fontisan/tables/cmap.rb +11 -1
  65. data/lib/fontisan/tables/glyf.rb +4 -6
  66. data/lib/fontisan/tables/gpos.rb +7 -0
  67. data/lib/fontisan/tables/gsub.rb +7 -0
  68. data/lib/fontisan/tables/layout_common.rb +172 -0
  69. data/lib/fontisan/tables/name.rb +10 -4
  70. data/lib/fontisan/utilities/brotli_wrapper.rb +39 -4
  71. data/lib/fontisan/validators/opentype_validator.rb +15 -3
  72. data/lib/fontisan/variable/instancer.rb +23 -192
  73. data/lib/fontisan/variable/static_font_builder.rb +4 -1
  74. data/lib/fontisan/variable/table_updater.rb +4 -2
  75. data/lib/fontisan/variation/instance_generator.rb +3 -149
  76. data/lib/fontisan/variation/instancing/axis_space.rb +136 -0
  77. data/lib/fontisan/variation/instancing/glyph_outline.rb +204 -0
  78. data/lib/fontisan/variation/instancing/gvar_reader.rb +146 -0
  79. data/lib/fontisan/variation/instancing/item_variation_store.rb +92 -0
  80. data/lib/fontisan/variation/instancing/name_rewriter.rb +90 -0
  81. data/lib/fontisan/variation/instancing/true_type_instancer.rb +326 -0
  82. data/lib/fontisan/variation/instancing.rb +20 -0
  83. data/lib/fontisan/variation/variable_svg_generator.rb +19 -1
  84. data/lib/fontisan/variation.rb +1 -0
  85. data/lib/fontisan/version.rb +1 -1
  86. data/lib/fontisan/woff2/collection_decoder.rb +15 -1
  87. data/lib/fontisan/woff2/collection_encoder.rb +29 -6
  88. data/lib/fontisan/woff2/encoder_rules.rb +12 -4
  89. data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +1 -1
  90. data/lib/fontisan/woff2_collection.rb +7 -5
  91. data/lib/fontisan/woff2_font.rb +12 -3
  92. data/lib/fontisan/woff_container.rb +83 -0
  93. data/lib/fontisan/woff_font.rb +24 -20
  94. data/lib/fontisan.rb +24 -1
  95. data/rakelib/bench.rake +9 -0
  96. data/rakelib/cdylib.rake +39 -0
  97. data/rakelib/corpus.rake +272 -0
  98. data/rakelib/corpus_convert.rake +43 -0
  99. data/rakelib/corpus_goldens.rake +22 -0
  100. data/rakelib/corpus_malformed.rake +20 -0
  101. data/rakelib/corpus_scan.rake +24 -0
  102. data/rakelib/java.rake +61 -0
  103. data/rakelib/native.rake +43 -0
  104. data/scripts/build-woff2.sh +57 -0
  105. data/scripts/corpus/generate_convert_goldens.rb +478 -0
  106. data/scripts/corpus/generate_goldens.rb +282 -0
  107. data/scripts/corpus/generate_malformed_goldens.rb +254 -0
  108. data/scripts/corpus/generate_scan_goldens.rb +98 -0
  109. data/scripts/tier_fallback.rb +219 -0
  110. data/scripts/tier_smoke.rb +64 -0
  111. metadata +75 -3
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scripts/corpus/generate_convert_goldens"
4
+
5
+ # Convert goldens (card 0c). `corpus:convert:verify` only compares;
6
+ # `corpus:convert:regenerate` is the one task that writes. TIER=ci|large|all,
7
+ # ONLY=<font-id>[,<font-id>] narrows verify/regenerate. Needs `opentype-sanitizer`
8
+ # (OTS_PYTHON=<python with it>) and the pinned woff2_decompress.
9
+ module ConvertTasks
10
+ module_function
11
+
12
+ def tiers
13
+ Corpus.tiers_for(ENV.fetch("TIER", "ci"))
14
+ end
15
+
16
+ def only
17
+ ENV["ONLY"]&.split(",")
18
+ end
19
+ end
20
+
21
+ namespace :corpus do
22
+ namespace :convert do
23
+ desc "Regenerate convert goldens from the reference (TIER, ONLY)"
24
+ task regenerate: "corpus:fetch" do
25
+ files = ConvertGoldens.generate(ConvertTasks.tiers, only: ConvertTasks.only)
26
+ ConvertGoldens.write(files)
27
+ puts "[corpus:convert:regenerate] wrote #{files.size} goldens"
28
+ rescue ConvertGoldens::ToolMissing, Corpus::Error => e
29
+ abort "[corpus:convert:regenerate] #{e.message}"
30
+ end
31
+
32
+ desc "Compare regenerated convert output with the committed goldens; never writes (TIER, ONLY)"
33
+ task verify: "corpus:fetch" do
34
+ problems = ConvertGoldens.verify(ConvertTasks.tiers, only: ConvertTasks.only)
35
+ problems.concat(ConvertGoldens.ots_failures.reject { |f| ConvertGoldens.listed_exceptions.include?(f) }
36
+ .map { |f| "#{f}: OTS failure not listed in corpus/TRIAGE_CONVERT.md" })
37
+ abort "[corpus:convert:verify] #{problems.size} problem(s):\n #{problems.join("\n ")}" unless problems.empty?
38
+ puts "[corpus:convert:verify] ok"
39
+ rescue ConvertGoldens::ToolMissing => e
40
+ abort "[corpus:convert:verify] #{e.message}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Read goldens (fontist/fontisan#156 card 0b). `corpus:verify` compares only
4
+ # and never writes; `corpus:regenerate` rewrites the goldens and is explicit.
5
+ namespace :corpus do
6
+ desc "Compare the read goldens with fresh output (TIER=ci|large, default ci); never writes"
7
+ task verify: :fetch do
8
+ require_relative "../scripts/corpus/generate_goldens"
9
+ tier = ENV.fetch("TIER", "ci")
10
+ problems = CorpusGoldens.verify(tier)
11
+ abort "[corpus:verify] #{problems.size} problem(s):\n #{problems.join("\n ")}" unless problems.empty?
12
+
13
+ puts "[corpus:verify] tier #{tier}: #{CorpusGoldens.entries(tier).size} read goldens match"
14
+ end
15
+
16
+ desc "Rewrite the read goldens from the corpus (TIER=ci|large); review the diff"
17
+ task regenerate: :fetch do
18
+ require_relative "../scripts/corpus/generate_goldens"
19
+ tier = ENV.fetch("TIER", "ci")
20
+ puts "[corpus:regenerate] wrote #{CorpusGoldens.write(tier)} goldens (tier #{tier})"
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Malformed-input goldens (fontist/fontisan#156, card 0e).
4
+ namespace :corpus do
5
+ namespace :malformed do
6
+ desc "Check corpus/goldens/malformed against current Ruby behaviour"
7
+ task :verify do
8
+ require_relative "../scripts/corpus/generate_malformed_goldens"
9
+ problems = MalformedGoldens.verify
10
+ abort "[corpus:malformed:verify]\n #{problems.join("\n ")}" unless problems.empty?
11
+ puts "[corpus:malformed:verify] ok (#{MalformedGoldens.entries.size} kinds)"
12
+ end
13
+
14
+ desc "Regenerate corpus/goldens/malformed (only in a PR that shows the golden diff)"
15
+ task :regenerate do
16
+ require_relative "../scripts/corpus/generate_malformed_goldens"
17
+ puts "[corpus:malformed:regenerate] wrote #{MalformedGoldens.regenerate} goldens"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Scan goldens (fontist/fontisan#156 card 0f). `corpus:scan:verify` compares
4
+ # only and never writes; `corpus:scan:regenerate` rewrites and is explicit.
5
+ namespace :corpus do
6
+ namespace :scan do
7
+ desc "Compare corpus/goldens/scan with fresh Reference.scan output (TIER=ci|large); never writes"
8
+ task verify: "corpus:fetch" do
9
+ require_relative "../scripts/corpus/generate_scan_goldens"
10
+ tier = ENV.fetch("TIER", "ci")
11
+ problems = ScanGoldens.verify(tier)
12
+ abort "[corpus:scan:verify] #{problems.size} problem(s):\n #{problems.join("\n ")}" unless problems.empty?
13
+
14
+ puts "[corpus:scan:verify] tier #{tier}: #{ScanGoldens.entries(tier).size} scan goldens match"
15
+ end
16
+
17
+ desc "Rewrite the scan goldens (TIER=ci|large); review the diff"
18
+ task regenerate: "corpus:fetch" do
19
+ require_relative "../scripts/corpus/generate_scan_goldens"
20
+ tier = ENV.fetch("TIER", "ci")
21
+ puts "[corpus:scan:regenerate] wrote #{ScanGoldens.write(tier)} goldens (tier #{tier})"
22
+ end
23
+ end
24
+ end
data/rakelib/java.rake ADDED
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `java`-platform gem (fontisan#156 card 2g). `brotli` is a C extension with
4
+ # no JRuby build, and a gemspec is evaluated when the gem is built, so JRuby
5
+ # gets its own gem: no `brotli`, no `rb_sys`, no magnus extension, a runtime
6
+ # dependency on `ffi`, and the cdylib for every bundled OS and architecture
7
+ # under lib/fontisan/ffi/<platform>/ (see Fontisan::FfiPlatform). WOFF2 there is
8
+ # served by the :ffi tier.
9
+ #
10
+ # rake java:gem # builds the host's cdylib, packs what is staged
11
+ # FONTISAN_JAVA_ALL=1 rake java:gem # CI: the four are already staged (extracted
12
+ # # from the platform gems); fail unless all are
13
+ # rake java:stage # only copies the host's cdylib into its directory
14
+ require "fileutils"
15
+ require "rubygems/package"
16
+ require_relative "../lib/fontisan/ffi_platform"
17
+
18
+ # Builds the java gem's specification from fontisan.gemspec.
19
+ module FontisanJavaGem
20
+ STAGED = "lib/fontisan/ffi/*/*fontisan_cdylib.{so,dylib,dll}"
21
+
22
+ def self.spec
23
+ # load caches its result: copy, or the mutations below reach the source spec.
24
+ base = Gem::Specification.load("fontisan.gemspec").dup
25
+ base.platform = "java"
26
+ base.extensions = []
27
+ base.dependencies.delete_if { |d| %w[brotli rb_sys].include?(d.name) }
28
+ base.add_dependency "ffi", "~> 1.15"
29
+ base.files = base.files.reject { |f| f.start_with?("ext/") } + Dir[STAGED]
30
+ base
31
+ end
32
+
33
+ def self.staged_platforms
34
+ Dir[STAGED].map { |f| File.basename(File.dirname(f)) }.uniq
35
+ end
36
+ end
37
+
38
+ namespace :java do
39
+ desc "Build the host cdylib and copy it to lib/fontisan/ffi/<platform>/"
40
+ task :stage do
41
+ next if ENV["FONTISAN_JAVA_ALL"]
42
+
43
+ dir = Fontisan::FfiPlatform.dir or abort "no java-gem directory for #{RbConfig::CONFIG['host_os']}"
44
+ Rake::Task["cdylib:vendor"].invoke
45
+ dest = File.join("lib/fontisan/ffi", dir)
46
+ FileUtils.mkdir_p(dest)
47
+ FileUtils.cp(Dir["lib/fontisan/ffi/*fontisan_cdylib.{so,dylib,dll}"], dest)
48
+ end
49
+
50
+ desc "Build pkg/fontisan-VERSION-java.gem without brotli, bundling the staged cdylibs"
51
+ task gem: :stage do
52
+ missing = Fontisan::FfiPlatform::DIRS - FontisanJavaGem.staged_platforms
53
+ abort "missing cdylibs for: #{missing.join(', ')}" if ENV["FONTISAN_JAVA_ALL"] && !missing.empty?
54
+ puts "bundling cdylibs for: #{FontisanJavaGem.staged_platforms.join(', ')}; not staged: #{missing.join(', ')}"
55
+
56
+ FileUtils.mkdir_p("pkg")
57
+ built = Gem::Package.build(FontisanJavaGem.spec)
58
+ FileUtils.mv(built, "pkg/")
59
+ puts "pkg/#{built}"
60
+ end
61
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds the :native tier (ext/fontisan_native, magnus + rb-sys) into
4
+ # lib/fontisan/native/ with `rake compile`, and the platform gems with
5
+ # `rake native gem` (this machine) or rb-sys-dock / oxidize-rb/actions/cross-gem
6
+ # (see .github/workflows/gem-build.yml). Needs a Rust toolchain: without cargo
7
+ # (or the rb_sys gem) the task is not defined and every other task still loads.
8
+ cargo = begin
9
+ system("cargo", "--version", out: File::NULL, err: File::NULL)
10
+ rescue SystemCallError
11
+ false
12
+ end
13
+
14
+ # A musl cdylib cannot link with a static C runtime, and the native extension
15
+ # is itself a shared object. rb-sys-dock exports the target as RUST_TARGET.
16
+ rust_target = ENV["CARGO_BUILD_TARGET"] || ENV["RUST_TARGET"] || ENV.fetch("RUBY_TARGET", nil)
17
+ if rust_target.to_s.include?("musl") && !ENV.fetch("RUSTFLAGS", "").include?("crt-static")
18
+ ENV["RUSTFLAGS"] = [ENV.fetch("RUSTFLAGS", nil), "-C target-feature=-crt-static"].compact.join(" ")
19
+ end
20
+
21
+ if cargo
22
+ begin
23
+ require "rb_sys/extensiontask"
24
+
25
+ RbSys::ExtensionTask.new("fontisan_native", Gem::Specification.load("fontisan.gemspec")) do |ext|
26
+ ext.ext_dir = "ext/fontisan_native"
27
+ ext.lib_dir = "lib/fontisan/native"
28
+
29
+ # Runs when a platform gem's spec is assembled. The platform gem carries
30
+ # its binaries and builds nothing at install: no extension, no rb_sys, no
31
+ # cdylib sources, and the cdylib built for this target (lib/fontisan/ffi)
32
+ # next to the magnus extension, so the :ffi tier works too.
33
+ ext.cross_compiling do |spec|
34
+ Rake::Task["cdylib:vendor"].invoke
35
+ spec.extensions.clear
36
+ spec.files.reject! { |f| f.start_with?("ext/") }
37
+ spec.files += Dir["lib/fontisan/ffi/*.{so,dylib,dll}"]
38
+ end
39
+ end
40
+ rescue LoadError
41
+ # rb_sys / rake-compiler absent: nothing to compile.
42
+ end
43
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Build Google's woff2_decompress from a pinned source commit (brotli comes
3
+ # pinned via the woff2 submodule). Used by the corpus to cross-check the
4
+ # WOFF2 collection and, later, to compare WOFF/WOFF2 encode output after
5
+ # decoding (compressed bytes depend on the brotli build, so they are not
6
+ # compared directly).
7
+ #
8
+ # Why `make` and not CMake: upstream's CMakeLists.txt resolves brotli with
9
+ # find_package (the SYSTEM brotli, measured: a CMake build on macOS linked
10
+ # /opt/homebrew brotli), which would silently un-pin brotli. The Makefile
11
+ # compiles the brotli submodule sources into the binary, so the whole decoder
12
+ # is pinned by one commit.
13
+ #
14
+ # Linux / macOS: needs git, make, a C/C++ compiler.
15
+ # Windows: run from an MSYS2 MINGW64 shell with
16
+ # pacman -S --needed git make mingw-w64-x86_64-gcc
17
+ # (upstream google/woff2 has no Windows CI; this path is untested upstream.)
18
+ #
19
+ # Output: prints the absolute path of woff2_decompress on stdout (last line).
20
+ # Override the install dir with WOFF2_DIR.
21
+ set -euo pipefail
22
+
23
+ PIN="7b67e12355fe7da148db5c19d4261f0a498a9537"
24
+ REPO="https://github.com/google/woff2.git"
25
+ root="$(cd "$(dirname "$0")/.." && pwd)"
26
+ dest="${WOFF2_DIR:-$root/corpus/fonts/.tools/woff2}"
27
+ src="$dest/src"
28
+ exe="$dest/bin/woff2_decompress"
29
+ case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) exe="$exe.exe" ;; esac
30
+
31
+ if [ -x "$exe" ] && [ "$(cat "$dest/PIN" 2>/dev/null || true)" = "$PIN make" ]; then
32
+ echo "woff2_decompress already built at pin $PIN" >&2
33
+ echo "$exe"
34
+ exit 0
35
+ fi
36
+
37
+ command -v git >/dev/null || { echo "build-woff2: git is required" >&2; exit 1; }
38
+ command -v make >/dev/null || { echo "build-woff2: make is required" >&2; exit 1; }
39
+
40
+ rm -rf "$dest"
41
+ mkdir -p "$dest/bin"
42
+ git clone --recursive "$REPO" "$src" >&2
43
+ (
44
+ cd "$src"
45
+ git checkout --quiet "$PIN"
46
+ git submodule update --init --recursive >&2
47
+ test "$(git rev-parse HEAD)" = "$PIN"
48
+ )
49
+
50
+ (cd "$src" && make woff2_decompress -j"$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)" >&2)
51
+
52
+ built="$src/woff2_decompress"
53
+ [ -f "$built" ] || built="$src/woff2_decompress.exe"
54
+ [ -f "$built" ] || { echo "build-woff2: woff2_decompress not produced" >&2; exit 1; }
55
+ cp "$built" "$exe"
56
+ echo "$PIN make" > "$dest/PIN"
57
+ echo "$exe"