From 859725ed5b75ba57c4344f68f488c407291f6f3a Mon Sep 17 00:00:00 2001 From: Federico Mena Quintero Date: Mon, 8 May 2023 13:25:18 -0600 Subject: (#956): Rename rsvg-convert directory to rsvg_convert to avoid clashing with the rsvg-convert binary When building with autotools and srcdir==builddir, ./target/.../rsvg-convert gets moved into ./rsvg-convert. But since ./rsvg-convert is already a directory, we get an ./rsvg-convert/rsvg-convert binary. Later, "make install" finds that ./rsvg-convert is a directory, not a file, and fails to install the binary. This renames the rsvg-convert directory to rsvg_convert, but makes Cargo keep the name of the binary as rsvg-convert. Another option would be to let Cargo use rsvg_convert per the package/directory name, and rename the resulting binary. Thanks to Abderrahim Kitouni for figuring this out! Fixes https://gitlab.gnome.org/GNOME/librsvg/-/issues/956 Part-of: --- Cargo.lock | 100 +- Cargo.toml | 4 +- Makefile.am | 24 +- rsvg-convert/Cargo.toml | 43 - rsvg-convert/build.rs | 3 - rsvg-convert/src/main.rs | 1552 -------------------- rsvg-convert/tests/fixtures/a-link.svg | 6 - rsvg-convert/tests/fixtures/accept-language-de.png | Bin 173 -> 0 bytes rsvg-convert/tests/fixtures/accept-language-es.png | Bin 172 -> 0 bytes .../tests/fixtures/accept-language-fallback.png | Bin 173 -> 0 bytes rsvg-convert/tests/fixtures/accept-language.svg | 7 - .../tests/fixtures/bug521-with-viewbox.svg | 4 - .../tests/fixtures/bug591-vbox-overflow.svg | 11 - .../bug601-zero-stroke-width-render-only-foo.png | Bin 95 -> 0 bytes .../tests/fixtures/bug601-zero-stroke-width.svg | 6 - .../tests/fixtures/bug677-partial-pixel.svg | 7 - rsvg-convert/tests/fixtures/dimensions-in.svg | 4 - rsvg-convert/tests/fixtures/dpi.svg | 5 - rsvg-convert/tests/fixtures/empty.svg | 3 - rsvg-convert/tests/fixtures/example.svg | 5 - rsvg-convert/tests/fixtures/geometry-element.svg | 6 - rsvg-convert/tests/fixtures/gimp-wilber-ref.png | Bin 2503 -> 0 bytes rsvg-convert/tests/fixtures/gimp-wilber.svg | 978 ------------ rsvg-convert/tests/fixtures/hello-world.svg | 11 - rsvg-convert/tests/fixtures/offset-png.png | Bin 1426 -> 0 bytes rsvg-convert/tests/fixtures/sub-rect-no-unit.svg | 13 - rsvg-convert/tests/fixtures/text-a-link.svg | 14 - rsvg-convert/tests/fixtures/zero-offset-png.png | Bin 1419 -> 0 bytes rsvg-convert/tests/internal_predicates/file.rs | 28 - rsvg-convert/tests/internal_predicates/mod.rs | 4 - rsvg-convert/tests/internal_predicates/pdf.rs | 358 ----- rsvg-convert/tests/internal_predicates/png.rs | 193 --- rsvg-convert/tests/internal_predicates/svg.rs | 179 --- rsvg-convert/tests/rsvg_convert.rs | 1078 -------------- rsvg_convert/Cargo.toml | 50 + rsvg_convert/build.rs | 3 + rsvg_convert/src/main.rs | 1552 ++++++++++++++++++++ rsvg_convert/tests/fixtures/a-link.svg | 6 + rsvg_convert/tests/fixtures/accept-language-de.png | Bin 0 -> 173 bytes rsvg_convert/tests/fixtures/accept-language-es.png | Bin 0 -> 172 bytes .../tests/fixtures/accept-language-fallback.png | Bin 0 -> 173 bytes rsvg_convert/tests/fixtures/accept-language.svg | 7 + .../tests/fixtures/bug521-with-viewbox.svg | 4 + .../tests/fixtures/bug591-vbox-overflow.svg | 11 + .../bug601-zero-stroke-width-render-only-foo.png | Bin 0 -> 95 bytes .../tests/fixtures/bug601-zero-stroke-width.svg | 6 + .../tests/fixtures/bug677-partial-pixel.svg | 7 + rsvg_convert/tests/fixtures/dimensions-in.svg | 4 + rsvg_convert/tests/fixtures/dpi.svg | 5 + rsvg_convert/tests/fixtures/empty.svg | 3 + rsvg_convert/tests/fixtures/example.svg | 5 + rsvg_convert/tests/fixtures/geometry-element.svg | 6 + rsvg_convert/tests/fixtures/gimp-wilber-ref.png | Bin 0 -> 2503 bytes rsvg_convert/tests/fixtures/gimp-wilber.svg | 978 ++++++++++++ rsvg_convert/tests/fixtures/hello-world.svg | 11 + rsvg_convert/tests/fixtures/offset-png.png | Bin 0 -> 1426 bytes rsvg_convert/tests/fixtures/sub-rect-no-unit.svg | 13 + rsvg_convert/tests/fixtures/text-a-link.svg | 14 + rsvg_convert/tests/fixtures/zero-offset-png.png | Bin 0 -> 1419 bytes rsvg_convert/tests/internal_predicates/file.rs | 28 + rsvg_convert/tests/internal_predicates/mod.rs | 4 + rsvg_convert/tests/internal_predicates/pdf.rs | 358 +++++ rsvg_convert/tests/internal_predicates/png.rs | 193 +++ rsvg_convert/tests/internal_predicates/svg.rs | 179 +++ rsvg_convert/tests/rsvg_convert.rs | 1078 ++++++++++++++ 65 files changed, 4585 insertions(+), 4586 deletions(-) delete mode 100644 rsvg-convert/Cargo.toml delete mode 100644 rsvg-convert/build.rs delete mode 100644 rsvg-convert/src/main.rs delete mode 100644 rsvg-convert/tests/fixtures/a-link.svg delete mode 100644 rsvg-convert/tests/fixtures/accept-language-de.png delete mode 100644 rsvg-convert/tests/fixtures/accept-language-es.png delete mode 100644 rsvg-convert/tests/fixtures/accept-language-fallback.png delete mode 100644 rsvg-convert/tests/fixtures/accept-language.svg delete mode 100644 rsvg-convert/tests/fixtures/bug521-with-viewbox.svg delete mode 100644 rsvg-convert/tests/fixtures/bug591-vbox-overflow.svg delete mode 100644 rsvg-convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png delete mode 100644 rsvg-convert/tests/fixtures/bug601-zero-stroke-width.svg delete mode 100644 rsvg-convert/tests/fixtures/bug677-partial-pixel.svg delete mode 100644 rsvg-convert/tests/fixtures/dimensions-in.svg delete mode 100644 rsvg-convert/tests/fixtures/dpi.svg delete mode 100644 rsvg-convert/tests/fixtures/empty.svg delete mode 100644 rsvg-convert/tests/fixtures/example.svg delete mode 100644 rsvg-convert/tests/fixtures/geometry-element.svg delete mode 100644 rsvg-convert/tests/fixtures/gimp-wilber-ref.png delete mode 100644 rsvg-convert/tests/fixtures/gimp-wilber.svg delete mode 100644 rsvg-convert/tests/fixtures/hello-world.svg delete mode 100644 rsvg-convert/tests/fixtures/offset-png.png delete mode 100644 rsvg-convert/tests/fixtures/sub-rect-no-unit.svg delete mode 100644 rsvg-convert/tests/fixtures/text-a-link.svg delete mode 100644 rsvg-convert/tests/fixtures/zero-offset-png.png delete mode 100644 rsvg-convert/tests/internal_predicates/file.rs delete mode 100644 rsvg-convert/tests/internal_predicates/mod.rs delete mode 100644 rsvg-convert/tests/internal_predicates/pdf.rs delete mode 100644 rsvg-convert/tests/internal_predicates/png.rs delete mode 100644 rsvg-convert/tests/internal_predicates/svg.rs delete mode 100644 rsvg-convert/tests/rsvg_convert.rs create mode 100644 rsvg_convert/Cargo.toml create mode 100644 rsvg_convert/build.rs create mode 100644 rsvg_convert/src/main.rs create mode 100644 rsvg_convert/tests/fixtures/a-link.svg create mode 100644 rsvg_convert/tests/fixtures/accept-language-de.png create mode 100644 rsvg_convert/tests/fixtures/accept-language-es.png create mode 100644 rsvg_convert/tests/fixtures/accept-language-fallback.png create mode 100644 rsvg_convert/tests/fixtures/accept-language.svg create mode 100644 rsvg_convert/tests/fixtures/bug521-with-viewbox.svg create mode 100644 rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg create mode 100644 rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png create mode 100644 rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg create mode 100644 rsvg_convert/tests/fixtures/bug677-partial-pixel.svg create mode 100644 rsvg_convert/tests/fixtures/dimensions-in.svg create mode 100644 rsvg_convert/tests/fixtures/dpi.svg create mode 100644 rsvg_convert/tests/fixtures/empty.svg create mode 100644 rsvg_convert/tests/fixtures/example.svg create mode 100644 rsvg_convert/tests/fixtures/geometry-element.svg create mode 100644 rsvg_convert/tests/fixtures/gimp-wilber-ref.png create mode 100644 rsvg_convert/tests/fixtures/gimp-wilber.svg create mode 100644 rsvg_convert/tests/fixtures/hello-world.svg create mode 100644 rsvg_convert/tests/fixtures/offset-png.png create mode 100644 rsvg_convert/tests/fixtures/sub-rect-no-unit.svg create mode 100644 rsvg_convert/tests/fixtures/text-a-link.svg create mode 100644 rsvg_convert/tests/fixtures/zero-offset-png.png create mode 100644 rsvg_convert/tests/internal_predicates/file.rs create mode 100644 rsvg_convert/tests/internal_predicates/mod.rs create mode 100644 rsvg_convert/tests/internal_predicates/pdf.rs create mode 100644 rsvg_convert/tests/internal_predicates/png.rs create mode 100644 rsvg_convert/tests/internal_predicates/svg.rs create mode 100644 rsvg_convert/tests/rsvg_convert.rs diff --git a/Cargo.lock b/Cargo.lock index d5a40c75..c98e64b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6342bd4f5a1205d7f41e94a41a901f5647c938cdfa96036338e8533c9d6c2450" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" dependencies = [ "anstyle", "anstyle-parse", @@ -83,9 +83,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "approx" @@ -279,9 +279,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.24" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef2b3ded6a26dfaec672a742c93c8cf6b689220324da509ec5caa20de55dc83" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "bitflags", "clap_lex 0.2.4", @@ -291,9 +291,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.2.4" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62" +checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" dependencies = [ "clap_builder", "clap_derive", @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.4" +version = "4.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749" +checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" dependencies = [ "anstream", "anstyle", @@ -320,7 +320,7 @@ version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a19591b2ab0e3c04b588a0e04ddde7b9eaa423646d1b4a8092879216bf47473" dependencies = [ - "clap 4.2.4", + "clap 4.2.7", ] [[package]] @@ -403,7 +403,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.24", + "clap 3.2.25", "criterion-plot", "itertools", "lazy_static", @@ -664,12 +664,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", - "miniz_oxide 0.6.2", + "miniz_oxide", ] [[package]] @@ -1075,9 +1075,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.142" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libloading" @@ -1178,9 +1178,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" [[package]] name = "locale_config" @@ -1269,10 +1269,11 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matrixmultiply" -version = "0.3.3" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb99c395ae250e1bf9133673f03ca9f97b7e71b705436bf8f089453445d1e9fe" +checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" dependencies = [ + "autocfg", "rawpointer", ] @@ -1297,15 +1298,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1676,9 +1668,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plotters" @@ -1718,7 +1710,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.7.1", + "miniz_oxide", ] [[package]] @@ -1848,9 +1840,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -2041,20 +2033,20 @@ version = "2.56.0" dependencies = [ "anyhow", "cairo-rs", - "clap 4.2.4", + "clap 4.2.7", "librsvg", "thiserror", ] [[package]] -name = "rsvg-convert" +name = "rsvg_convert" version = "2.56.0" dependencies = [ "assert_cmd", "cairo-rs", "cast", "chrono", - "clap 4.2.4", + "clap 4.2.7", "clap_complete", "cssparser", "float-cmp", @@ -2082,9 +2074,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.14" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b864d3c18a5785a05953adeed93e2dca37ed30f18e69bba9f30079d51f363f" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", @@ -2168,18 +2160,18 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" dependencies = [ "proc-macro2", "quote", @@ -2318,9 +2310,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.5" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fe581ad25d11420b873cf9aedaca0419c2b411487b134d4d21065f3d092055" +checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" dependencies = [ "cfg-expr", "heck", @@ -2402,9 +2394,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "serde", @@ -2414,15 +2406,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -2845,9 +2837,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 2fc0b429..64eb10d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,10 @@ members = [ "gdk-pixbuf-loader", "librsvg-c", "rsvg", - "rsvg-convert", + "rsvg_convert", "rsvg-bench", ] default-members = [ "rsvg", - "rsvg-convert", + "rsvg_convert", ] diff --git a/Makefile.am b/Makefile.am index 27c8e342..e6176e37 100644 --- a/Makefile.am +++ b/Makefile.am @@ -155,12 +155,12 @@ RUST_EXTRA = \ rsvg/src/test_utils/reference_utils.rs \ rsvg-bench/Cargo.toml \ rsvg-bench/src/main.rs \ - rsvg-convert/tests/internal_predicates/file.rs \ - rsvg-convert/tests/internal_predicates/mod.rs \ - rsvg-convert/tests/internal_predicates/pdf.rs \ - rsvg-convert/tests/internal_predicates/png.rs \ - rsvg-convert/tests/internal_predicates/svg.rs \ - rsvg-convert/tests/rsvg_convert.rs \ + rsvg_convert/tests/internal_predicates/file.rs \ + rsvg_convert/tests/internal_predicates/mod.rs \ + rsvg_convert/tests/internal_predicates/pdf.rs \ + rsvg_convert/tests/internal_predicates/png.rs \ + rsvg_convert/tests/internal_predicates/svg.rs \ + rsvg_convert/tests/rsvg_convert.rs \ librsvg-c/tests/legacy_sizing.rs \ gdk-pixbuf-loader/Cargo.toml \ gdk-pixbuf-loader/src/lib.rs \ @@ -246,9 +246,9 @@ CLEANFILES += $(bin_SCRIPTS) RSVG_CONVERT_BIN=$(CARGO_TARGET_DIR)/$(RUST_TARGET_SUBDIR)/rsvg-convert$(EXEEXT) RSVG_CONVERT_SRC = \ - rsvg-convert/Cargo.toml \ - rsvg-convert/build.rs \ - rsvg-convert/src/main.rs \ + rsvg_convert/Cargo.toml \ + rsvg_convert/build.rs \ + rsvg_convert/src/main.rs \ $(NULL) $(RSVG_CONVERT_BIN): $(RSVG_CONVERT_SRC) | librsvg_c_api.la @@ -256,7 +256,7 @@ $(RSVG_CONVERT_BIN): $(RSVG_CONVERT_SRC) | librsvg_c_api.la PKG_CONFIG_ALLOW_CROSS=1 \ PKG_CONFIG='$(PKG_CONFIG)' \ CARGO_TARGET_DIR=$(CARGO_TARGET_DIR) \ - $(CARGO) --locked build $(CARGO_VERBOSE) $(CARGO_TARGET_ARGS) $(CARGO_RELEASE_ARGS) --package rsvg-convert + $(CARGO) --locked build $(CARGO_VERBOSE) $(CARGO_TARGET_ARGS) $(CARGO_RELEASE_ARGS) --package rsvg_convert rsvg-convert$(EXEEXT): $(RSVG_CONVERT_BIN) cd $(LIBRSVG_BUILD_DIR) && mv $(RSVG_CONVERT_BIN) rsvg-convert$(EXEEXT) @@ -313,8 +313,8 @@ test_fixtures = \ $(wildcard $(srcdir)/rsvg/tests/fixtures/render-crash/*.svg) \ $(wildcard $(srcdir)/rsvg/tests/fixtures/text/*.svg) \ $(wildcard $(srcdir)/rsvg/tests/fixtures/dimensions/*.svg) \ - $(wildcard $(srcdir)/rsvg-convert/tests/fixtures/*.svg) \ - $(wildcard $(srcdir)/rsvg-convert/tests/fixtures/*.png) + $(wildcard $(srcdir)/rsvg_convert/tests/fixtures/*.svg) \ + $(wildcard $(srcdir)/rsvg_convert/tests/fixtures/*.png) EXTRA_DIST = \ $(LIBRSVG_SRC) \ diff --git a/rsvg-convert/Cargo.toml b/rsvg-convert/Cargo.toml deleted file mode 100644 index ab451cb6..00000000 --- a/rsvg-convert/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "rsvg-convert" -version.workspace = true -authors.workspace = true -description.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -edition.workspace = true -rust-version.workspace = true - -[package.metadata.system-deps] -cairo-pdf = { version = "1.16", optional = true } -cairo-ps = { version = "1.16", optional = true } -cairo-svg = { version = "1.16", optional = true } - -[dependencies] -# Keep these in sync with respect to the cairo-rs version: -# src/lib.rs - toplevel example in the docs -cairo-rs = { version = "0.17", features=["v1_16", "png", "pdf", "ps", "svg"] } -cast = "0.3.0" -chrono = { version = "0.4.23", default-features = false, features = ["clock", "std"] } -clap = { version = "4.0.17", features = ["cargo", "derive"] } # rsvg-convert -clap_complete = "4.0.5" # rsvg-convert -cssparser = "0.29.0" -gio = "0.17" -glib = "0.17" -libc = "0.2" -librsvg = { path = "../rsvg" } -librsvg-c = { path = "../librsvg-c" } - -[dev-dependencies] -assert_cmd = "2.0.2" -predicates = "3.0.3" -tempfile = "3" -url = "2" -lopdf = "0.30.0" -png = "0.17.2" -float-cmp = "0.9.0" -librsvg = { path = "../rsvg", features = ["test-utils"] } - -[build-dependencies] -system-deps = "6.0.0" diff --git a/rsvg-convert/build.rs b/rsvg-convert/build.rs deleted file mode 100644 index eec6d526..00000000 --- a/rsvg-convert/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - system_deps::Config::new().probe().unwrap(); -} diff --git a/rsvg-convert/src/main.rs b/rsvg-convert/src/main.rs deleted file mode 100644 index 0a8c527d..00000000 --- a/rsvg-convert/src/main.rs +++ /dev/null @@ -1,1552 +0,0 @@ -use clap::crate_version; -use clap_complete::{Generator, Shell}; - -use gio::prelude::*; -use gio::{Cancellable, FileCreateFlags, InputStream, OutputStream}; - -#[cfg(unix)] -use gio::{UnixInputStream, UnixOutputStream}; - -#[cfg(windows)] -mod windows_imports { - pub use gio::{Win32InputStream, WriteOutputStream}; - pub use glib::ffi::gboolean; - pub use glib::translate::*; - pub use libc::c_void; - pub use std::io; - pub use std::os::windows::io::AsRawHandle; -} -#[cfg(windows)] -use self::windows_imports::*; - -use cssparser::{_cssparser_internal_to_lowercase, match_ignore_ascii_case}; - -use librsvg_c::{handle::PathOrUrl, sizing::LegacySize}; -use rsvg::rsvg_convert_only::{ - AspectRatio, CssLength, Horizontal, Length, Normalize, NormalizeParams, Parse, Signed, ULength, - Unsigned, Validate, Vertical, ViewBox, -}; -use rsvg::{ - AcceptLanguage, CairoRenderer, Color, Dpi, Language, LengthUnit, Loader, Rect, RenderingError, -}; - -use std::ffi::OsString; -use std::io; -use std::ops::Deref; -use std::path::PathBuf; - -#[derive(Debug)] -pub struct Error(String); - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for Error { - fn from(s: cairo::Error) -> Self { - match s { - cairo::Error::InvalidSize => Self(String::from( - "The resulting image would be larger than 32767 pixels on either dimension.\n\ - Librsvg currently cannot render to images bigger than that.\n\ - Please specify a smaller size.", - )), - e => Self(format!("{e}")), - } - } -} - -macro_rules! impl_error_from { - ($err:ty) => { - impl From<$err> for Error { - fn from(e: $err) -> Self { - Self(format!("{e}")) - } - } - }; -} - -impl_error_from!(RenderingError); -impl_error_from!(cairo::IoError); -impl_error_from!(cairo::StreamWithError); -impl_error_from!(clap::Error); - -macro_rules! error { - ($($arg:tt)*) => (Error(std::format!($($arg)*))); -} - -#[derive(Clone, Copy, Debug)] -struct Scale { - pub x: f64, - pub y: f64, -} - -impl Scale { - #[allow(clippy::float_cmp)] - pub fn is_identity(&self) -> bool { - self.x == 1.0 && self.y == 1.0 - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -struct Size { - pub w: f64, - pub h: f64, -} - -impl Size { - pub fn new(w: f64, h: f64) -> Self { - Self { w, h } - } -} - -#[derive(Clone, Copy, Debug)] -enum ResizeStrategy { - Scale(Scale), - Fit { - size: Size, - keep_aspect_ratio: bool, - }, - FitWidth(f64), - FitHeight(f64), - ScaleWithMaxSize { - scale: Scale, - max_width: Option, - max_height: Option, - keep_aspect_ratio: bool, - }, -} - -impl ResizeStrategy { - pub fn apply(self, input: &Size) -> Option { - if input.w == 0.0 || input.h == 0.0 { - return None; - } - - let output_size = match self { - ResizeStrategy::Scale(s) => Size::new(input.w * s.x, input.h * s.y), - - ResizeStrategy::Fit { - size, - keep_aspect_ratio, - } => { - if keep_aspect_ratio { - let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap(); - let rect = aspect.compute( - &ViewBox::from(Rect::from_size(input.w, input.h)), - &Rect::from_size(size.w, size.h), - ); - Size::new(rect.width(), rect.height()) - } else { - size - } - } - - ResizeStrategy::FitWidth(w) => Size::new(w, input.h * w / input.w), - - ResizeStrategy::FitHeight(h) => Size::new(input.w * h / input.h, h), - - ResizeStrategy::ScaleWithMaxSize { - scale, - max_width, - max_height, - keep_aspect_ratio, - } => { - let scaled = Size::new(input.w * scale.x, input.h * scale.y); - - match (max_width, max_height, keep_aspect_ratio) { - (None, None, _) => scaled, - - (Some(max_width), Some(max_height), false) => { - if scaled.w <= max_width && scaled.h <= max_height { - scaled - } else { - Size::new(max_width, max_height) - } - } - - (Some(max_width), Some(max_height), true) => { - if scaled.w <= max_width && scaled.h <= max_height { - scaled - } else { - let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap(); - let rect = aspect.compute( - &ViewBox::from(Rect::from_size(scaled.w, scaled.h)), - &Rect::from_size(max_width, max_height), - ); - Size::new(rect.width(), rect.height()) - } - } - - (Some(max_width), None, false) => { - if scaled.w <= max_width { - scaled - } else { - Size::new(max_width, scaled.h) - } - } - - (Some(max_width), None, true) => { - if scaled.w <= max_width { - scaled - } else { - let factor = max_width / scaled.w; - Size::new(max_width, scaled.h * factor) - } - } - - (None, Some(max_height), false) => { - if scaled.h <= max_height { - scaled - } else { - Size::new(scaled.w, max_height) - } - } - - (None, Some(max_height), true) => { - if scaled.h <= max_height { - scaled - } else { - let factor = max_height / scaled.h; - Size::new(scaled.w * factor, max_height) - } - } - } - } - }; - - Some(output_size) - } -} - -enum Surface { - Png(cairo::ImageSurface, OutputStream), - #[cfg(system_deps_have_cairo_pdf)] - Pdf(cairo::PdfSurface, Size), - #[cfg(system_deps_have_cairo_ps)] - Ps(cairo::PsSurface, Size), - #[cfg(system_deps_have_cairo_svg)] - Svg(cairo::SvgSurface, Size), -} - -impl Deref for Surface { - type Target = cairo::Surface; - - fn deref(&self) -> &cairo::Surface { - match self { - Self::Png(surface, _) => surface, - #[cfg(system_deps_have_cairo_pdf)] - Self::Pdf(surface, _) => surface, - #[cfg(system_deps_have_cairo_ps)] - Self::Ps(surface, _) => surface, - #[cfg(system_deps_have_cairo_svg)] - Self::Svg(surface, _) => surface, - } - } -} - -impl AsRef for Surface { - fn as_ref(&self) -> &cairo::Surface { - self - } -} - -impl Surface { - pub fn new( - format: Format, - size: Size, - stream: OutputStream, - unit: LengthUnit, - ) -> Result { - match format { - Format::Png => Self::new_for_png(size, stream), - Format::Pdf => Self::new_for_pdf(size, stream), - Format::Ps => Self::new_for_ps(size, stream, false), - Format::Eps => Self::new_for_ps(size, stream, true), - Format::Svg => Self::new_for_svg(size, stream, unit), - } - } - - fn new_for_png(size: Size, stream: OutputStream) -> Result { - // We use ceil() to avoid chopping off the last pixel if it is partially covered. - let w = checked_i32(size.w.ceil())?; - let h = checked_i32(size.h.ceil())?; - let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h)?; - Ok(Self::Png(surface, stream)) - } - - #[cfg(system_deps_have_cairo_pdf)] - fn new_for_pdf(size: Size, stream: OutputStream) -> Result { - let surface = cairo::PdfSurface::for_stream(size.w, size.h, stream.into_write())?; - if let Some(date) = metadata::creation_date()? { - surface.set_metadata(cairo::PdfMetadata::CreateDate, &date)?; - } - Ok(Self::Pdf(surface, size)) - } - - #[cfg(not(system_deps_have_cairo_pdf))] - fn new_for_pdf(_size: Size, _stream: OutputStream) -> Result { - Err(Error("unsupported format".to_string())) - } - - #[cfg(system_deps_have_cairo_ps)] - fn new_for_ps(size: Size, stream: OutputStream, eps: bool) -> Result { - let surface = cairo::PsSurface::for_stream(size.w, size.h, stream.into_write())?; - surface.set_eps(eps); - Ok(Self::Ps(surface, size)) - } - - #[cfg(not(system_deps_have_cairo_ps))] - fn new_for_ps(_size: Size, _stream: OutputStream, _eps: bool) -> Result { - Err(Error("unsupported format".to_string())) - } - - #[cfg(system_deps_have_cairo_svg)] - fn new_for_svg(size: Size, stream: OutputStream, unit: LengthUnit) -> Result { - let mut surface = cairo::SvgSurface::for_stream(size.w, size.h, stream.into_write())?; - - let svg_unit = match unit { - LengthUnit::Cm => cairo::SvgUnit::Cm, - LengthUnit::In => cairo::SvgUnit::In, - LengthUnit::Mm => cairo::SvgUnit::Mm, - LengthUnit::Pc => cairo::SvgUnit::Pc, - LengthUnit::Pt => cairo::SvgUnit::Pt, - _ => cairo::SvgUnit::User, - }; - - surface.set_document_unit(svg_unit); - Ok(Self::Svg(surface, size)) - } - - #[cfg(not(system_deps_have_cairo_svg))] - fn new_for_svg(_size: Size, _stream: OutputStream, u: LengthUnit) -> Result { - Err(Error("unsupported format".to_string())) - } - - #[allow(clippy::too_many_arguments)] // yeah, yeah, we'll refactor it eventually - pub fn render( - &self, - renderer: &CairoRenderer, - left: f64, - top: f64, - final_size: Size, - geometry: cairo::Rectangle, - background_color: Option, - id: Option<&str>, - ) -> Result<(), Error> { - let cr = cairo::Context::new(self)?; - - if let Some(Color::RGBA(rgba)) = background_color { - cr.set_source_rgba( - rgba.red_f32().into(), - rgba.green_f32().into(), - rgba.blue_f32().into(), - rgba.alpha_f32().into(), - ); - - cr.paint()?; - } - - cr.translate(left, top); - - // Note that we don't scale the viewport; we change the cr's transform instead. This - // is because SVGs are rendered proportionally to fit within the viewport, regardless - // of the viewport's proportions. Rsvg-convert allows non-proportional scaling, so - // we do that with a separate transform. - - let scale = Scale { - x: final_size.w / geometry.width(), - y: final_size.h / geometry.height(), - }; - - cr.scale(scale.x, scale.y); - - let viewport = cairo::Rectangle::new(0.0, 0.0, geometry.width(), geometry.height()); - - match id { - None => renderer.render_document(&cr, &viewport)?, - Some(_) => renderer.render_element(&cr, id, &viewport)?, - } - - if !matches!(self, Self::Png(_, _)) { - cr.show_page()?; - } - - Ok(()) - } - - pub fn finish(self) -> Result<(), Error> { - match self { - Self::Png(surface, stream) => surface.write_to_png(&mut stream.into_write())?, - _ => self.finish_output_stream().map(|_| ())?, - } - - Ok(()) - } -} - -fn checked_i32(x: f64) -> Result { - cast::i32(x).map_err(|_| cairo::Error::InvalidSize) -} - -mod metadata { - use super::Error; - use chrono::prelude::*; - use std::env; - use std::str::FromStr; - - pub fn creation_date() -> Result, Error> { - match env::var("SOURCE_DATE_EPOCH") { - Ok(epoch) => match i64::from_str(&epoch) { - Ok(seconds) => { - let datetime = Utc.timestamp_opt(seconds, 0).unwrap(); - Ok(Some(datetime.to_rfc3339())) - } - Err(e) => Err(error!("Environment variable $SOURCE_DATE_EPOCH: {}", e)), - }, - Err(env::VarError::NotPresent) => Ok(None), - Err(env::VarError::NotUnicode(_)) => Err(error!( - "Environment variable $SOURCE_DATE_EPOCH is not valid Unicode" - )), - } - } -} - -struct Stdin; - -impl Stdin { - #[cfg(unix)] - pub fn stream() -> InputStream { - let stream = unsafe { UnixInputStream::with_fd(0) }; - stream.upcast::() - } - - #[cfg(windows)] - pub fn stream() -> InputStream { - let stream = unsafe { Win32InputStream::with_handle(io::stdin()) }; - stream.upcast::() - } -} - -struct Stdout; - -impl Stdout { - #[cfg(unix)] - pub fn stream() -> OutputStream { - let stream = unsafe { UnixOutputStream::with_fd(1) }; - stream.upcast::() - } - - #[cfg(windows)] - pub fn stream() -> OutputStream { - // Ideally, we could use a Win32OutputStream, but when it's used with a file redirect, - // it gets buggy. - // https://gitlab.gnome.org/GNOME/librsvg/-/issues/812 - let stream = WriteOutputStream::new(io::stdout()); - stream.upcast::() - } -} - -#[derive(Clone, Debug)] -enum Input { - Stdin, - Named(PathOrUrl), -} - -impl std::fmt::Display for Input { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Input::Stdin => "stdin".fmt(f), - Input::Named(p) => p.fmt(f), - } - } -} - -#[derive(Clone, Debug)] -enum Output { - Stdout, - Path(PathBuf), -} - -impl std::fmt::Display for Output { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Output::Stdout => "stdout".fmt(f), - Output::Path(p) => p.display().fmt(f), - } - } -} - -// Keep this enum in sync with supported_formats in parse_args() -#[derive(Clone, Copy, Debug)] -enum Format { - Png, - Pdf, - Ps, - Eps, - Svg, -} - -struct Converter { - pub dpi_x: Resolution, - pub dpi_y: Resolution, - pub zoom: Scale, - pub width: Option>, - pub height: Option>, - pub left: Option>, - pub top: Option>, - pub page_size: Option<(ULength, ULength)>, - pub format: Format, - pub export_id: Option, - pub keep_aspect_ratio: bool, - pub background_color: Option, - pub stylesheet: Option, - pub language: Language, - pub unlimited: bool, - pub keep_image_data: bool, - pub input: Vec, - pub output: Output, - pub testing: bool, -} - -impl Converter { - pub fn convert(self) -> Result<(), Error> { - let stylesheet = match self.stylesheet { - Some(ref p) => std::fs::read_to_string(p) - .map(Some) - .map_err(|e| error!("Error reading stylesheet: {}", e))?, - None => None, - }; - - let mut surface: Option = None; - - // Use user units per default - let mut unit = LengthUnit::Px; - - fn set_unit( - l: CssLength, - p: &NormalizeParams, - u: LengthUnit, - ) -> f64 { - match u { - LengthUnit::Pt => l.to_points(p), - LengthUnit::In => l.to_inches(p), - LengthUnit::Cm => l.to_cm(p), - LengthUnit::Mm => l.to_mm(p), - LengthUnit::Pc => l.to_picas(p), - _ => l.to_user(p), - } - } - - for (page_idx, input) in self.input.iter().enumerate() { - let (stream, basefile) = match input { - Input::Stdin => (Stdin::stream(), None), - Input::Named(p) => { - let file = p.get_gfile(); - let stream = file - .read(None::<&Cancellable>) - .map_err(|e| error!("Error reading file \"{}\": {}", input, e))?; - (stream.upcast::(), Some(file)) - } - }; - - let mut handle = Loader::new() - .with_unlimited_size(self.unlimited) - .keep_image_data(self.keep_image_data) - .read_stream(&stream, basefile.as_ref(), None::<&Cancellable>) - .map_err(|e| error!("Error reading SVG {}: {}", input, e))?; - - if let Some(ref css) = stylesheet { - handle - .set_stylesheet(css) - .map_err(|e| error!("Error applying stylesheet: {}", e))?; - } - - let renderer = CairoRenderer::new(&handle) - .with_dpi(self.dpi_x.0, self.dpi_y.0) - .with_language(&self.language) - .test_mode(self.testing); - - let geometry = natural_geometry(&renderer, input, self.export_id.as_deref())?; - - let natural_size = Size::new(geometry.width(), geometry.height()); - - let params = NormalizeParams::from_dpi(Dpi::new(self.dpi_x.0, self.dpi_y.0)); - - // Convert natural size and requested size to pixels or points, depending on the target format, - let (natural_size, requested_width, requested_height, page_size) = match self.format { - Format::Png => { - // PNG surface requires units in pixels - ( - natural_size, - self.width.map(|l| l.to_user(¶ms)), - self.height.map(|l| l.to_user(¶ms)), - self.page_size.map(|(w, h)| Size { - w: w.to_user(¶ms), - h: h.to_user(¶ms), - }), - ) - } - - Format::Pdf | Format::Ps | Format::Eps => { - // These surfaces require units in points - unit = LengthUnit::Pt; - - ( - Size { - w: ULength::::new(natural_size.w, LengthUnit::Px) - .to_points(¶ms), - h: ULength::::new(natural_size.h, LengthUnit::Px) - .to_points(¶ms), - }, - self.width.map(|l| l.to_points(¶ms)), - self.height.map(|l| l.to_points(¶ms)), - self.page_size.map(|(w, h)| Size { - w: w.to_points(¶ms), - h: h.to_points(¶ms), - }), - ) - } - - Format::Svg => { - let (w_unit, h_unit) = - (self.width.map(|l| l.unit), self.height.map(|l| l.unit)); - - unit = match (w_unit, h_unit) { - (None, None) => LengthUnit::Px, - (None, u) | (u, None) => u.unwrap(), - (u1, u2) => { - if u1 == u2 { - u1.unwrap() - } else { - LengthUnit::Px - } - } - }; - - // Supported SVG units are px, in, cm, mm, pt, pc - ( - Size { - w: set_unit( - ULength::::new(natural_size.w, LengthUnit::Px), - ¶ms, - unit, - ), - h: set_unit( - ULength::::new(natural_size.h, LengthUnit::Px), - ¶ms, - unit, - ), - }, - self.width.map(|l| set_unit(l, ¶ms, unit)), - self.height.map(|l| set_unit(l, ¶ms, unit)), - self.page_size.map(|(w, h)| Size { - w: set_unit(w, ¶ms, unit), - h: set_unit(h, ¶ms, unit), - }), - ) - } - }; - - let strategy = match (requested_width, requested_height) { - // when w and h are not specified, scale to the requested zoom (if any) - (None, None) => ResizeStrategy::Scale(self.zoom), - - // when w and h are specified, but zoom is not, scale to the requested size - (Some(width), Some(height)) if self.zoom.is_identity() => ResizeStrategy::Fit { - size: Size::new(width, height), - keep_aspect_ratio: self.keep_aspect_ratio, - }, - - // if only one between w and h is specified and there is no zoom, scale to the - // requested w or h and use the same scaling factor for the other - (Some(w), None) if self.zoom.is_identity() => ResizeStrategy::FitWidth(w), - (None, Some(h)) if self.zoom.is_identity() => ResizeStrategy::FitHeight(h), - - // otherwise scale the image, but cap the zoom to match the requested size - _ => ResizeStrategy::ScaleWithMaxSize { - scale: self.zoom, - max_width: requested_width, - max_height: requested_height, - keep_aspect_ratio: self.keep_aspect_ratio, - }, - }; - - let final_size = self.final_size(&strategy, &natural_size, input)?; - - // Create the surface once on the first input, - // except for PDF, PS, and EPS, which allow differently-sized pages. - let page_size = page_size.unwrap_or(final_size); - let s = match &mut surface { - Some(s) => { - match s { - #[cfg(system_deps_have_cairo_pdf)] - Surface::Pdf(pdf, size) => { - pdf.set_size(page_size.w, page_size.h).map_err(|e| { - error!( - "Error setting PDF page #{} size {}: {}", - page_idx + 1, - input, - e - ) - })?; - *size = page_size; - } - #[cfg(system_deps_have_cairo_ps)] - Surface::Ps(ps, size) => { - ps.set_size(page_size.w, page_size.h); - *size = page_size; - } - _ => {} - } - s - } - surface @ None => surface.insert(self.create_surface(page_size, unit)?), - }; - - let left = self.left.map(|l| set_unit(l, ¶ms, unit)).unwrap_or(0.0); - let top = self.top.map(|l| set_unit(l, ¶ms, unit)).unwrap_or(0.0); - - s.render( - &renderer, - left, - top, - final_size, - geometry, - self.background_color, - self.export_id.as_deref(), - ) - .map_err(|e| error!("Error rendering SVG {}: {}", input, e))? - } - - if let Some(s) = surface.take() { - s.finish() - .map_err(|e| error!("Error saving output {}: {}", self.output, e))? - }; - - Ok(()) - } - - fn final_size( - &self, - strategy: &ResizeStrategy, - natural_size: &Size, - input: &Input, - ) -> Result { - strategy - .apply(natural_size) - .ok_or_else(|| error!("The SVG {} has no dimensions", input)) - } - - fn create_surface(&self, size: Size, unit: LengthUnit) -> Result { - let output_stream = match self.output { - Output::Stdout => Stdout::stream(), - Output::Path(ref p) => { - let file = gio::File::for_path(p); - let stream = file - .replace(None, false, FileCreateFlags::NONE, None::<&Cancellable>) - .map_err(|e| error!("Error opening output \"{}\": {}", self.output, e))?; - stream.upcast::() - } - }; - - Surface::new(self.format, size, output_stream, unit) - } -} - -fn natural_geometry( - renderer: &CairoRenderer, - input: &Input, - export_id: Option<&str>, -) -> Result { - match export_id { - None => renderer.legacy_layer_geometry(None), - Some(id) => renderer.geometry_for_element(Some(id)), - } - .map(|(ink_r, _)| ink_r) - .map_err(|e| match e { - RenderingError::IdNotFound => error!( - "File {} does not have an object with id \"{}\")", - input, - export_id.unwrap() - ), - _ => error!("Error rendering SVG {}: {}", input, e), - }) -} - -fn build_cli() -> clap::Command { - let supported_formats = vec![ - "png", - #[cfg(system_deps_have_cairo_pdf)] - "pdf", - #[cfg(system_deps_have_cairo_ps)] - "ps", - #[cfg(system_deps_have_cairo_ps)] - "eps", - #[cfg(system_deps_have_cairo_svg)] - "svg", - ]; - - clap::Command::new("rsvg-convert") - .version(concat!("version ", crate_version!())) - .about("Convert SVG files to other image formats") - .disable_version_flag(true) - .disable_help_flag(true) - .arg( - clap::Arg::new("help") - .short('?') - .long("help") - .help("Display the help") - .action(clap::ArgAction::Help) - ) - .arg( - clap::Arg::new("version") - .short('v') - .long("version") - .help("Display the version information") - .action(clap::ArgAction::Version) - ) - .arg( - clap::Arg::new("res_x") - .short('d') - .long("dpi-x") - .num_args(1) - .value_name("number") - .default_value("96") - .value_parser(parse_resolution) - .help("Pixels per inch") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("res_y") - .short('p') - .long("dpi-y") - .num_args(1) - .value_name("number") - .default_value("96") - .value_parser(parse_resolution) - .help("Pixels per inch") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("zoom_x") - .short('x') - .long("x-zoom") - .num_args(1) - .value_name("number") - .conflicts_with("zoom") - .value_parser(parse_zoom_factor) - .help("Horizontal zoom factor") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("zoom_y") - .short('y') - .long("y-zoom") - .num_args(1) - .value_name("number") - .conflicts_with("zoom") - .value_parser(parse_zoom_factor) - .help("Vertical zoom factor") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("zoom") - .short('z') - .long("zoom") - .num_args(1) - .value_name("number") - .value_parser(parse_zoom_factor) - .help("Zoom factor") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("size_x") - .short('w') - .long("width") - .num_args(1) - .value_name("length") - .value_parser(parse_length::) - .help("Width [defaults to the width of the SVG]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("size_y") - .short('h') - .long("height") - .num_args(1) - .value_name("length") - .value_parser(parse_length::) - .help("Height [defaults to the height of the SVG]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("top") - .long("top") - .num_args(1) - .value_name("length") - .value_parser(parse_length::) - .help("Distance between top edge of page and the image [defaults to 0]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("left") - .long("left") - .num_args(1) - .value_name("length") - .value_parser(parse_length::) - .help("Distance between left edge of page and the image [defaults to 0]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("page_width") - .long("page-width") - .num_args(1) - .value_name("length") - .value_parser(parse_length::) - .help("Width of output media [defaults to the width of the SVG]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("page_height") - .long("page-height") - .num_args(1) - .value_name("length") - .value_parser(parse_length::) - .help("Height of output media [defaults to the height of the SVG]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("format") - .short('f') - .long("format") - .num_args(1) - .value_parser(clap::builder::PossibleValuesParser::new(supported_formats.as_slice())) - .ignore_case(true) - .default_value("png") - .help("Output format") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("output") - .short('o') - .long("output") - .num_args(1) - .value_parser(clap::value_parser!(PathBuf)) - .help("Output filename [defaults to stdout]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("export_id") - .short('i') - .long("export-id") - .value_parser(clap::builder::NonEmptyStringValueParser::new()) - .value_name("object id") - .help("SVG id of object to export [default is to export all objects]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("accept-language") - .short('l') - .long("accept-language") - .value_parser(clap::builder::NonEmptyStringValueParser::new()) - .value_name("languages") - .help("Languages to accept, for example \"es-MX,de,en\" [default uses language from the environment]") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("keep_aspect") - .short('a') - .long("keep-aspect-ratio") - .help("Preserve the aspect ratio") - .action(clap::ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("background") - .short('b') - .long("background-color") - .num_args(1) - .value_name("color") - .value_parser(clap::builder::NonEmptyStringValueParser::new()) - .default_value("none") - .help("Set the background color using a CSS color spec") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("stylesheet") - .short('s') - .long("stylesheet") - .num_args(1) - .value_parser(clap::value_parser!(PathBuf)) - .value_name("filename.css") - .help("Filename of CSS stylesheet to apply") - .action(clap::ArgAction::Set), - ) - .arg( - clap::Arg::new("unlimited") - .short('u') - .long("unlimited") - .help("Allow huge SVG files") - .action(clap::ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("keep_image_data") - .long("keep-image-data") - .help("Keep image data") - .conflicts_with("no_keep_image_data") - .action(clap::ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("no_keep_image_data") - .long("no-keep-image-data") - .help("Do not keep image data") - .conflicts_with("keep_image_data") - .action(clap::ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("testing") - .long("testing") - .help("Render images for librsvg's test suite") - .hide(true) - .action(clap::ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("completion") - .long("completion") - .help("Output shell completion for the given shell") - .num_args(1) - .action(clap::ArgAction::Set) - .value_parser(clap::value_parser!(Shell)), - ) - .arg( - clap::Arg::new("FILE") - .value_parser(clap::value_parser!(OsString)) - .help("The input file(s) to convert") - .num_args(1..) - .action(clap::ArgAction::Append), - ) -} - -fn print_completions(gen: G, cmd: &mut clap::Command) { - clap_complete::generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); -} - -fn parse_args() -> Result { - let cli = build_cli(); - let matches = cli.get_matches(); - - if let Some(shell) = matches.get_one::("completion").copied() { - let mut cmd = build_cli(); - eprintln!("Generating completion file for {shell}"); - print_completions(shell, &mut cmd); - std::process::exit(0); - } - - let format_str: &String = matches - .get_one("format") - .expect("already provided default_value"); - - let format = match_ignore_ascii_case! { - format_str, - "png" => Format::Png, - "pdf" => Format::Pdf, - "ps" => Format::Ps, - "eps" => Format::Eps, - "svg" => Format::Svg, - _ => unreachable!("clap should already have the list of possible values"), - }; - - let keep_image_data = match format { - Format::Ps | Format::Eps | Format::Pdf => !matches.get_flag("no_keep_image_data"), - _ => matches.get_flag("keep_image_data"), - }; - - let language = match matches.get_one::("accept-language") { - None => Language::FromEnvironment, - Some(s) => AcceptLanguage::parse(s) - .map(Language::AcceptLanguage) - .map_err(|e| { - let desc = format!("{e}"); - clap::Error::raw(clap::error::ErrorKind::InvalidValue, desc) - })?, - }; - - let background_str: &String = matches - .get_one("background") - .expect("already provided default_value"); - let background_color: Option = parse_background_color(background_str) - .map_err(|e| clap::Error::raw(clap::error::ErrorKind::InvalidValue, e))?; - - // librsvg expects ids starting with '#', so it can lookup ids in externs like "subfile.svg#subid". - // For the user's convenience, we prepend '#' automatically; we only support specifying ids from - // the toplevel, and don't expect users to lookup things in externs. - let lookup_id = |id: &String| { - if id.starts_with('#') { - id.clone() - } else { - format!("#{id}") - } - }; - - let width: Option> = matches.get_one("size_x").copied(); - let height: Option> = matches.get_one("size_y").copied(); - - let left: Option> = matches.get_one("left").copied(); - let top: Option> = matches.get_one("top").copied(); - - let page_width: Option> = matches.get_one("page_width").copied(); - let page_height: Option> = matches.get_one("page_height").copied(); - - let page_size = match (page_width, page_height) { - (None, None) => None, - (Some(_), None) | (None, Some(_)) => { - return Err(error!( - "Please specify both the --page-width and --page-height options together." - )); - } - (Some(w), Some(h)) => Some((w, h)), - }; - - let dpi_x = *matches - .get_one::("res_x") - .expect("already provided default_value"); - let dpi_y = *matches - .get_one::("res_y") - .expect("already provided default_value"); - - let zoom: Option = matches.get_one("zoom").copied(); - let zoom_x: Option = matches.get_one("zoom_x").copied(); - let zoom_y: Option = matches.get_one("zoom_y").copied(); - - let input = match matches.get_many::("FILE") { - Some(values) => values - .map(|f| PathOrUrl::from_os_str(f).map_err(Error)) - .map(|r| r.map(Input::Named)) - .collect::, Error>>()?, - - None => vec![Input::Stdin], - }; - - if input.len() > 1 && !matches!(format, Format::Ps | Format::Eps | Format::Pdf) { - return Err(error!( - "Multiple SVG files are only allowed for PDF and (E)PS output." - )); - } - - let export_id: Option = matches.get_one::("export_id").map(lookup_id); - - let output = match matches.get_one::("output") { - None => Output::Stdout, - Some(path) => Output::Path(path.clone()), - }; - - Ok(Converter { - dpi_x, - dpi_y, - zoom: Scale { - x: zoom.or(zoom_x).map(|factor| factor.0).unwrap_or(1.0), - y: zoom.or(zoom_y).map(|factor| factor.0).unwrap_or(1.0), - }, - width, - height, - left, - top, - page_size, - format, - export_id, - keep_aspect_ratio: matches.get_flag("keep_aspect"), - background_color, - stylesheet: matches.get_one("stylesheet").cloned(), - unlimited: matches.get_flag("unlimited"), - keep_image_data, - language, - input, - output, - testing: matches.get_flag("testing"), - }) -} - -#[derive(Copy, Clone)] -struct Resolution(f64); - -fn parse_resolution(v: &str) -> Result { - match v.parse::() { - Ok(res) if res > 0.0 => Ok(Resolution(res)), - Ok(_) => Err(String::from("Invalid resolution")), - Err(e) => Err(format!("{e}")), - } -} - -#[derive(Copy, Clone)] -struct ZoomFactor(f64); - -fn parse_zoom_factor(v: &str) -> Result { - match v.parse::() { - Ok(res) if res > 0.0 => Ok(ZoomFactor(res)), - Ok(_) => Err(String::from("Invalid zoom factor")), - Err(e) => Err(format!("{e}")), - } -} - -trait NotFound { - type Ok; - type Error; - - fn or_none(self) -> Result, Self::Error>; -} - -impl NotFound for Result { - type Ok = T; - type Error = clap::Error; - - /// Maps the Result to an Option, translating the ArgumentNotFound error to - /// Ok(None), while mapping other kinds of errors to Err(e). - /// - /// This allows to get proper error reporting for invalid values on optional - /// arguments. - fn or_none(self) -> Result, clap::Error> { - self.map_or_else( - |e| match e.kind() { - clap::error::ErrorKind::UnknownArgument => Ok(None), - _ => Err(e), - }, - |v| Ok(Some(v)), - ) - } -} - -fn parse_background_color(s: &str) -> Result, String> { - match s { - "none" | "None" => Ok(None), - _ => ::parse_str(s).map(Some).map_err(|_| { - format!("Invalid value: The argument '{s}' can not be parsed as a CSS color value") - }), - } -} - -fn is_absolute_unit(u: LengthUnit) -> bool { - use LengthUnit::*; - - match u { - Percent | Em | Ex => false, - Px | In | Cm | Mm | Pt | Pc => true, - } -} - -fn parse_length(s: &str) -> Result, String> { - as Parse>::parse_str(s) - .map_err(|_| format!("Invalid value: The argument '{s}' can not be parsed as a length")) - .and_then(|l| { - if is_absolute_unit(l.unit) { - Ok(l) - } else { - Err(format!( - "Invalid value '{s}': supported units are px, in, cm, mm, pt, pc" - )) - } - }) -} - -fn main() { - if let Err(e) = parse_args().and_then(|converter| converter.convert()) { - std::eprintln!("{e}"); - std::process::exit(1); - } -} - -#[cfg(test)] -mod color_tests { - use super::*; - - #[test] - fn valid_color_is_ok() { - assert!(parse_background_color("Red").is_ok()); - } - - #[test] - fn none_is_handled_as_transparent() { - assert_eq!(parse_background_color("None").unwrap(), None,); - } - - #[test] - fn invalid_is_handled_as_invalid_value() { - assert!(parse_background_color("foo").is_err()); - } -} - -#[cfg(test)] -mod sizing_tests { - use super::*; - - #[test] - fn detects_empty_size() { - let strategy = ResizeStrategy::Scale(Scale { x: 42.0, y: 42.0 }); - assert!(strategy.apply(&Size::new(0.0, 0.0)).is_none()); - } - - #[test] - fn scale() { - let strategy = ResizeStrategy::Scale(Scale { x: 2.0, y: 3.0 }); - assert_eq!( - strategy.apply(&Size::new(1.0, 2.0)).unwrap(), - Size::new(2.0, 6.0), - ); - } - - #[test] - fn fit_non_proportional() { - let strategy = ResizeStrategy::Fit { - size: Size::new(40.0, 10.0), - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(2.0, 1.0)).unwrap(), - Size::new(40.0, 10.0), - ); - } - - #[test] - fn fit_proportional_wider_than_tall() { - let strategy = ResizeStrategy::Fit { - size: Size::new(40.0, 10.0), - keep_aspect_ratio: true, - }; - - assert_eq!( - strategy.apply(&Size::new(2.0, 1.0)).unwrap(), - Size::new(20.0, 10.0), - ); - } - - #[test] - fn fit_proportional_taller_than_wide() { - let strategy = ResizeStrategy::Fit { - size: Size::new(100.0, 50.0), - keep_aspect_ratio: true, - }; - - assert_eq!( - strategy.apply(&Size::new(1.0, 2.0)).unwrap(), - Size::new(25.0, 50.0), - ); - } - - #[test] - fn fit_width() { - let strategy = ResizeStrategy::FitWidth(100.0); - - assert_eq!( - strategy.apply(&Size::new(1.0, 2.0)).unwrap(), - Size::new(100.0, 200.0), - ); - } - - #[test] - fn fit_height() { - let strategy = ResizeStrategy::FitHeight(100.0); - - assert_eq!( - strategy.apply(&Size::new(1.0, 2.0)).unwrap(), - Size::new(50.0, 100.0), - ); - } - - #[test] - fn scale_no_max_size_non_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 2.0, y: 3.0 }, - max_width: None, - max_height: None, - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(1.0, 2.0)).unwrap(), - Size::new(2.0, 6.0), - ); - } - - #[test] - fn scale_with_max_width_and_height_fits_non_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 2.0, y: 3.0 }, - max_width: Some(10.0), - max_height: Some(20.0), - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(4.0, 2.0)).unwrap(), - Size::new(8.0, 6.0) - ); - } - - #[test] - fn scale_with_max_width_and_height_fits_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 2.0, y: 3.0 }, - max_width: Some(10.0), - max_height: Some(20.0), - keep_aspect_ratio: true, - }; - - assert_eq!( - strategy.apply(&Size::new(4.0, 2.0)).unwrap(), - Size::new(8.0, 6.0) - ); - } - - #[test] - fn scale_with_max_width_and_height_doesnt_fit_non_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 10.0, y: 20.0 }, - max_width: Some(10.0), - max_height: Some(20.0), - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(4.0, 5.0)).unwrap(), - Size::new(10.0, 20.0) - ); - } - - #[test] - fn scale_with_max_width_and_height_doesnt_fit_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 10.0, y: 20.0 }, - max_width: Some(10.0), - max_height: Some(15.0), - keep_aspect_ratio: true, - }; - - assert_eq!( - // this will end up with a 40:120 aspect ratio - strategy.apply(&Size::new(4.0, 6.0)).unwrap(), - // which should be shrunk to 1:3 that fits in (10, 15) per the max_width/max_height above - Size::new(5.0, 15.0) - ); - } - - #[test] - fn scale_with_max_width_fits_non_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 5.0, y: 20.0 }, - max_width: Some(10.0), - max_height: None, - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(1.0, 10.0)).unwrap(), - Size::new(5.0, 200.0), - ); - } - - #[test] - fn scale_with_max_width_fits_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 5.0, y: 20.0 }, - max_width: Some(10.0), - max_height: None, - keep_aspect_ratio: true, - }; - - assert_eq!( - strategy.apply(&Size::new(1.0, 10.0)).unwrap(), - Size::new(5.0, 200.0), - ); - } - - #[test] - fn scale_with_max_height_fits_non_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 20.0, y: 5.0 }, - max_width: None, - max_height: Some(10.0), - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(10.0, 1.0)).unwrap(), - Size::new(200.0, 5.0), - ); - } - - #[test] - fn scale_with_max_height_fits_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 20.0, y: 5.0 }, - max_width: None, - max_height: Some(10.0), - keep_aspect_ratio: true, - }; - - assert_eq!( - strategy.apply(&Size::new(10.0, 1.0)).unwrap(), - Size::new(200.0, 5.0), - ); - } - - #[test] - fn scale_with_max_width_doesnt_fit_non_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 10.0, y: 20.0 }, - max_width: Some(10.0), - max_height: None, - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(5.0, 10.0)).unwrap(), - Size::new(10.0, 200.0), - ); - } - - #[test] - fn scale_with_max_width_doesnt_fit_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 10.0, y: 20.0 }, - max_width: Some(10.0), - max_height: None, - keep_aspect_ratio: true, - }; - - assert_eq!( - strategy.apply(&Size::new(5.0, 10.0)).unwrap(), - Size::new(10.0, 40.0), - ); - } - - #[test] - fn scale_with_max_height_doesnt_fit_non_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 10.0, y: 20.0 }, - max_width: None, - max_height: Some(10.0), - keep_aspect_ratio: false, - }; - - assert_eq!( - strategy.apply(&Size::new(5.0, 10.0)).unwrap(), - Size::new(50.0, 10.0), - ); - } - - #[test] - fn scale_with_max_height_doesnt_fit_proportional() { - let strategy = ResizeStrategy::ScaleWithMaxSize { - scale: Scale { x: 8.0, y: 20.0 }, - max_width: None, - max_height: Some(10.0), - keep_aspect_ratio: true, - }; - - assert_eq!( - strategy.apply(&Size::new(5.0, 10.0)).unwrap(), - Size::new(2.0, 10.0), - ); - } -} diff --git a/rsvg-convert/tests/fixtures/a-link.svg b/rsvg-convert/tests/fixtures/a-link.svg deleted file mode 100644 index 1ae8ace5..00000000 --- a/rsvg-convert/tests/fixtures/a-link.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/rsvg-convert/tests/fixtures/accept-language-de.png b/rsvg-convert/tests/fixtures/accept-language-de.png deleted file mode 100644 index cc797dc2..00000000 Binary files a/rsvg-convert/tests/fixtures/accept-language-de.png and /dev/null differ diff --git a/rsvg-convert/tests/fixtures/accept-language-es.png b/rsvg-convert/tests/fixtures/accept-language-es.png deleted file mode 100644 index 4cf3a21f..00000000 Binary files a/rsvg-convert/tests/fixtures/accept-language-es.png and /dev/null differ diff --git a/rsvg-convert/tests/fixtures/accept-language-fallback.png b/rsvg-convert/tests/fixtures/accept-language-fallback.png deleted file mode 100644 index 43b20f01..00000000 Binary files a/rsvg-convert/tests/fixtures/accept-language-fallback.png and /dev/null differ diff --git a/rsvg-convert/tests/fixtures/accept-language.svg b/rsvg-convert/tests/fixtures/accept-language.svg deleted file mode 100644 index c132b65d..00000000 --- a/rsvg-convert/tests/fixtures/accept-language.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/rsvg-convert/tests/fixtures/bug521-with-viewbox.svg b/rsvg-convert/tests/fixtures/bug521-with-viewbox.svg deleted file mode 100644 index c3f34e6d..00000000 --- a/rsvg-convert/tests/fixtures/bug521-with-viewbox.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/rsvg-convert/tests/fixtures/bug591-vbox-overflow.svg b/rsvg-convert/tests/fixtures/bug591-vbox-overflow.svg deleted file mode 100644 index 1cee7759..00000000 --- a/rsvg-convert/tests/fixtures/bug591-vbox-overflow.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/rsvg-convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png b/rsvg-convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png deleted file mode 100644 index 1ed070ca..00000000 Binary files a/rsvg-convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png and /dev/null differ diff --git a/rsvg-convert/tests/fixtures/bug601-zero-stroke-width.svg b/rsvg-convert/tests/fixtures/bug601-zero-stroke-width.svg deleted file mode 100644 index ee96d474..00000000 --- a/rsvg-convert/tests/fixtures/bug601-zero-stroke-width.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/rsvg-convert/tests/fixtures/bug677-partial-pixel.svg b/rsvg-convert/tests/fixtures/bug677-partial-pixel.svg deleted file mode 100644 index aeac8c30..00000000 --- a/rsvg-convert/tests/fixtures/bug677-partial-pixel.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/rsvg-convert/tests/fixtures/dimensions-in.svg b/rsvg-convert/tests/fixtures/dimensions-in.svg deleted file mode 100644 index aa4f3219..00000000 --- a/rsvg-convert/tests/fixtures/dimensions-in.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/rsvg-convert/tests/fixtures/dpi.svg b/rsvg-convert/tests/fixtures/dpi.svg deleted file mode 100644 index 499ee206..00000000 --- a/rsvg-convert/tests/fixtures/dpi.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/rsvg-convert/tests/fixtures/empty.svg b/rsvg-convert/tests/fixtures/empty.svg deleted file mode 100644 index 01a940a2..00000000 --- a/rsvg-convert/tests/fixtures/empty.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/rsvg-convert/tests/fixtures/example.svg b/rsvg-convert/tests/fixtures/example.svg deleted file mode 100644 index 850fba3e..00000000 --- a/rsvg-convert/tests/fixtures/example.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/rsvg-convert/tests/fixtures/geometry-element.svg b/rsvg-convert/tests/fixtures/geometry-element.svg deleted file mode 100644 index 3d707cdc..00000000 --- a/rsvg-convert/tests/fixtures/geometry-element.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/rsvg-convert/tests/fixtures/gimp-wilber-ref.png b/rsvg-convert/tests/fixtures/gimp-wilber-ref.png deleted file mode 100644 index 606f2a4d..00000000 Binary files a/rsvg-convert/tests/fixtures/gimp-wilber-ref.png and /dev/null differ diff --git a/rsvg-convert/tests/fixtures/gimp-wilber.svg b/rsvg-convert/tests/fixtures/gimp-wilber.svg deleted file mode 100644 index 97c821a7..00000000 --- a/rsvg-convert/tests/fixtures/gimp-wilber.svg +++ /dev/null @@ -1,978 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - Jakub Steiner - - - http://jimmac.musichall.cz - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/rsvg-convert/tests/fixtures/hello-world.svg b/rsvg-convert/tests/fixtures/hello-world.svg deleted file mode 100644 index 45a65c0f..00000000 --- a/rsvg-convert/tests/fixtures/hello-world.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - Hello world! - Hello again! - diff --git a/rsvg-convert/tests/fixtures/offset-png.png b/rsvg-convert/tests/fixtures/offset-png.png deleted file mode 100644 index a4bc7e2a..00000000 Binary files a/rsvg-convert/tests/fixtures/offset-png.png and /dev/null differ diff --git a/rsvg-convert/tests/fixtures/sub-rect-no-unit.svg b/rsvg-convert/tests/fixtures/sub-rect-no-unit.svg deleted file mode 100644 index fb8312ac..00000000 --- a/rsvg-convert/tests/fixtures/sub-rect-no-unit.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/rsvg-convert/tests/fixtures/text-a-link.svg b/rsvg-convert/tests/fixtures/text-a-link.svg deleted file mode 100644 index d205c768..00000000 --- a/rsvg-convert/tests/fixtures/text-a-link.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - This is a link to example.com - - - - - This is a link to another.example.com - - - - diff --git a/rsvg-convert/tests/fixtures/zero-offset-png.png b/rsvg-convert/tests/fixtures/zero-offset-png.png deleted file mode 100644 index adbe9524..00000000 Binary files a/rsvg-convert/tests/fixtures/zero-offset-png.png and /dev/null differ diff --git a/rsvg-convert/tests/internal_predicates/file.rs b/rsvg-convert/tests/internal_predicates/file.rs deleted file mode 100644 index a7af5acf..00000000 --- a/rsvg-convert/tests/internal_predicates/file.rs +++ /dev/null @@ -1,28 +0,0 @@ -use predicates::prelude::*; -use predicates::str::StartsWithPredicate; - -use super::pdf::PdfPredicate; -use super::png::PngPredicate; -use super::svg::SvgPredicate; - -/// Predicates to check that some output ([u8]) is of a certain file type - -pub fn is_png() -> PngPredicate { - PngPredicate {} -} - -pub fn is_ps() -> StartsWithPredicate { - predicate::str::starts_with("%!PS-Adobe-3.0\n") -} - -pub fn is_eps() -> StartsWithPredicate { - predicate::str::starts_with("%!PS-Adobe-3.0 EPSF-3.0\n") -} - -pub fn is_pdf() -> PdfPredicate { - PdfPredicate {} -} - -pub fn is_svg() -> SvgPredicate { - SvgPredicate {} -} diff --git a/rsvg-convert/tests/internal_predicates/mod.rs b/rsvg-convert/tests/internal_predicates/mod.rs deleted file mode 100644 index 7e15354d..00000000 --- a/rsvg-convert/tests/internal_predicates/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod file; -mod pdf; -mod png; -mod svg; diff --git a/rsvg-convert/tests/internal_predicates/pdf.rs b/rsvg-convert/tests/internal_predicates/pdf.rs deleted file mode 100644 index f7872d71..00000000 --- a/rsvg-convert/tests/internal_predicates/pdf.rs +++ /dev/null @@ -1,358 +0,0 @@ -use chrono::{DateTime, Utc}; -use float_cmp::approx_eq; -use lopdf::{self, Dictionary, Object}; -use predicates::prelude::*; -use predicates::reflection::{Case, Child, PredicateReflection, Product}; -use std::cmp; -use std::fmt; - -/// Checks that the variable of type [u8] can be parsed as a PDF file. -#[derive(Debug)] -pub struct PdfPredicate {} - -impl PdfPredicate { - pub fn with_page_count(self: Self, num_pages: usize) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::PageCount(num_pages), - } - } - - pub fn with_page_size( - self: Self, - idx: usize, - width_in_points: f32, - height_in_points: f32, - ) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::PageSize( - Dimensions { - w: width_in_points, - h: height_in_points, - unit: 1.0, - }, - idx, - ), - } - } - - pub fn with_creation_date(self: Self, when: DateTime) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::CreationDate(when), - } - } - - pub fn with_link(self: Self, link: &str) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::Link(link.to_string()), - } - } - - pub fn with_text(self: Self, text: &str) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::Text(text.to_string()), - } - } -} - -impl Predicate<[u8]> for PdfPredicate { - fn eval(&self, data: &[u8]) -> bool { - lopdf::Document::load_mem(data).is_ok() - } - - fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option> { - match lopdf::Document::load_mem(data) { - Ok(_) => None, - Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), - } - } -} - -impl PredicateReflection for PdfPredicate {} - -impl fmt::Display for PdfPredicate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "is a PDF") - } -} - -/// Extends a PdfPredicate by a check for page count, page size or creation date. -#[derive(Debug)] -pub struct DetailPredicate { - p: PdfPredicate, - d: Detail, -} - -#[derive(Debug)] -enum Detail { - PageCount(usize), - PageSize(Dimensions, usize), - CreationDate(DateTime), - Link(String), - Text(String), -} - -/// A PDF page's dimensions from its `MediaBox`. -/// -/// Note that `w` and `h` given in `UserUnit`, which is by default 1.0 = 1/72 inch. -#[derive(Debug)] -struct Dimensions { - w: f32, - h: f32, - unit: f32, // UserUnit, in points (1/72 of an inch) -} - -impl Dimensions { - pub fn from_media_box(obj: &lopdf::Object, unit: Option) -> lopdf::Result { - let a = obj.as_array()?; - Ok(Dimensions { - w: a[2].as_float()?, - h: a[3].as_float()?, - unit: unit.unwrap_or(1.0), - }) - } - - pub fn width_in_pt(self: &Self) -> f32 { - self.w * self.unit - } - - pub fn height_in_pt(self: &Self) -> f32 { - self.h * self.unit - } -} - -impl fmt::Display for Dimensions { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} pt x {} pt", self.width_in_pt(), self.height_in_pt()) - } -} - -impl cmp::PartialEq for Dimensions { - fn eq(&self, other: &Self) -> bool { - approx_eq!( - f32, - self.width_in_pt(), - other.width_in_pt(), - epsilon = 0.0001 - ) && approx_eq!( - f32, - self.height_in_pt(), - other.height_in_pt(), - epsilon = 0.0001 - ) - } -} - -impl cmp::Eq for Dimensions {} - -trait Details { - fn get_page_count(&self) -> usize; - fn get_page_size(&self, idx: usize) -> Option; - fn get_creation_date(&self) -> Option>; - fn get_from_trailer<'a>(self: &'a Self, key: &[u8]) -> lopdf::Result<&'a lopdf::Object>; - fn get_from_page<'a>( - self: &'a Self, - idx: usize, - key: &[u8], - ) -> lopdf::Result<&'a lopdf::Object>; -} - -impl DetailPredicate { - fn eval_doc(&self, doc: &lopdf::Document) -> bool { - match &self.d { - Detail::PageCount(n) => doc.get_page_count() == *n, - Detail::PageSize(d, idx) => doc.get_page_size(*idx).map_or(false, |dim| dim == *d), - Detail::CreationDate(d) => doc.get_creation_date().map_or(false, |date| date == *d), - Detail::Link(link) => document_has_link(doc, &link), - Detail::Text(text) => document_has_text(doc, &text), - } - } - - fn find_case_for_doc<'a>(&'a self, expected: bool, doc: &lopdf::Document) -> Option> { - if self.eval_doc(doc) == expected { - let product = self.product_for_doc(doc); - Some(Case::new(Some(self), false).add_product(product)) - } else { - None - } - } - - fn product_for_doc(&self, doc: &lopdf::Document) -> Product { - match &self.d { - Detail::PageCount(_) => Product::new( - "actual page count", - format!("{} page(s)", doc.get_page_count()), - ), - Detail::PageSize(_, idx) => Product::new( - "actual page size", - match doc.get_page_size(*idx) { - Some(dim) => format!("{}", dim), - None => "None".to_string(), - }, - ), - Detail::CreationDate(_) => Product::new( - "actual creation date", - format!("{:?}", doc.get_creation_date()), - ), - Detail::Link(_) => Product::new( - "actual link contents", - "FIXME: who knows, but it's not what we expected".to_string(), - ), - Detail::Text(_) => { - Product::new("actual text contents", doc.extract_text(&[1]).unwrap()) - } - } - } -} - -// Extensions to lopdf::Object; can be removed after lopdf 0.26 -trait ObjExt { - /// Get the object value as a float. - /// Unlike as_f32() this will also cast an Integer to a Real. - fn as_float(&self) -> lopdf::Result; -} - -impl ObjExt for lopdf::Object { - fn as_float(&self) -> lopdf::Result { - match *self { - lopdf::Object::Integer(ref value) => Ok(*value as f32), - lopdf::Object::Real(ref value) => Ok(*value), - _ => Err(lopdf::Error::Type), - } - } -} - -impl Details for lopdf::Document { - fn get_page_count(self: &Self) -> usize { - self.get_pages().len() - } - - fn get_page_size(self: &Self, idx: usize) -> Option { - match self.get_from_page(idx, b"MediaBox") { - Ok(obj) => { - let unit = self - .get_from_page(idx, b"UserUnit") - .and_then(ObjExt::as_float) - .ok(); - Dimensions::from_media_box(obj, unit).ok() - } - Err(_) => None, - } - } - - fn get_creation_date(self: &Self) -> Option> { - match self.get_from_trailer(b"CreationDate") { - Ok(obj) => obj.as_datetime().map(|date| date.with_timezone(&Utc)), - Err(_) => None, - } - } - - fn get_from_trailer<'a>(self: &'a Self, key: &[u8]) -> lopdf::Result<&'a lopdf::Object> { - let id = self.trailer.get(b"Info")?.as_reference()?; - self.get_object(id)?.as_dict()?.get(key) - } - - fn get_from_page<'a>( - self: &'a Self, - idx: usize, - key: &[u8], - ) -> lopdf::Result<&'a lopdf::Object> { - let mut iter = self.page_iter(); - for _ in 0..idx { - let _ = iter.next(); - } - match iter.next() { - Some(id) => self.get_object(id)?.as_dict()?.get(key), - None => Err(lopdf::Error::ObjectNotFound), - } - } -} - -impl Predicate<[u8]> for DetailPredicate { - fn eval(&self, data: &[u8]) -> bool { - match lopdf::Document::load_mem(data) { - Ok(doc) => self.eval_doc(&doc), - _ => false, - } - } - - fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { - match lopdf::Document::load_mem(data) { - Ok(doc) => self.find_case_for_doc(expected, &doc), - Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), - } - } -} - -impl PredicateReflection for DetailPredicate { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for DetailPredicate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self.d { - Detail::PageCount(n) => write!(f, "is a PDF with {} page(s)", n), - Detail::PageSize(d, _) => write!(f, "is a PDF sized {}", d), - Detail::CreationDate(d) => write!(f, "is a PDF created {:?}", d), - Detail::Link(l) => write!(f, "is a PDF with a link to {}", l), - Detail::Text(t) => write!(f, "is a PDF with \"{}\" in its text content", t), - } - } -} - -// This is an extremely trivial test for a string being present in the document's -// text objects. -fn document_has_text(document: &lopdf::Document, needle: &str) -> bool { - if let Ok(haystack) = text_from_first_page(document) { - haystack.contains(needle) - } else { - false - } -} - -// We do a super simple test that a PDF actually contains an Annotation object -// with a particular link. We don't test that this annotation is actually linked -// from a page; that would be nicer. -fn document_has_link(document: &lopdf::Document, link_text: &str) -> bool { - document - .objects - .iter() - .map(|(_obj_id, object)| object) - .any(|obj| object_is_annotation_with_link(obj, link_text)) -} - -fn object_is_annotation_with_link(object: &Object, link_text: &str) -> bool { - object - .as_dict() - .map(|dict| dict_is_annotation(dict) && dict_has_a_with_link(dict, link_text)) - .unwrap_or(false) -} - -fn dict_is_annotation(dict: &Dictionary) -> bool { - dict.get(b"Type") - .and_then(|type_val| type_val.as_name_str()) - .map(|name| name == "Annot") - .unwrap_or(false) -} - -fn dict_has_a_with_link(dict: &Dictionary, link_text: &str) -> bool { - dict.get(b"A") - .and_then(|obj| obj.as_dict()) - .and_then(|dict| dict.get(b"URI")) - .and_then(|obj| obj.as_str()) - .map(|string| string == link_text.as_bytes()) - .unwrap_or(false) -} - -fn text_from_first_page(doc: &lopdf::Document) -> lopdf::Result { - // This is extremely simplistic; lopdf just concatenates all the text in the page - // into a single string. - doc.extract_text(&[1]) -} diff --git a/rsvg-convert/tests/internal_predicates/png.rs b/rsvg-convert/tests/internal_predicates/png.rs deleted file mode 100644 index f629b510..00000000 --- a/rsvg-convert/tests/internal_predicates/png.rs +++ /dev/null @@ -1,193 +0,0 @@ -use png; -use predicates::prelude::*; -use predicates::reflection::{Case, Child, PredicateReflection, Product}; -use std::fmt; -use std::io::BufReader; -use std::path::{Path, PathBuf}; - -use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; - -use rsvg::test_utils::compare_surfaces::BufferDiff; -use rsvg::test_utils::reference_utils::{surface_from_png, Compare, Deviation, Reference}; - -/// Checks that the variable of type [u8] can be parsed as a PNG file. -#[derive(Debug)] -pub struct PngPredicate {} - -impl PngPredicate { - pub fn with_size(self: Self, w: u32, h: u32) -> SizePredicate { - SizePredicate:: { p: self, w, h } - } - - pub fn with_contents>(self: Self, reference: P) -> ReferencePredicate { - let mut path = PathBuf::new(); - path.push(reference); - ReferencePredicate:: { p: self, path } - } -} - -impl Predicate<[u8]> for PngPredicate { - fn eval(&self, data: &[u8]) -> bool { - let decoder = png::Decoder::new(data); - decoder.read_info().is_ok() - } - - fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option> { - let decoder = png::Decoder::new(data); - match decoder.read_info() { - Ok(_) => None, - Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), - } - } -} - -impl PredicateReflection for PngPredicate {} - -impl fmt::Display for PngPredicate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "is a PNG") - } -} - -/// Extends a PngPredicate by a check for a given size of the PNG file. -#[derive(Debug)] -pub struct SizePredicate { - p: PngPredicate, - w: u32, - h: u32, -} - -impl SizePredicate { - fn eval_info(&self, info: &png::Info) -> bool { - info.width == self.w && info.height == self.h - } - - fn find_case_for_info<'a>(&'a self, expected: bool, info: &png::Info) -> Option> { - if self.eval_info(info) == expected { - let product = self.product_for_info(info); - Some(Case::new(Some(self), false).add_product(product)) - } else { - None - } - } - - fn product_for_info(&self, info: &png::Info) -> Product { - let actual_size = format!("{} x {}", info.width, info.height); - Product::new("actual size", actual_size) - } -} - -impl Predicate<[u8]> for SizePredicate { - fn eval(&self, data: &[u8]) -> bool { - let decoder = png::Decoder::new(data); - match decoder.read_info() { - Ok(reader) => self.eval_info(&reader.info()), - _ => false, - } - } - - fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { - let decoder = png::Decoder::new(data); - match decoder.read_info() { - Ok(reader) => self.find_case_for_info(expected, reader.info()), - Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), - } - } -} - -impl PredicateReflection for SizePredicate { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for SizePredicate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "is a PNG with size {} x {}", self.w, self.h) - } -} - -/// Extends a PngPredicate by a comparison to the contents of a reference file -#[derive(Debug)] -pub struct ReferencePredicate { - p: PngPredicate, - path: PathBuf, -} - -impl ReferencePredicate { - fn diff_acceptable(diff: &BufferDiff) -> bool { - match diff { - BufferDiff::DifferentSizes => false, - BufferDiff::Diff(diff) => !diff.inacceptable(), - } - } - - fn diff_surface(&self, surface: &SharedImageSurface) -> Option { - let reference = Reference::from_png(&self.path) - .unwrap_or_else(|_| panic!("could not open {:?}", self.path)); - if let Ok(diff) = reference.compare(&surface) { - if !Self::diff_acceptable(&diff) { - return Some(diff); - } - } - None - } - - fn find_case_for_surface<'a>( - &'a self, - expected: bool, - surface: &SharedImageSurface, - ) -> Option> { - let diff = self.diff_surface(&surface); - if diff.is_some() != expected { - let product = self.product_for_diff(&diff.unwrap()); - Some(Case::new(Some(self), false).add_product(product)) - } else { - None - } - } - - fn product_for_diff(&self, diff: &BufferDiff) -> Product { - let difference = format!("{}", diff); - Product::new("images differ", difference) - } -} - -impl Predicate<[u8]> for ReferencePredicate { - fn eval(&self, data: &[u8]) -> bool { - if let Ok(surface) = surface_from_png(&mut BufReader::new(data)) { - let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb).unwrap(); - self.diff_surface(&surface).is_some() - } else { - false - } - } - - fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { - match surface_from_png(&mut BufReader::new(data)) { - Ok(surface) => { - let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb).unwrap(); - self.find_case_for_surface(expected, &surface) - } - Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), - } - } -} - -impl PredicateReflection for ReferencePredicate { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for ReferencePredicate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "is a PNG that matches the reference {}", - self.path.display() - ) - } -} diff --git a/rsvg-convert/tests/internal_predicates/svg.rs b/rsvg-convert/tests/internal_predicates/svg.rs deleted file mode 100644 index 70473812..00000000 --- a/rsvg-convert/tests/internal_predicates/svg.rs +++ /dev/null @@ -1,179 +0,0 @@ -use float_cmp::approx_eq; -use gio::glib::Bytes; -use gio::MemoryInputStream; -use predicates::prelude::*; -use predicates::reflection::{Case, Child, PredicateReflection, Product}; -use std::cmp; -use std::fmt; - -use rsvg::{CairoRenderer, Length, Loader, LoadingError, SvgHandle}; - -/// Checks that the variable of type [u8] can be parsed as a SVG file. -#[derive(Debug)] -pub struct SvgPredicate {} - -impl SvgPredicate { - pub fn with_size(self: Self, width: Length, height: Length) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::Size(Dimensions { - w: width, - h: height, - }), - } - } -} - -fn svg_from_bytes(data: &[u8]) -> Result { - let bytes = Bytes::from(data); - let stream = MemoryInputStream::from_bytes(&bytes); - Loader::new().read_stream(&stream, None::<&gio::File>, None::<&gio::Cancellable>) -} - -impl Predicate<[u8]> for SvgPredicate { - fn eval(&self, data: &[u8]) -> bool { - svg_from_bytes(data).is_ok() - } - - fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option> { - match svg_from_bytes(data) { - Ok(_) => None, - Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), - } - } -} - -impl PredicateReflection for SvgPredicate {} - -impl fmt::Display for SvgPredicate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "is an SVG") - } -} - -/// Extends a SVG Predicate by a check for its size -#[derive(Debug)] -pub struct DetailPredicate { - p: SvgPredicate, - d: Detail, -} - -#[derive(Debug)] -enum Detail { - Size(Dimensions), -} - -/// SVG's dimensions -#[derive(Debug)] -struct Dimensions { - w: Length, - h: Length, -} - -impl Dimensions { - pub fn width(self: &Self) -> f64 { - self.w.length - } - - pub fn height(self: &Self) -> f64 { - self.h.length - } -} - -impl fmt::Display for Dimensions { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}{} x {}{}", - self.width(), - self.w.unit, - self.height(), - self.h.unit - ) - } -} - -impl cmp::PartialEq for Dimensions { - fn eq(&self, other: &Self) -> bool { - approx_eq!(f64, self.width(), other.width(), epsilon = 0.000_001) - && approx_eq!(f64, self.height(), other.height(), epsilon = 0.000_001) - && (self.w.unit == self.h.unit) - && (self.h.unit == other.h.unit) - && (other.h.unit == other.w.unit) - } -} - -impl cmp::Eq for Dimensions {} - -trait Details { - fn get_size(&self) -> Option; -} - -impl DetailPredicate { - fn eval_doc(&self, handle: &SvgHandle) -> bool { - match &self.d { - Detail::Size(d) => { - let renderer = CairoRenderer::new(handle); - let dimensions = renderer.intrinsic_dimensions(); - (dimensions.width, dimensions.height) == (d.w, d.h) - } - } - } - - fn find_case_for_doc<'a>(&'a self, expected: bool, handle: &SvgHandle) -> Option> { - if self.eval_doc(handle) == expected { - let product = self.product_for_doc(handle); - Some(Case::new(Some(self), false).add_product(product)) - } else { - None - } - } - - fn product_for_doc(&self, handle: &SvgHandle) -> Product { - match &self.d { - Detail::Size(_) => { - let renderer = CairoRenderer::new(handle); - let dimensions = renderer.intrinsic_dimensions(); - - Product::new( - "actual size", - format!( - "width={:?}, height={:?}", - dimensions.width, dimensions.height - ), - ) - } - } - } -} - -impl Predicate<[u8]> for DetailPredicate { - fn eval(&self, data: &[u8]) -> bool { - match svg_from_bytes(data) { - Ok(handle) => self.eval_doc(&handle), - _ => false, - } - } - - fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { - match svg_from_bytes(data) { - Ok(handle) => self.find_case_for_doc(expected, &handle), - Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), - } - } -} - -impl PredicateReflection for DetailPredicate { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for DetailPredicate { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match &self.d { - Detail::Size(d) => write!(f, "is an SVG sized {}", d), - } - } -} diff --git a/rsvg-convert/tests/rsvg_convert.rs b/rsvg-convert/tests/rsvg_convert.rs deleted file mode 100644 index f58edf6c..00000000 --- a/rsvg-convert/tests/rsvg_convert.rs +++ /dev/null @@ -1,1078 +0,0 @@ -//use crate::predicates::ends_with_pkg_version; -mod internal_predicates; -use internal_predicates::file; - -use assert_cmd::assert::IntoOutputPredicate; -use assert_cmd::Command; -#[cfg(system_deps_have_cairo_pdf)] -use chrono::{TimeZone, Utc}; -use predicates::boolean::*; -use predicates::prelude::*; -use predicates::str::*; -use rsvg::{Length, LengthUnit}; -use std::path::Path; -use tempfile::Builder; -use url::Url; - -// What should be tested here? -// The goal is to test the code in rsvg-convert, not the entire library. -// -// - command-line options that affect size (width, height, zoom, resolution) ✔ -// - pixel dimensions of the output (should be sufficient to do that for PNG) ✔ -// - limit on output size (32767 pixels) ✔ -// - output formats (PNG, PDF, PS, EPS, SVG) ✔ -// - multi-page output (for PDF) ✔ -// - output file option ✔ -// - SOURCE_DATA_EPOCH environment variable for PDF output ✔ -// - background color option ✔ -// - optional CSS stylesheet ✔ -// - error handling for missing SVG dimensions ✔ -// - error handling for export lookup ID ✔ -// - error handling for invalid input ✔ - -struct RsvgConvert {} - -impl RsvgConvert { - fn new() -> Command { - Command::cargo_bin("rsvg-convert").unwrap() - } - - fn new_with_input

(file: P) -> Command - where - P: AsRef, - { - let mut command = RsvgConvert::new(); - match command.pipe_stdin(&file) { - Ok(_) => command, - Err(e) => panic!("Error opening file '{}': {}", file.as_ref().display(), e), - } - } - - fn accepts_arg(option: &str) { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg(option) - .assert() - .success(); - } - - fn option_yields_output(option: &str, output_pred: I) - where - I: IntoOutputPredicate

, - P: Predicate<[u8]>, - { - RsvgConvert::new() - .arg(option) - .assert() - .success() - .stdout(output_pred); - } -} - -#[test] -fn converts_svg_from_stdin_to_png() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .assert() - .success() - .stdout(file::is_png()); -} - -#[test] -fn argument_is_input_filename() { - let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - RsvgConvert::new() - .arg(input) - .assert() - .success() - .stdout(file::is_png()); -} - -#[test] -fn argument_is_url() { - let path = Path::new("tests/fixtures/bug521-with-viewbox.svg") - .canonicalize() - .unwrap(); - let url = Url::from_file_path(path).unwrap(); - let stringified = url.as_str(); - assert!(stringified.starts_with("file://")); - - RsvgConvert::new() - .arg(stringified) - .assert() - .success() - .stdout(file::is_png()); -} - -#[test] -fn output_format_png() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--format=png") - .assert() - .success() - .stdout(file::is_png()); -} - -#[cfg(system_deps_have_cairo_ps)] -#[test] -fn output_format_ps() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--format=ps") - .assert() - .success() - .stdout(file::is_ps()); -} - -#[cfg(system_deps_have_cairo_ps)] -#[test] -fn output_format_eps() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--format=eps") - .assert() - .success() - .stdout(file::is_eps()); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn output_format_pdf() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--format=pdf") - .assert() - .success() - .stdout(file::is_pdf()); -} - -#[cfg(system_deps_have_cairo_svg)] -#[test] -fn output_format_svg_short_option() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("-f") - .arg("svg") - .assert() - .success() - .stdout(file::is_svg()); -} - -#[cfg(system_deps_have_cairo_svg)] -#[test] -fn user_specified_width_and_height() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--format") - .arg("svg") - .arg("--width") - .arg("42cm") - .arg("--height") - .arg("43cm") - .assert() - .success() - .stdout(file::is_svg().with_size( - Length::new(42.0, LengthUnit::Cm), - Length::new(43.0, LengthUnit::Cm), - )); -} - -#[cfg(system_deps_have_cairo_svg)] -#[test] -fn user_specified_width_and_height_px_output() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--format") - .arg("svg") - .arg("--width") - .arg("1920") - .arg("--height") - .arg("508mm") - .assert() - .success() - .stdout(file::is_svg().with_size( - Length::new(1920.0, LengthUnit::Px), - Length::new(1920.0, LengthUnit::Px), - )); -} - -#[cfg(system_deps_have_cairo_svg)] -#[test] -fn user_specified_width_and_height_a4() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--format") - .arg("svg") - .arg("--page-width") - .arg("210mm") - .arg("--page-height") - .arg("297mm") - .arg("--left") - .arg("1cm") - .arg("--top") - .arg("1cm") - .arg("--width") - .arg("190mm") - .arg("--height") - .arg("277mm") - .assert() - .success() - .stdout(file::is_svg().with_size( - Length::new(210.0, LengthUnit::Mm), - Length::new(297.0, LengthUnit::Mm), - )); -} - -#[test] -fn output_file_option() { - let output = { - let tempfile = Builder::new().suffix(".png").tempfile().unwrap(); - tempfile.path().to_path_buf() - }; - assert!(predicates::path::is_file().not().eval(&output)); - - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg(format!("--output={}", output.display())) - .assert() - .success() - .stdout(is_empty()); - - assert!(predicates::path::is_file().eval(&output)); - std::fs::remove_file(&output).unwrap(); -} - -#[test] -fn output_file_short_option() { - let output = { - let tempfile = Builder::new().suffix(".png").tempfile().unwrap(); - tempfile.path().to_path_buf() - }; - assert!(predicates::path::is_file().not().eval(&output)); - - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("-o") - .arg(format!("{}", output.display())) - .assert() - .success() - .stdout(is_empty()); - - assert!(predicates::path::is_file().eval(&output)); - std::fs::remove_file(&output).unwrap(); -} - -#[test] -fn overwrites_existing_output_file() { - let output = { - let tempfile = Builder::new().suffix(".png").tempfile().unwrap(); - tempfile.path().to_path_buf() - }; - assert!(predicates::path::is_file().not().eval(&output)); - - for _ in 0..2 { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg(format!("--output={}", output.display())) - .assert() - .success() - .stdout(is_empty()); - - assert!(predicates::path::is_file().eval(&output)); - } - - std::fs::remove_file(&output).unwrap(); -} - -#[test] -fn empty_input_yields_error() { - let starts_with = starts_with("Error reading SVG"); - let ends_with = ends_with("Input file is too short").trim(); - RsvgConvert::new() - .assert() - .failure() - .stderr(starts_with.and(ends_with)); -} - -#[test] -fn empty_svg_yields_error() { - RsvgConvert::new_with_input("tests/fixtures/empty.svg") - .assert() - .failure() - .stderr("The SVG stdin has no dimensions\n"); -} - -#[test] -fn multiple_input_files_not_allowed_for_png_output() { - let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); - RsvgConvert::new() - .arg(one) - .arg(two) - .assert() - .failure() - .stderr(contains( - "Multiple SVG files are only allowed for PDF and (E)PS output", - )); -} - -#[cfg(system_deps_have_cairo_ps)] -#[test] -fn multiple_input_files_accepted_for_eps_output() { - let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); - RsvgConvert::new() - .arg("--format=eps") - .arg(one) - .arg(two) - .assert() - .success() - .stdout(file::is_eps()); -} - -#[cfg(system_deps_have_cairo_ps)] -#[test] -fn multiple_input_files_accepted_for_ps_output() { - let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); - RsvgConvert::new() - .arg("--format=ps") - .arg(one) - .arg(two) - .assert() - .success() - .stdout(file::is_ps()); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn multiple_input_files_create_multi_page_pdf_output() { - let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); - let three = Path::new("tests/fixtures/example.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .arg(one) - .arg(two) - .arg(three) - .assert() - .success() - .stdout( - file::is_pdf() - .with_page_count(3) - .and(file::is_pdf().with_page_size(0, 150.0, 75.0)) - .and(file::is_pdf().with_page_size(1, 123.0, 123.0)) - .and(file::is_pdf().with_page_size(2, 75.0, 300.0)), - ); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn multiple_input_files_create_multi_page_pdf_output_fixed_size() { - let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); - let three = Path::new("tests/fixtures/example.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .arg("--page-width=8.5in") - .arg("--page-height=11in") - .arg("--width=7.5in") - .arg("--height=10in") - .arg("--left=0.5in") - .arg("--top=0.5in") - .arg("--keep-aspect-ratio") - .arg(one) - .arg(two) - .arg(three) - .assert() - .success() - .stdout( - file::is_pdf() - .with_page_count(3) - // https://www.wolframalpha.com/input/?i=convert+11+inches+to+desktop+publishing+points - .and(file::is_pdf().with_page_size(0, 612.0, 792.0)) - .and(file::is_pdf().with_page_size(1, 612.0, 792.0)) - .and(file::is_pdf().with_page_size(2, 612.0, 792.0)), - ); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn pdf_has_link() { - let input = Path::new("tests/fixtures/a-link.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .arg(input) - .assert() - .success() - .stdout(file::is_pdf().with_link("https://example.com")); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn pdf_has_link_inside_text() { - let input = Path::new("tests/fixtures/text-a-link.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .arg(input) - .assert() - .success() - .stdout( - file::is_pdf() - .with_link("https://example.com") - .and(file::is_pdf().with_link("https://another.example.com")), - ); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn pdf_has_text() { - let input = Path::new("tests/fixtures/hello-world.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .arg(input) - .assert() - .success() - .stdout( - file::is_pdf() - .with_text("Hello world!") - .and(file::is_pdf().with_text("Hello again!")), - ); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn env_source_data_epoch_controls_pdf_creation_date() { - let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - let date = 1581411039; // seconds since epoch - RsvgConvert::new() - .env("SOURCE_DATE_EPOCH", format!("{}", date)) - .arg("--format=pdf") - .arg(input) - .assert() - .success() - .stdout(file::is_pdf().with_creation_date(Utc.timestamp_opt(date, 0).unwrap())); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn env_source_data_epoch_no_digits() { - // intentionally not testing for the full error string here - let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - RsvgConvert::new() - .env("SOURCE_DATE_EPOCH", "foobar") - .arg("--format=pdf") - .arg(input) - .assert() - .failure() - .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH")); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn env_source_data_epoch_trailing_garbage() { - // intentionally not testing for the full error string here - let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .env("SOURCE_DATE_EPOCH", "1234556+") - .arg(input) - .assert() - .failure() - .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH")); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn env_source_data_epoch_empty() { - // intentionally not testing for the full error string here - let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .env("SOURCE_DATE_EPOCH", "") - .arg(input) - .assert() - .failure() - .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH")); -} - -#[test] -fn width_option() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--width=300") - .assert() - .success() - .stdout(file::is_png().with_size(300, 150)); -} - -#[test] -fn height_option() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--height=200") - .assert() - .success() - .stdout(file::is_png().with_size(400, 200)); -} - -#[test] -fn width_and_height_options() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--width=300") - .arg("--height=200") - .assert() - .success() - .stdout(file::is_png().with_size(300, 200)); -} - -#[test] -fn unsupported_unit_in_width_and_height() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--height=200ex") - .assert() - .failure() - .stderr(contains("supported units")); -} - -#[test] -fn invalid_length() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--page-width=foo") - .assert() - .failure() - .stderr(contains("can not be parsed as a length")); -} - -#[test] -fn zoom_factor() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--zoom=0.8") - .assert() - .success() - .stdout(file::is_png().with_size(160, 80)); -} - -#[test] -fn zoom_factor_and_larger_size() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--width=400") - .arg("--height=200") - .arg("--zoom=1.5") - .assert() - .success() - .stdout(file::is_png().with_size(300, 150)); -} - -#[test] -fn zoom_factor_and_smaller_size() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--width=400") - .arg("--height=200") - .arg("--zoom=3.5") - .assert() - .success() - .stdout(file::is_png().with_size(400, 200)); -} - -#[test] -fn x_zoom_option() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--x-zoom=2") - .assert() - .success() - .stdout(file::is_png().with_size(400, 100)); -} - -#[test] -fn x_short_option() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("-x") - .arg("2.0") - .assert() - .success() - .stdout(file::is_png().with_size(400, 100)); -} - -#[test] -fn y_zoom_option() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--y-zoom=2.0") - .assert() - .success() - .stdout(file::is_png().with_size(200, 200)); -} - -#[test] -fn y_short_option() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("-y") - .arg("2") - .assert() - .success() - .stdout(file::is_png().with_size(200, 200)); -} - -#[test] -fn huge_zoom_factor_yields_error() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--zoom=1000") - .assert() - .failure() - .stderr(starts_with( - "The resulting image would be larger than 32767 pixels", - )); -} - -#[test] -fn negative_zoom_factor_yields_error() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--zoom=-2") - .assert() - .failure() - .stderr(contains("Invalid zoom")); -} - -#[test] -fn invalid_zoom_factor_yields_error() { - RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") - .arg("--zoom=foo") - .assert() - .failure() - .stderr(contains("invalid value")); -} - -#[test] -fn default_resolution_is_96dpi() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .assert() - .success() - .stdout(file::is_png().with_size(96, 384)); -} - -#[test] -fn x_resolution() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--dpi-x=300") - .assert() - .success() - .stdout(file::is_png().with_size(300, 384)); -} - -#[test] -fn x_resolution_short_option() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("-d") - .arg("45") - .assert() - .success() - .stdout(file::is_png().with_size(45, 384)); -} - -#[test] -fn y_resolution() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--dpi-y=300") - .assert() - .success() - .stdout(file::is_png().with_size(96, 1200)); -} - -#[test] -fn y_resolution_short_option() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("-p") - .arg("45") - .assert() - .success() - .stdout(file::is_png().with_size(96, 180)); -} - -#[test] -fn x_and_y_resolution() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--dpi-x=300") - .arg("--dpi-y=150") - .assert() - .success() - .stdout(file::is_png().with_size(300, 600)); -} - -#[test] -fn zero_resolution_is_invalid() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--dpi-x=0") - .arg("--dpi-y=0") - .assert() - .failure() - .stderr(contains("Invalid resolution")); -} - -#[test] -fn negative_resolution_is_invalid() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--dpi-x=-100") - .arg("--dpi-y=-100") - .assert() - .failure() - .stderr(contains("Invalid resolution")); -} - -#[test] -fn zero_offset_png() { - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--page-width=640") - .arg("--page-height=480") - .arg("--width=200") - .arg("--height=100") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/zero-offset-png.png")); -} - -#[test] -fn offset_png() { - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--page-width=640") - .arg("--page-height=480") - .arg("--width=200") - .arg("--height=100") - .arg("--left=100") - .arg("--top=50") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/offset-png.png")); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn unscaled_pdf_size() { - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--format=pdf") - .assert() - .success() - .stdout(file::is_pdf().with_page_size(0, 72.0, 72.0)); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn pdf_size_width_height() { - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--format=pdf") - .arg("--width=2in") - .arg("--height=3in") - .assert() - .success() - .stdout(file::is_pdf().with_page_size(0, 144.0, 216.0)); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn pdf_size_width_height_proportional() { - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--format=pdf") - .arg("--width=2in") - .arg("--height=3in") - .arg("--keep-aspect-ratio") - .assert() - .success() - .stdout(file::is_pdf().with_page_size(0, 144.0, 144.0)); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn pdf_page_size() { - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--format=pdf") - .arg("--page-width=210mm") - .arg("--page-height=297mm") - .assert() - .success() - .stdout(file::is_pdf().with_page_size(0, 210.0 / 25.4 * 72.0, 297.0 / 25.4 * 72.0)); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn multiple_input_files_create_multi_page_pdf_size_override() { - let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); - let three = Path::new("tests/fixtures/example.svg"); - RsvgConvert::new() - .arg("--format=pdf") - .arg("--width=300pt") - .arg("--height=200pt") - .arg(one) - .arg(two) - .arg(three) - .assert() - .success() - .stdout( - file::is_pdf() - .with_page_count(3) - .and(file::is_pdf().with_page_size(0, 300.0, 200.0)) - .and(file::is_pdf().with_page_size(1, 300.0, 200.0)) - .and(file::is_pdf().with_page_size(2, 300.0, 200.0)), - ); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn missing_page_size_yields_error() { - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--format=pdf") - .arg("--page-width=210mm") - .assert() - .failure() - .stderr(contains("both").and(contains("options"))); - - RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") - .arg("--format=pdf") - .arg("--page-height=297mm") - .assert() - .failure() - .stderr(contains("both").and(contains("options"))); -} - -#[test] -fn does_not_clip_partial_coverage_pixels() { - RsvgConvert::new_with_input("tests/fixtures/bug677-partial-pixel.svg") - .assert() - .success() - .stdout(file::is_png().with_size(2, 2)); -} - -#[test] -fn background_color_option_with_valid_color() { - RsvgConvert::accepts_arg("--background-color=LimeGreen"); -} - -#[test] -fn background_color_option_none() { - RsvgConvert::accepts_arg("--background-color=None"); -} - -#[test] -fn background_color_short_option() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("-b") - .arg("#aabbcc") - .assert() - .success(); -} - -#[test] -fn background_color_option_invalid_color_yields_error() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--background-color=foobar") - .assert() - .failure() - .stderr(contains("Invalid").and(contains("color"))); -} - -#[test] -fn background_color_is_rendered() { - RsvgConvert::new_with_input("tests/fixtures/gimp-wilber.svg") - .arg("--background-color=purple") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/gimp-wilber-ref.png")); -} - -#[test] -fn stylesheet_option() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--stylesheet=tests/fixtures/empty.svg") - .assert() - .success(); -} - -#[test] -fn stylesheet_short_option() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("-s") - .arg("tests/fixtures/empty.svg") - .assert() - .success(); -} - -#[test] -fn stylesheet_option_error() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--stylesheet=foobar") - .assert() - .failure() - .stderr(starts_with("Error reading stylesheet")); -} - -#[test] -fn export_id_option() { - RsvgConvert::new_with_input("tests/fixtures/geometry-element.svg") - .arg("--export-id=foo") - .assert() - .success() - .stdout(file::is_png().with_size(40, 50)); -} - -#[test] -fn export_id_with_zero_stroke_width() { - // https://gitlab.gnome.org/GNOME/librsvg/-/issues/601 - // - // This tests a bug that manifested itself easily with the --export-id option, but it - // is not a bug with the option itself. An object with stroke_width=0 was causing - // an extra point at the origin to be put in the bounding box, so the final image - // spanned the origin to the actual visible bounds of the rendered object. - // - // We can probably test this more cleanly once we have a render tree. - RsvgConvert::new_with_input("tests/fixtures/bug601-zero-stroke-width.svg") - .arg("--export-id=foo") - .assert() - .success() - .stdout( - file::is_png() - .with_contents("tests/fixtures/bug601-zero-stroke-width-render-only-foo.png"), - ); -} - -#[test] -fn export_id_short_option() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("-i") - .arg("two") - .assert() - .success() - .stdout(file::is_png().with_size(100, 200)); -} - -#[test] -fn export_id_with_hash_prefix() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("-i") - .arg("#two") - .assert() - .success() - .stdout(file::is_png().with_size(100, 200)); -} - -#[test] -fn export_id_option_error() { - RsvgConvert::new_with_input("tests/fixtures/dpi.svg") - .arg("--export-id=foobar") - .assert() - .failure() - .stderr(starts_with("File stdin does not have an object with id \"")); -} - -#[test] -fn unlimited_option() { - RsvgConvert::accepts_arg("--unlimited"); -} - -#[test] -fn unlimited_short_option() { - RsvgConvert::accepts_arg("-u"); -} - -#[test] -fn keep_aspect_ratio_option() { - let input = Path::new("tests/fixtures/dpi.svg"); - RsvgConvert::new_with_input(input) - .arg("--width=500") - .arg("--height=1000") - .assert() - .success() - .stdout(file::is_png().with_size(500, 1000)); - RsvgConvert::new_with_input(input) - .arg("--width=500") - .arg("--height=1000") - .arg("--keep-aspect-ratio") - .assert() - .success() - .stdout(file::is_png().with_size(250, 1000)); -} - -#[test] -fn keep_aspect_ratio_short_option() { - let input = Path::new("tests/fixtures/dpi.svg"); - RsvgConvert::new_with_input(input) - .arg("--width=1000") - .arg("--height=500") - .assert() - .success() - .stdout(file::is_png().with_size(1000, 500)); - RsvgConvert::new_with_input(input) - .arg("--width=1000") - .arg("--height=500") - .arg("-a") - .assert() - .success() - .stdout(file::is_png().with_size(125, 500)); -} - -#[test] -fn overflowing_size_is_detected() { - RsvgConvert::new_with_input("tests/fixtures/bug591-vbox-overflow.svg") - .assert() - .failure() - .stderr(starts_with( - "The resulting image would be larger than 32767 pixels", - )); -} - -#[test] -fn accept_language_given() { - RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") - .arg("--accept-language=es-MX") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/accept-language-es.png")); - - RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") - .arg("--accept-language=de") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/accept-language-de.png")); -} - -#[test] -fn accept_language_fallback() { - RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") - .arg("--accept-language=fr") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/accept-language-fallback.png")); -} - -#[test] -fn accept_language_invalid_tag() { - // underscores are not valid in BCP47 language tags - RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") - .arg("--accept-language=foo_bar") - .assert() - .failure() - .stderr(contains("invalid language tag")); -} - -#[test] -fn keep_image_data_option() { - RsvgConvert::accepts_arg("--keep-image-data"); -} - -#[test] -fn no_keep_image_data_option() { - RsvgConvert::accepts_arg("--no-keep-image-data"); -} - -fn is_version_output() -> AndPredicate, str> { - starts_with("rsvg-convert version ") - .and(predicates::str::ends_with(env!("CARGO_PKG_VERSION")).trim()) -} - -#[test] -fn version_option() { - RsvgConvert::option_yields_output("--version", is_version_output()) -} - -#[test] -fn version_short_option() { - RsvgConvert::option_yields_output("-v", is_version_output()) -} - -fn is_usage_output() -> OrPredicate { - contains("Usage:").or(contains("USAGE:")) -} - -#[test] -fn help_option() { - RsvgConvert::option_yields_output("--help", is_usage_output()) -} - -#[test] -fn help_short_option() { - RsvgConvert::option_yields_output("-?", is_usage_output()) -} diff --git a/rsvg_convert/Cargo.toml b/rsvg_convert/Cargo.toml new file mode 100644 index 00000000..035b2419 --- /dev/null +++ b/rsvg_convert/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "rsvg_convert" +version.workspace = true +authors.workspace = true +description.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true + +# So that we can use an rsvg-convert name instead of the default rsvg_convert +autobins = false + +[package.metadata.system-deps] +cairo-pdf = { version = "1.16", optional = true } +cairo-ps = { version = "1.16", optional = true } +cairo-svg = { version = "1.16", optional = true } + +[dependencies] +# Keep these in sync with respect to the cairo-rs version: +# src/lib.rs - toplevel example in the docs +cairo-rs = { version = "0.17", features=["v1_16", "png", "pdf", "ps", "svg"] } +cast = "0.3.0" +chrono = { version = "0.4.23", default-features = false, features = ["clock", "std"] } +clap = { version = "4.0.17", features = ["cargo", "derive"] } # rsvg-convert +clap_complete = "4.0.5" # rsvg-convert +cssparser = "0.29.0" +gio = "0.17" +glib = "0.17" +libc = "0.2" +librsvg = { path = "../rsvg" } +librsvg-c = { path = "../librsvg-c" } + +[dev-dependencies] +assert_cmd = "2.0.2" +predicates = "3.0.3" +tempfile = "3" +url = "2" +lopdf = "0.30.0" +png = "0.17.2" +float-cmp = "0.9.0" +librsvg = { path = "../rsvg", features = ["test-utils"] } + +[build-dependencies] +system-deps = "6.0.0" + +[[bin]] +name = "rsvg-convert" +path = "src/main.rs" diff --git a/rsvg_convert/build.rs b/rsvg_convert/build.rs new file mode 100644 index 00000000..eec6d526 --- /dev/null +++ b/rsvg_convert/build.rs @@ -0,0 +1,3 @@ +fn main() { + system_deps::Config::new().probe().unwrap(); +} diff --git a/rsvg_convert/src/main.rs b/rsvg_convert/src/main.rs new file mode 100644 index 00000000..0a8c527d --- /dev/null +++ b/rsvg_convert/src/main.rs @@ -0,0 +1,1552 @@ +use clap::crate_version; +use clap_complete::{Generator, Shell}; + +use gio::prelude::*; +use gio::{Cancellable, FileCreateFlags, InputStream, OutputStream}; + +#[cfg(unix)] +use gio::{UnixInputStream, UnixOutputStream}; + +#[cfg(windows)] +mod windows_imports { + pub use gio::{Win32InputStream, WriteOutputStream}; + pub use glib::ffi::gboolean; + pub use glib::translate::*; + pub use libc::c_void; + pub use std::io; + pub use std::os::windows::io::AsRawHandle; +} +#[cfg(windows)] +use self::windows_imports::*; + +use cssparser::{_cssparser_internal_to_lowercase, match_ignore_ascii_case}; + +use librsvg_c::{handle::PathOrUrl, sizing::LegacySize}; +use rsvg::rsvg_convert_only::{ + AspectRatio, CssLength, Horizontal, Length, Normalize, NormalizeParams, Parse, Signed, ULength, + Unsigned, Validate, Vertical, ViewBox, +}; +use rsvg::{ + AcceptLanguage, CairoRenderer, Color, Dpi, Language, LengthUnit, Loader, Rect, RenderingError, +}; + +use std::ffi::OsString; +use std::io; +use std::ops::Deref; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct Error(String); + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Error { + fn from(s: cairo::Error) -> Self { + match s { + cairo::Error::InvalidSize => Self(String::from( + "The resulting image would be larger than 32767 pixels on either dimension.\n\ + Librsvg currently cannot render to images bigger than that.\n\ + Please specify a smaller size.", + )), + e => Self(format!("{e}")), + } + } +} + +macro_rules! impl_error_from { + ($err:ty) => { + impl From<$err> for Error { + fn from(e: $err) -> Self { + Self(format!("{e}")) + } + } + }; +} + +impl_error_from!(RenderingError); +impl_error_from!(cairo::IoError); +impl_error_from!(cairo::StreamWithError); +impl_error_from!(clap::Error); + +macro_rules! error { + ($($arg:tt)*) => (Error(std::format!($($arg)*))); +} + +#[derive(Clone, Copy, Debug)] +struct Scale { + pub x: f64, + pub y: f64, +} + +impl Scale { + #[allow(clippy::float_cmp)] + pub fn is_identity(&self) -> bool { + self.x == 1.0 && self.y == 1.0 + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct Size { + pub w: f64, + pub h: f64, +} + +impl Size { + pub fn new(w: f64, h: f64) -> Self { + Self { w, h } + } +} + +#[derive(Clone, Copy, Debug)] +enum ResizeStrategy { + Scale(Scale), + Fit { + size: Size, + keep_aspect_ratio: bool, + }, + FitWidth(f64), + FitHeight(f64), + ScaleWithMaxSize { + scale: Scale, + max_width: Option, + max_height: Option, + keep_aspect_ratio: bool, + }, +} + +impl ResizeStrategy { + pub fn apply(self, input: &Size) -> Option { + if input.w == 0.0 || input.h == 0.0 { + return None; + } + + let output_size = match self { + ResizeStrategy::Scale(s) => Size::new(input.w * s.x, input.h * s.y), + + ResizeStrategy::Fit { + size, + keep_aspect_ratio, + } => { + if keep_aspect_ratio { + let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap(); + let rect = aspect.compute( + &ViewBox::from(Rect::from_size(input.w, input.h)), + &Rect::from_size(size.w, size.h), + ); + Size::new(rect.width(), rect.height()) + } else { + size + } + } + + ResizeStrategy::FitWidth(w) => Size::new(w, input.h * w / input.w), + + ResizeStrategy::FitHeight(h) => Size::new(input.w * h / input.h, h), + + ResizeStrategy::ScaleWithMaxSize { + scale, + max_width, + max_height, + keep_aspect_ratio, + } => { + let scaled = Size::new(input.w * scale.x, input.h * scale.y); + + match (max_width, max_height, keep_aspect_ratio) { + (None, None, _) => scaled, + + (Some(max_width), Some(max_height), false) => { + if scaled.w <= max_width && scaled.h <= max_height { + scaled + } else { + Size::new(max_width, max_height) + } + } + + (Some(max_width), Some(max_height), true) => { + if scaled.w <= max_width && scaled.h <= max_height { + scaled + } else { + let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap(); + let rect = aspect.compute( + &ViewBox::from(Rect::from_size(scaled.w, scaled.h)), + &Rect::from_size(max_width, max_height), + ); + Size::new(rect.width(), rect.height()) + } + } + + (Some(max_width), None, false) => { + if scaled.w <= max_width { + scaled + } else { + Size::new(max_width, scaled.h) + } + } + + (Some(max_width), None, true) => { + if scaled.w <= max_width { + scaled + } else { + let factor = max_width / scaled.w; + Size::new(max_width, scaled.h * factor) + } + } + + (None, Some(max_height), false) => { + if scaled.h <= max_height { + scaled + } else { + Size::new(scaled.w, max_height) + } + } + + (None, Some(max_height), true) => { + if scaled.h <= max_height { + scaled + } else { + let factor = max_height / scaled.h; + Size::new(scaled.w * factor, max_height) + } + } + } + } + }; + + Some(output_size) + } +} + +enum Surface { + Png(cairo::ImageSurface, OutputStream), + #[cfg(system_deps_have_cairo_pdf)] + Pdf(cairo::PdfSurface, Size), + #[cfg(system_deps_have_cairo_ps)] + Ps(cairo::PsSurface, Size), + #[cfg(system_deps_have_cairo_svg)] + Svg(cairo::SvgSurface, Size), +} + +impl Deref for Surface { + type Target = cairo::Surface; + + fn deref(&self) -> &cairo::Surface { + match self { + Self::Png(surface, _) => surface, + #[cfg(system_deps_have_cairo_pdf)] + Self::Pdf(surface, _) => surface, + #[cfg(system_deps_have_cairo_ps)] + Self::Ps(surface, _) => surface, + #[cfg(system_deps_have_cairo_svg)] + Self::Svg(surface, _) => surface, + } + } +} + +impl AsRef for Surface { + fn as_ref(&self) -> &cairo::Surface { + self + } +} + +impl Surface { + pub fn new( + format: Format, + size: Size, + stream: OutputStream, + unit: LengthUnit, + ) -> Result { + match format { + Format::Png => Self::new_for_png(size, stream), + Format::Pdf => Self::new_for_pdf(size, stream), + Format::Ps => Self::new_for_ps(size, stream, false), + Format::Eps => Self::new_for_ps(size, stream, true), + Format::Svg => Self::new_for_svg(size, stream, unit), + } + } + + fn new_for_png(size: Size, stream: OutputStream) -> Result { + // We use ceil() to avoid chopping off the last pixel if it is partially covered. + let w = checked_i32(size.w.ceil())?; + let h = checked_i32(size.h.ceil())?; + let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h)?; + Ok(Self::Png(surface, stream)) + } + + #[cfg(system_deps_have_cairo_pdf)] + fn new_for_pdf(size: Size, stream: OutputStream) -> Result { + let surface = cairo::PdfSurface::for_stream(size.w, size.h, stream.into_write())?; + if let Some(date) = metadata::creation_date()? { + surface.set_metadata(cairo::PdfMetadata::CreateDate, &date)?; + } + Ok(Self::Pdf(surface, size)) + } + + #[cfg(not(system_deps_have_cairo_pdf))] + fn new_for_pdf(_size: Size, _stream: OutputStream) -> Result { + Err(Error("unsupported format".to_string())) + } + + #[cfg(system_deps_have_cairo_ps)] + fn new_for_ps(size: Size, stream: OutputStream, eps: bool) -> Result { + let surface = cairo::PsSurface::for_stream(size.w, size.h, stream.into_write())?; + surface.set_eps(eps); + Ok(Self::Ps(surface, size)) + } + + #[cfg(not(system_deps_have_cairo_ps))] + fn new_for_ps(_size: Size, _stream: OutputStream, _eps: bool) -> Result { + Err(Error("unsupported format".to_string())) + } + + #[cfg(system_deps_have_cairo_svg)] + fn new_for_svg(size: Size, stream: OutputStream, unit: LengthUnit) -> Result { + let mut surface = cairo::SvgSurface::for_stream(size.w, size.h, stream.into_write())?; + + let svg_unit = match unit { + LengthUnit::Cm => cairo::SvgUnit::Cm, + LengthUnit::In => cairo::SvgUnit::In, + LengthUnit::Mm => cairo::SvgUnit::Mm, + LengthUnit::Pc => cairo::SvgUnit::Pc, + LengthUnit::Pt => cairo::SvgUnit::Pt, + _ => cairo::SvgUnit::User, + }; + + surface.set_document_unit(svg_unit); + Ok(Self::Svg(surface, size)) + } + + #[cfg(not(system_deps_have_cairo_svg))] + fn new_for_svg(_size: Size, _stream: OutputStream, u: LengthUnit) -> Result { + Err(Error("unsupported format".to_string())) + } + + #[allow(clippy::too_many_arguments)] // yeah, yeah, we'll refactor it eventually + pub fn render( + &self, + renderer: &CairoRenderer, + left: f64, + top: f64, + final_size: Size, + geometry: cairo::Rectangle, + background_color: Option, + id: Option<&str>, + ) -> Result<(), Error> { + let cr = cairo::Context::new(self)?; + + if let Some(Color::RGBA(rgba)) = background_color { + cr.set_source_rgba( + rgba.red_f32().into(), + rgba.green_f32().into(), + rgba.blue_f32().into(), + rgba.alpha_f32().into(), + ); + + cr.paint()?; + } + + cr.translate(left, top); + + // Note that we don't scale the viewport; we change the cr's transform instead. This + // is because SVGs are rendered proportionally to fit within the viewport, regardless + // of the viewport's proportions. Rsvg-convert allows non-proportional scaling, so + // we do that with a separate transform. + + let scale = Scale { + x: final_size.w / geometry.width(), + y: final_size.h / geometry.height(), + }; + + cr.scale(scale.x, scale.y); + + let viewport = cairo::Rectangle::new(0.0, 0.0, geometry.width(), geometry.height()); + + match id { + None => renderer.render_document(&cr, &viewport)?, + Some(_) => renderer.render_element(&cr, id, &viewport)?, + } + + if !matches!(self, Self::Png(_, _)) { + cr.show_page()?; + } + + Ok(()) + } + + pub fn finish(self) -> Result<(), Error> { + match self { + Self::Png(surface, stream) => surface.write_to_png(&mut stream.into_write())?, + _ => self.finish_output_stream().map(|_| ())?, + } + + Ok(()) + } +} + +fn checked_i32(x: f64) -> Result { + cast::i32(x).map_err(|_| cairo::Error::InvalidSize) +} + +mod metadata { + use super::Error; + use chrono::prelude::*; + use std::env; + use std::str::FromStr; + + pub fn creation_date() -> Result, Error> { + match env::var("SOURCE_DATE_EPOCH") { + Ok(epoch) => match i64::from_str(&epoch) { + Ok(seconds) => { + let datetime = Utc.timestamp_opt(seconds, 0).unwrap(); + Ok(Some(datetime.to_rfc3339())) + } + Err(e) => Err(error!("Environment variable $SOURCE_DATE_EPOCH: {}", e)), + }, + Err(env::VarError::NotPresent) => Ok(None), + Err(env::VarError::NotUnicode(_)) => Err(error!( + "Environment variable $SOURCE_DATE_EPOCH is not valid Unicode" + )), + } + } +} + +struct Stdin; + +impl Stdin { + #[cfg(unix)] + pub fn stream() -> InputStream { + let stream = unsafe { UnixInputStream::with_fd(0) }; + stream.upcast::() + } + + #[cfg(windows)] + pub fn stream() -> InputStream { + let stream = unsafe { Win32InputStream::with_handle(io::stdin()) }; + stream.upcast::() + } +} + +struct Stdout; + +impl Stdout { + #[cfg(unix)] + pub fn stream() -> OutputStream { + let stream = unsafe { UnixOutputStream::with_fd(1) }; + stream.upcast::() + } + + #[cfg(windows)] + pub fn stream() -> OutputStream { + // Ideally, we could use a Win32OutputStream, but when it's used with a file redirect, + // it gets buggy. + // https://gitlab.gnome.org/GNOME/librsvg/-/issues/812 + let stream = WriteOutputStream::new(io::stdout()); + stream.upcast::() + } +} + +#[derive(Clone, Debug)] +enum Input { + Stdin, + Named(PathOrUrl), +} + +impl std::fmt::Display for Input { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Input::Stdin => "stdin".fmt(f), + Input::Named(p) => p.fmt(f), + } + } +} + +#[derive(Clone, Debug)] +enum Output { + Stdout, + Path(PathBuf), +} + +impl std::fmt::Display for Output { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Output::Stdout => "stdout".fmt(f), + Output::Path(p) => p.display().fmt(f), + } + } +} + +// Keep this enum in sync with supported_formats in parse_args() +#[derive(Clone, Copy, Debug)] +enum Format { + Png, + Pdf, + Ps, + Eps, + Svg, +} + +struct Converter { + pub dpi_x: Resolution, + pub dpi_y: Resolution, + pub zoom: Scale, + pub width: Option>, + pub height: Option>, + pub left: Option>, + pub top: Option>, + pub page_size: Option<(ULength, ULength)>, + pub format: Format, + pub export_id: Option, + pub keep_aspect_ratio: bool, + pub background_color: Option, + pub stylesheet: Option, + pub language: Language, + pub unlimited: bool, + pub keep_image_data: bool, + pub input: Vec, + pub output: Output, + pub testing: bool, +} + +impl Converter { + pub fn convert(self) -> Result<(), Error> { + let stylesheet = match self.stylesheet { + Some(ref p) => std::fs::read_to_string(p) + .map(Some) + .map_err(|e| error!("Error reading stylesheet: {}", e))?, + None => None, + }; + + let mut surface: Option = None; + + // Use user units per default + let mut unit = LengthUnit::Px; + + fn set_unit( + l: CssLength, + p: &NormalizeParams, + u: LengthUnit, + ) -> f64 { + match u { + LengthUnit::Pt => l.to_points(p), + LengthUnit::In => l.to_inches(p), + LengthUnit::Cm => l.to_cm(p), + LengthUnit::Mm => l.to_mm(p), + LengthUnit::Pc => l.to_picas(p), + _ => l.to_user(p), + } + } + + for (page_idx, input) in self.input.iter().enumerate() { + let (stream, basefile) = match input { + Input::Stdin => (Stdin::stream(), None), + Input::Named(p) => { + let file = p.get_gfile(); + let stream = file + .read(None::<&Cancellable>) + .map_err(|e| error!("Error reading file \"{}\": {}", input, e))?; + (stream.upcast::(), Some(file)) + } + }; + + let mut handle = Loader::new() + .with_unlimited_size(self.unlimited) + .keep_image_data(self.keep_image_data) + .read_stream(&stream, basefile.as_ref(), None::<&Cancellable>) + .map_err(|e| error!("Error reading SVG {}: {}", input, e))?; + + if let Some(ref css) = stylesheet { + handle + .set_stylesheet(css) + .map_err(|e| error!("Error applying stylesheet: {}", e))?; + } + + let renderer = CairoRenderer::new(&handle) + .with_dpi(self.dpi_x.0, self.dpi_y.0) + .with_language(&self.language) + .test_mode(self.testing); + + let geometry = natural_geometry(&renderer, input, self.export_id.as_deref())?; + + let natural_size = Size::new(geometry.width(), geometry.height()); + + let params = NormalizeParams::from_dpi(Dpi::new(self.dpi_x.0, self.dpi_y.0)); + + // Convert natural size and requested size to pixels or points, depending on the target format, + let (natural_size, requested_width, requested_height, page_size) = match self.format { + Format::Png => { + // PNG surface requires units in pixels + ( + natural_size, + self.width.map(|l| l.to_user(¶ms)), + self.height.map(|l| l.to_user(¶ms)), + self.page_size.map(|(w, h)| Size { + w: w.to_user(¶ms), + h: h.to_user(¶ms), + }), + ) + } + + Format::Pdf | Format::Ps | Format::Eps => { + // These surfaces require units in points + unit = LengthUnit::Pt; + + ( + Size { + w: ULength::::new(natural_size.w, LengthUnit::Px) + .to_points(¶ms), + h: ULength::::new(natural_size.h, LengthUnit::Px) + .to_points(¶ms), + }, + self.width.map(|l| l.to_points(¶ms)), + self.height.map(|l| l.to_points(¶ms)), + self.page_size.map(|(w, h)| Size { + w: w.to_points(¶ms), + h: h.to_points(¶ms), + }), + ) + } + + Format::Svg => { + let (w_unit, h_unit) = + (self.width.map(|l| l.unit), self.height.map(|l| l.unit)); + + unit = match (w_unit, h_unit) { + (None, None) => LengthUnit::Px, + (None, u) | (u, None) => u.unwrap(), + (u1, u2) => { + if u1 == u2 { + u1.unwrap() + } else { + LengthUnit::Px + } + } + }; + + // Supported SVG units are px, in, cm, mm, pt, pc + ( + Size { + w: set_unit( + ULength::::new(natural_size.w, LengthUnit::Px), + ¶ms, + unit, + ), + h: set_unit( + ULength::::new(natural_size.h, LengthUnit::Px), + ¶ms, + unit, + ), + }, + self.width.map(|l| set_unit(l, ¶ms, unit)), + self.height.map(|l| set_unit(l, ¶ms, unit)), + self.page_size.map(|(w, h)| Size { + w: set_unit(w, ¶ms, unit), + h: set_unit(h, ¶ms, unit), + }), + ) + } + }; + + let strategy = match (requested_width, requested_height) { + // when w and h are not specified, scale to the requested zoom (if any) + (None, None) => ResizeStrategy::Scale(self.zoom), + + // when w and h are specified, but zoom is not, scale to the requested size + (Some(width), Some(height)) if self.zoom.is_identity() => ResizeStrategy::Fit { + size: Size::new(width, height), + keep_aspect_ratio: self.keep_aspect_ratio, + }, + + // if only one between w and h is specified and there is no zoom, scale to the + // requested w or h and use the same scaling factor for the other + (Some(w), None) if self.zoom.is_identity() => ResizeStrategy::FitWidth(w), + (None, Some(h)) if self.zoom.is_identity() => ResizeStrategy::FitHeight(h), + + // otherwise scale the image, but cap the zoom to match the requested size + _ => ResizeStrategy::ScaleWithMaxSize { + scale: self.zoom, + max_width: requested_width, + max_height: requested_height, + keep_aspect_ratio: self.keep_aspect_ratio, + }, + }; + + let final_size = self.final_size(&strategy, &natural_size, input)?; + + // Create the surface once on the first input, + // except for PDF, PS, and EPS, which allow differently-sized pages. + let page_size = page_size.unwrap_or(final_size); + let s = match &mut surface { + Some(s) => { + match s { + #[cfg(system_deps_have_cairo_pdf)] + Surface::Pdf(pdf, size) => { + pdf.set_size(page_size.w, page_size.h).map_err(|e| { + error!( + "Error setting PDF page #{} size {}: {}", + page_idx + 1, + input, + e + ) + })?; + *size = page_size; + } + #[cfg(system_deps_have_cairo_ps)] + Surface::Ps(ps, size) => { + ps.set_size(page_size.w, page_size.h); + *size = page_size; + } + _ => {} + } + s + } + surface @ None => surface.insert(self.create_surface(page_size, unit)?), + }; + + let left = self.left.map(|l| set_unit(l, ¶ms, unit)).unwrap_or(0.0); + let top = self.top.map(|l| set_unit(l, ¶ms, unit)).unwrap_or(0.0); + + s.render( + &renderer, + left, + top, + final_size, + geometry, + self.background_color, + self.export_id.as_deref(), + ) + .map_err(|e| error!("Error rendering SVG {}: {}", input, e))? + } + + if let Some(s) = surface.take() { + s.finish() + .map_err(|e| error!("Error saving output {}: {}", self.output, e))? + }; + + Ok(()) + } + + fn final_size( + &self, + strategy: &ResizeStrategy, + natural_size: &Size, + input: &Input, + ) -> Result { + strategy + .apply(natural_size) + .ok_or_else(|| error!("The SVG {} has no dimensions", input)) + } + + fn create_surface(&self, size: Size, unit: LengthUnit) -> Result { + let output_stream = match self.output { + Output::Stdout => Stdout::stream(), + Output::Path(ref p) => { + let file = gio::File::for_path(p); + let stream = file + .replace(None, false, FileCreateFlags::NONE, None::<&Cancellable>) + .map_err(|e| error!("Error opening output \"{}\": {}", self.output, e))?; + stream.upcast::() + } + }; + + Surface::new(self.format, size, output_stream, unit) + } +} + +fn natural_geometry( + renderer: &CairoRenderer, + input: &Input, + export_id: Option<&str>, +) -> Result { + match export_id { + None => renderer.legacy_layer_geometry(None), + Some(id) => renderer.geometry_for_element(Some(id)), + } + .map(|(ink_r, _)| ink_r) + .map_err(|e| match e { + RenderingError::IdNotFound => error!( + "File {} does not have an object with id \"{}\")", + input, + export_id.unwrap() + ), + _ => error!("Error rendering SVG {}: {}", input, e), + }) +} + +fn build_cli() -> clap::Command { + let supported_formats = vec![ + "png", + #[cfg(system_deps_have_cairo_pdf)] + "pdf", + #[cfg(system_deps_have_cairo_ps)] + "ps", + #[cfg(system_deps_have_cairo_ps)] + "eps", + #[cfg(system_deps_have_cairo_svg)] + "svg", + ]; + + clap::Command::new("rsvg-convert") + .version(concat!("version ", crate_version!())) + .about("Convert SVG files to other image formats") + .disable_version_flag(true) + .disable_help_flag(true) + .arg( + clap::Arg::new("help") + .short('?') + .long("help") + .help("Display the help") + .action(clap::ArgAction::Help) + ) + .arg( + clap::Arg::new("version") + .short('v') + .long("version") + .help("Display the version information") + .action(clap::ArgAction::Version) + ) + .arg( + clap::Arg::new("res_x") + .short('d') + .long("dpi-x") + .num_args(1) + .value_name("number") + .default_value("96") + .value_parser(parse_resolution) + .help("Pixels per inch") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("res_y") + .short('p') + .long("dpi-y") + .num_args(1) + .value_name("number") + .default_value("96") + .value_parser(parse_resolution) + .help("Pixels per inch") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("zoom_x") + .short('x') + .long("x-zoom") + .num_args(1) + .value_name("number") + .conflicts_with("zoom") + .value_parser(parse_zoom_factor) + .help("Horizontal zoom factor") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("zoom_y") + .short('y') + .long("y-zoom") + .num_args(1) + .value_name("number") + .conflicts_with("zoom") + .value_parser(parse_zoom_factor) + .help("Vertical zoom factor") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("zoom") + .short('z') + .long("zoom") + .num_args(1) + .value_name("number") + .value_parser(parse_zoom_factor) + .help("Zoom factor") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("size_x") + .short('w') + .long("width") + .num_args(1) + .value_name("length") + .value_parser(parse_length::) + .help("Width [defaults to the width of the SVG]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("size_y") + .short('h') + .long("height") + .num_args(1) + .value_name("length") + .value_parser(parse_length::) + .help("Height [defaults to the height of the SVG]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("top") + .long("top") + .num_args(1) + .value_name("length") + .value_parser(parse_length::) + .help("Distance between top edge of page and the image [defaults to 0]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("left") + .long("left") + .num_args(1) + .value_name("length") + .value_parser(parse_length::) + .help("Distance between left edge of page and the image [defaults to 0]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("page_width") + .long("page-width") + .num_args(1) + .value_name("length") + .value_parser(parse_length::) + .help("Width of output media [defaults to the width of the SVG]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("page_height") + .long("page-height") + .num_args(1) + .value_name("length") + .value_parser(parse_length::) + .help("Height of output media [defaults to the height of the SVG]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("format") + .short('f') + .long("format") + .num_args(1) + .value_parser(clap::builder::PossibleValuesParser::new(supported_formats.as_slice())) + .ignore_case(true) + .default_value("png") + .help("Output format") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("output") + .short('o') + .long("output") + .num_args(1) + .value_parser(clap::value_parser!(PathBuf)) + .help("Output filename [defaults to stdout]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("export_id") + .short('i') + .long("export-id") + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .value_name("object id") + .help("SVG id of object to export [default is to export all objects]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("accept-language") + .short('l') + .long("accept-language") + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .value_name("languages") + .help("Languages to accept, for example \"es-MX,de,en\" [default uses language from the environment]") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("keep_aspect") + .short('a') + .long("keep-aspect-ratio") + .help("Preserve the aspect ratio") + .action(clap::ArgAction::SetTrue), + ) + .arg( + clap::Arg::new("background") + .short('b') + .long("background-color") + .num_args(1) + .value_name("color") + .value_parser(clap::builder::NonEmptyStringValueParser::new()) + .default_value("none") + .help("Set the background color using a CSS color spec") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("stylesheet") + .short('s') + .long("stylesheet") + .num_args(1) + .value_parser(clap::value_parser!(PathBuf)) + .value_name("filename.css") + .help("Filename of CSS stylesheet to apply") + .action(clap::ArgAction::Set), + ) + .arg( + clap::Arg::new("unlimited") + .short('u') + .long("unlimited") + .help("Allow huge SVG files") + .action(clap::ArgAction::SetTrue), + ) + .arg( + clap::Arg::new("keep_image_data") + .long("keep-image-data") + .help("Keep image data") + .conflicts_with("no_keep_image_data") + .action(clap::ArgAction::SetTrue), + ) + .arg( + clap::Arg::new("no_keep_image_data") + .long("no-keep-image-data") + .help("Do not keep image data") + .conflicts_with("keep_image_data") + .action(clap::ArgAction::SetTrue), + ) + .arg( + clap::Arg::new("testing") + .long("testing") + .help("Render images for librsvg's test suite") + .hide(true) + .action(clap::ArgAction::SetTrue), + ) + .arg( + clap::Arg::new("completion") + .long("completion") + .help("Output shell completion for the given shell") + .num_args(1) + .action(clap::ArgAction::Set) + .value_parser(clap::value_parser!(Shell)), + ) + .arg( + clap::Arg::new("FILE") + .value_parser(clap::value_parser!(OsString)) + .help("The input file(s) to convert") + .num_args(1..) + .action(clap::ArgAction::Append), + ) +} + +fn print_completions(gen: G, cmd: &mut clap::Command) { + clap_complete::generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); +} + +fn parse_args() -> Result { + let cli = build_cli(); + let matches = cli.get_matches(); + + if let Some(shell) = matches.get_one::("completion").copied() { + let mut cmd = build_cli(); + eprintln!("Generating completion file for {shell}"); + print_completions(shell, &mut cmd); + std::process::exit(0); + } + + let format_str: &String = matches + .get_one("format") + .expect("already provided default_value"); + + let format = match_ignore_ascii_case! { + format_str, + "png" => Format::Png, + "pdf" => Format::Pdf, + "ps" => Format::Ps, + "eps" => Format::Eps, + "svg" => Format::Svg, + _ => unreachable!("clap should already have the list of possible values"), + }; + + let keep_image_data = match format { + Format::Ps | Format::Eps | Format::Pdf => !matches.get_flag("no_keep_image_data"), + _ => matches.get_flag("keep_image_data"), + }; + + let language = match matches.get_one::("accept-language") { + None => Language::FromEnvironment, + Some(s) => AcceptLanguage::parse(s) + .map(Language::AcceptLanguage) + .map_err(|e| { + let desc = format!("{e}"); + clap::Error::raw(clap::error::ErrorKind::InvalidValue, desc) + })?, + }; + + let background_str: &String = matches + .get_one("background") + .expect("already provided default_value"); + let background_color: Option = parse_background_color(background_str) + .map_err(|e| clap::Error::raw(clap::error::ErrorKind::InvalidValue, e))?; + + // librsvg expects ids starting with '#', so it can lookup ids in externs like "subfile.svg#subid". + // For the user's convenience, we prepend '#' automatically; we only support specifying ids from + // the toplevel, and don't expect users to lookup things in externs. + let lookup_id = |id: &String| { + if id.starts_with('#') { + id.clone() + } else { + format!("#{id}") + } + }; + + let width: Option> = matches.get_one("size_x").copied(); + let height: Option> = matches.get_one("size_y").copied(); + + let left: Option> = matches.get_one("left").copied(); + let top: Option> = matches.get_one("top").copied(); + + let page_width: Option> = matches.get_one("page_width").copied(); + let page_height: Option> = matches.get_one("page_height").copied(); + + let page_size = match (page_width, page_height) { + (None, None) => None, + (Some(_), None) | (None, Some(_)) => { + return Err(error!( + "Please specify both the --page-width and --page-height options together." + )); + } + (Some(w), Some(h)) => Some((w, h)), + }; + + let dpi_x = *matches + .get_one::("res_x") + .expect("already provided default_value"); + let dpi_y = *matches + .get_one::("res_y") + .expect("already provided default_value"); + + let zoom: Option = matches.get_one("zoom").copied(); + let zoom_x: Option = matches.get_one("zoom_x").copied(); + let zoom_y: Option = matches.get_one("zoom_y").copied(); + + let input = match matches.get_many::("FILE") { + Some(values) => values + .map(|f| PathOrUrl::from_os_str(f).map_err(Error)) + .map(|r| r.map(Input::Named)) + .collect::, Error>>()?, + + None => vec![Input::Stdin], + }; + + if input.len() > 1 && !matches!(format, Format::Ps | Format::Eps | Format::Pdf) { + return Err(error!( + "Multiple SVG files are only allowed for PDF and (E)PS output." + )); + } + + let export_id: Option = matches.get_one::("export_id").map(lookup_id); + + let output = match matches.get_one::("output") { + None => Output::Stdout, + Some(path) => Output::Path(path.clone()), + }; + + Ok(Converter { + dpi_x, + dpi_y, + zoom: Scale { + x: zoom.or(zoom_x).map(|factor| factor.0).unwrap_or(1.0), + y: zoom.or(zoom_y).map(|factor| factor.0).unwrap_or(1.0), + }, + width, + height, + left, + top, + page_size, + format, + export_id, + keep_aspect_ratio: matches.get_flag("keep_aspect"), + background_color, + stylesheet: matches.get_one("stylesheet").cloned(), + unlimited: matches.get_flag("unlimited"), + keep_image_data, + language, + input, + output, + testing: matches.get_flag("testing"), + }) +} + +#[derive(Copy, Clone)] +struct Resolution(f64); + +fn parse_resolution(v: &str) -> Result { + match v.parse::() { + Ok(res) if res > 0.0 => Ok(Resolution(res)), + Ok(_) => Err(String::from("Invalid resolution")), + Err(e) => Err(format!("{e}")), + } +} + +#[derive(Copy, Clone)] +struct ZoomFactor(f64); + +fn parse_zoom_factor(v: &str) -> Result { + match v.parse::() { + Ok(res) if res > 0.0 => Ok(ZoomFactor(res)), + Ok(_) => Err(String::from("Invalid zoom factor")), + Err(e) => Err(format!("{e}")), + } +} + +trait NotFound { + type Ok; + type Error; + + fn or_none(self) -> Result, Self::Error>; +} + +impl NotFound for Result { + type Ok = T; + type Error = clap::Error; + + /// Maps the Result to an Option, translating the ArgumentNotFound error to + /// Ok(None), while mapping other kinds of errors to Err(e). + /// + /// This allows to get proper error reporting for invalid values on optional + /// arguments. + fn or_none(self) -> Result, clap::Error> { + self.map_or_else( + |e| match e.kind() { + clap::error::ErrorKind::UnknownArgument => Ok(None), + _ => Err(e), + }, + |v| Ok(Some(v)), + ) + } +} + +fn parse_background_color(s: &str) -> Result, String> { + match s { + "none" | "None" => Ok(None), + _ => ::parse_str(s).map(Some).map_err(|_| { + format!("Invalid value: The argument '{s}' can not be parsed as a CSS color value") + }), + } +} + +fn is_absolute_unit(u: LengthUnit) -> bool { + use LengthUnit::*; + + match u { + Percent | Em | Ex => false, + Px | In | Cm | Mm | Pt | Pc => true, + } +} + +fn parse_length(s: &str) -> Result, String> { + as Parse>::parse_str(s) + .map_err(|_| format!("Invalid value: The argument '{s}' can not be parsed as a length")) + .and_then(|l| { + if is_absolute_unit(l.unit) { + Ok(l) + } else { + Err(format!( + "Invalid value '{s}': supported units are px, in, cm, mm, pt, pc" + )) + } + }) +} + +fn main() { + if let Err(e) = parse_args().and_then(|converter| converter.convert()) { + std::eprintln!("{e}"); + std::process::exit(1); + } +} + +#[cfg(test)] +mod color_tests { + use super::*; + + #[test] + fn valid_color_is_ok() { + assert!(parse_background_color("Red").is_ok()); + } + + #[test] + fn none_is_handled_as_transparent() { + assert_eq!(parse_background_color("None").unwrap(), None,); + } + + #[test] + fn invalid_is_handled_as_invalid_value() { + assert!(parse_background_color("foo").is_err()); + } +} + +#[cfg(test)] +mod sizing_tests { + use super::*; + + #[test] + fn detects_empty_size() { + let strategy = ResizeStrategy::Scale(Scale { x: 42.0, y: 42.0 }); + assert!(strategy.apply(&Size::new(0.0, 0.0)).is_none()); + } + + #[test] + fn scale() { + let strategy = ResizeStrategy::Scale(Scale { x: 2.0, y: 3.0 }); + assert_eq!( + strategy.apply(&Size::new(1.0, 2.0)).unwrap(), + Size::new(2.0, 6.0), + ); + } + + #[test] + fn fit_non_proportional() { + let strategy = ResizeStrategy::Fit { + size: Size::new(40.0, 10.0), + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(2.0, 1.0)).unwrap(), + Size::new(40.0, 10.0), + ); + } + + #[test] + fn fit_proportional_wider_than_tall() { + let strategy = ResizeStrategy::Fit { + size: Size::new(40.0, 10.0), + keep_aspect_ratio: true, + }; + + assert_eq!( + strategy.apply(&Size::new(2.0, 1.0)).unwrap(), + Size::new(20.0, 10.0), + ); + } + + #[test] + fn fit_proportional_taller_than_wide() { + let strategy = ResizeStrategy::Fit { + size: Size::new(100.0, 50.0), + keep_aspect_ratio: true, + }; + + assert_eq!( + strategy.apply(&Size::new(1.0, 2.0)).unwrap(), + Size::new(25.0, 50.0), + ); + } + + #[test] + fn fit_width() { + let strategy = ResizeStrategy::FitWidth(100.0); + + assert_eq!( + strategy.apply(&Size::new(1.0, 2.0)).unwrap(), + Size::new(100.0, 200.0), + ); + } + + #[test] + fn fit_height() { + let strategy = ResizeStrategy::FitHeight(100.0); + + assert_eq!( + strategy.apply(&Size::new(1.0, 2.0)).unwrap(), + Size::new(50.0, 100.0), + ); + } + + #[test] + fn scale_no_max_size_non_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 2.0, y: 3.0 }, + max_width: None, + max_height: None, + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(1.0, 2.0)).unwrap(), + Size::new(2.0, 6.0), + ); + } + + #[test] + fn scale_with_max_width_and_height_fits_non_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 2.0, y: 3.0 }, + max_width: Some(10.0), + max_height: Some(20.0), + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(4.0, 2.0)).unwrap(), + Size::new(8.0, 6.0) + ); + } + + #[test] + fn scale_with_max_width_and_height_fits_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 2.0, y: 3.0 }, + max_width: Some(10.0), + max_height: Some(20.0), + keep_aspect_ratio: true, + }; + + assert_eq!( + strategy.apply(&Size::new(4.0, 2.0)).unwrap(), + Size::new(8.0, 6.0) + ); + } + + #[test] + fn scale_with_max_width_and_height_doesnt_fit_non_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 10.0, y: 20.0 }, + max_width: Some(10.0), + max_height: Some(20.0), + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(4.0, 5.0)).unwrap(), + Size::new(10.0, 20.0) + ); + } + + #[test] + fn scale_with_max_width_and_height_doesnt_fit_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 10.0, y: 20.0 }, + max_width: Some(10.0), + max_height: Some(15.0), + keep_aspect_ratio: true, + }; + + assert_eq!( + // this will end up with a 40:120 aspect ratio + strategy.apply(&Size::new(4.0, 6.0)).unwrap(), + // which should be shrunk to 1:3 that fits in (10, 15) per the max_width/max_height above + Size::new(5.0, 15.0) + ); + } + + #[test] + fn scale_with_max_width_fits_non_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 5.0, y: 20.0 }, + max_width: Some(10.0), + max_height: None, + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(1.0, 10.0)).unwrap(), + Size::new(5.0, 200.0), + ); + } + + #[test] + fn scale_with_max_width_fits_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 5.0, y: 20.0 }, + max_width: Some(10.0), + max_height: None, + keep_aspect_ratio: true, + }; + + assert_eq!( + strategy.apply(&Size::new(1.0, 10.0)).unwrap(), + Size::new(5.0, 200.0), + ); + } + + #[test] + fn scale_with_max_height_fits_non_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 20.0, y: 5.0 }, + max_width: None, + max_height: Some(10.0), + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(10.0, 1.0)).unwrap(), + Size::new(200.0, 5.0), + ); + } + + #[test] + fn scale_with_max_height_fits_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 20.0, y: 5.0 }, + max_width: None, + max_height: Some(10.0), + keep_aspect_ratio: true, + }; + + assert_eq!( + strategy.apply(&Size::new(10.0, 1.0)).unwrap(), + Size::new(200.0, 5.0), + ); + } + + #[test] + fn scale_with_max_width_doesnt_fit_non_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 10.0, y: 20.0 }, + max_width: Some(10.0), + max_height: None, + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(5.0, 10.0)).unwrap(), + Size::new(10.0, 200.0), + ); + } + + #[test] + fn scale_with_max_width_doesnt_fit_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 10.0, y: 20.0 }, + max_width: Some(10.0), + max_height: None, + keep_aspect_ratio: true, + }; + + assert_eq!( + strategy.apply(&Size::new(5.0, 10.0)).unwrap(), + Size::new(10.0, 40.0), + ); + } + + #[test] + fn scale_with_max_height_doesnt_fit_non_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 10.0, y: 20.0 }, + max_width: None, + max_height: Some(10.0), + keep_aspect_ratio: false, + }; + + assert_eq!( + strategy.apply(&Size::new(5.0, 10.0)).unwrap(), + Size::new(50.0, 10.0), + ); + } + + #[test] + fn scale_with_max_height_doesnt_fit_proportional() { + let strategy = ResizeStrategy::ScaleWithMaxSize { + scale: Scale { x: 8.0, y: 20.0 }, + max_width: None, + max_height: Some(10.0), + keep_aspect_ratio: true, + }; + + assert_eq!( + strategy.apply(&Size::new(5.0, 10.0)).unwrap(), + Size::new(2.0, 10.0), + ); + } +} diff --git a/rsvg_convert/tests/fixtures/a-link.svg b/rsvg_convert/tests/fixtures/a-link.svg new file mode 100644 index 00000000..1ae8ace5 --- /dev/null +++ b/rsvg_convert/tests/fixtures/a-link.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/rsvg_convert/tests/fixtures/accept-language-de.png b/rsvg_convert/tests/fixtures/accept-language-de.png new file mode 100644 index 00000000..cc797dc2 Binary files /dev/null and b/rsvg_convert/tests/fixtures/accept-language-de.png differ diff --git a/rsvg_convert/tests/fixtures/accept-language-es.png b/rsvg_convert/tests/fixtures/accept-language-es.png new file mode 100644 index 00000000..4cf3a21f Binary files /dev/null and b/rsvg_convert/tests/fixtures/accept-language-es.png differ diff --git a/rsvg_convert/tests/fixtures/accept-language-fallback.png b/rsvg_convert/tests/fixtures/accept-language-fallback.png new file mode 100644 index 00000000..43b20f01 Binary files /dev/null and b/rsvg_convert/tests/fixtures/accept-language-fallback.png differ diff --git a/rsvg_convert/tests/fixtures/accept-language.svg b/rsvg_convert/tests/fixtures/accept-language.svg new file mode 100644 index 00000000..c132b65d --- /dev/null +++ b/rsvg_convert/tests/fixtures/accept-language.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/rsvg_convert/tests/fixtures/bug521-with-viewbox.svg b/rsvg_convert/tests/fixtures/bug521-with-viewbox.svg new file mode 100644 index 00000000..c3f34e6d --- /dev/null +++ b/rsvg_convert/tests/fixtures/bug521-with-viewbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg b/rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg new file mode 100644 index 00000000..1cee7759 --- /dev/null +++ b/rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png new file mode 100644 index 00000000..1ed070ca Binary files /dev/null and b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png differ diff --git a/rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg new file mode 100644 index 00000000..ee96d474 --- /dev/null +++ b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/rsvg_convert/tests/fixtures/bug677-partial-pixel.svg b/rsvg_convert/tests/fixtures/bug677-partial-pixel.svg new file mode 100644 index 00000000..aeac8c30 --- /dev/null +++ b/rsvg_convert/tests/fixtures/bug677-partial-pixel.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/rsvg_convert/tests/fixtures/dimensions-in.svg b/rsvg_convert/tests/fixtures/dimensions-in.svg new file mode 100644 index 00000000..aa4f3219 --- /dev/null +++ b/rsvg_convert/tests/fixtures/dimensions-in.svg @@ -0,0 +1,4 @@ + + + + diff --git a/rsvg_convert/tests/fixtures/dpi.svg b/rsvg_convert/tests/fixtures/dpi.svg new file mode 100644 index 00000000..499ee206 --- /dev/null +++ b/rsvg_convert/tests/fixtures/dpi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/rsvg_convert/tests/fixtures/empty.svg b/rsvg_convert/tests/fixtures/empty.svg new file mode 100644 index 00000000..01a940a2 --- /dev/null +++ b/rsvg_convert/tests/fixtures/empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/rsvg_convert/tests/fixtures/example.svg b/rsvg_convert/tests/fixtures/example.svg new file mode 100644 index 00000000..850fba3e --- /dev/null +++ b/rsvg_convert/tests/fixtures/example.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/rsvg_convert/tests/fixtures/geometry-element.svg b/rsvg_convert/tests/fixtures/geometry-element.svg new file mode 100644 index 00000000..3d707cdc --- /dev/null +++ b/rsvg_convert/tests/fixtures/geometry-element.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/rsvg_convert/tests/fixtures/gimp-wilber-ref.png b/rsvg_convert/tests/fixtures/gimp-wilber-ref.png new file mode 100644 index 00000000..606f2a4d Binary files /dev/null and b/rsvg_convert/tests/fixtures/gimp-wilber-ref.png differ diff --git a/rsvg_convert/tests/fixtures/gimp-wilber.svg b/rsvg_convert/tests/fixtures/gimp-wilber.svg new file mode 100644 index 00000000..97c821a7 --- /dev/null +++ b/rsvg_convert/tests/fixtures/gimp-wilber.svg @@ -0,0 +1,978 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rsvg_convert/tests/fixtures/hello-world.svg b/rsvg_convert/tests/fixtures/hello-world.svg new file mode 100644 index 00000000..45a65c0f --- /dev/null +++ b/rsvg_convert/tests/fixtures/hello-world.svg @@ -0,0 +1,11 @@ + + + + Hello world! + Hello again! + diff --git a/rsvg_convert/tests/fixtures/offset-png.png b/rsvg_convert/tests/fixtures/offset-png.png new file mode 100644 index 00000000..a4bc7e2a Binary files /dev/null and b/rsvg_convert/tests/fixtures/offset-png.png differ diff --git a/rsvg_convert/tests/fixtures/sub-rect-no-unit.svg b/rsvg_convert/tests/fixtures/sub-rect-no-unit.svg new file mode 100644 index 00000000..fb8312ac --- /dev/null +++ b/rsvg_convert/tests/fixtures/sub-rect-no-unit.svg @@ -0,0 +1,13 @@ + + + + diff --git a/rsvg_convert/tests/fixtures/text-a-link.svg b/rsvg_convert/tests/fixtures/text-a-link.svg new file mode 100644 index 00000000..d205c768 --- /dev/null +++ b/rsvg_convert/tests/fixtures/text-a-link.svg @@ -0,0 +1,14 @@ + + + + + This is a link to example.com + + + + + This is a link to another.example.com + + + + diff --git a/rsvg_convert/tests/fixtures/zero-offset-png.png b/rsvg_convert/tests/fixtures/zero-offset-png.png new file mode 100644 index 00000000..adbe9524 Binary files /dev/null and b/rsvg_convert/tests/fixtures/zero-offset-png.png differ diff --git a/rsvg_convert/tests/internal_predicates/file.rs b/rsvg_convert/tests/internal_predicates/file.rs new file mode 100644 index 00000000..a7af5acf --- /dev/null +++ b/rsvg_convert/tests/internal_predicates/file.rs @@ -0,0 +1,28 @@ +use predicates::prelude::*; +use predicates::str::StartsWithPredicate; + +use super::pdf::PdfPredicate; +use super::png::PngPredicate; +use super::svg::SvgPredicate; + +/// Predicates to check that some output ([u8]) is of a certain file type + +pub fn is_png() -> PngPredicate { + PngPredicate {} +} + +pub fn is_ps() -> StartsWithPredicate { + predicate::str::starts_with("%!PS-Adobe-3.0\n") +} + +pub fn is_eps() -> StartsWithPredicate { + predicate::str::starts_with("%!PS-Adobe-3.0 EPSF-3.0\n") +} + +pub fn is_pdf() -> PdfPredicate { + PdfPredicate {} +} + +pub fn is_svg() -> SvgPredicate { + SvgPredicate {} +} diff --git a/rsvg_convert/tests/internal_predicates/mod.rs b/rsvg_convert/tests/internal_predicates/mod.rs new file mode 100644 index 00000000..7e15354d --- /dev/null +++ b/rsvg_convert/tests/internal_predicates/mod.rs @@ -0,0 +1,4 @@ +pub mod file; +mod pdf; +mod png; +mod svg; diff --git a/rsvg_convert/tests/internal_predicates/pdf.rs b/rsvg_convert/tests/internal_predicates/pdf.rs new file mode 100644 index 00000000..f7872d71 --- /dev/null +++ b/rsvg_convert/tests/internal_predicates/pdf.rs @@ -0,0 +1,358 @@ +use chrono::{DateTime, Utc}; +use float_cmp::approx_eq; +use lopdf::{self, Dictionary, Object}; +use predicates::prelude::*; +use predicates::reflection::{Case, Child, PredicateReflection, Product}; +use std::cmp; +use std::fmt; + +/// Checks that the variable of type [u8] can be parsed as a PDF file. +#[derive(Debug)] +pub struct PdfPredicate {} + +impl PdfPredicate { + pub fn with_page_count(self: Self, num_pages: usize) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::PageCount(num_pages), + } + } + + pub fn with_page_size( + self: Self, + idx: usize, + width_in_points: f32, + height_in_points: f32, + ) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::PageSize( + Dimensions { + w: width_in_points, + h: height_in_points, + unit: 1.0, + }, + idx, + ), + } + } + + pub fn with_creation_date(self: Self, when: DateTime) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::CreationDate(when), + } + } + + pub fn with_link(self: Self, link: &str) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::Link(link.to_string()), + } + } + + pub fn with_text(self: Self, text: &str) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::Text(text.to_string()), + } + } +} + +impl Predicate<[u8]> for PdfPredicate { + fn eval(&self, data: &[u8]) -> bool { + lopdf::Document::load_mem(data).is_ok() + } + + fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option> { + match lopdf::Document::load_mem(data) { + Ok(_) => None, + Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), + } + } +} + +impl PredicateReflection for PdfPredicate {} + +impl fmt::Display for PdfPredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "is a PDF") + } +} + +/// Extends a PdfPredicate by a check for page count, page size or creation date. +#[derive(Debug)] +pub struct DetailPredicate { + p: PdfPredicate, + d: Detail, +} + +#[derive(Debug)] +enum Detail { + PageCount(usize), + PageSize(Dimensions, usize), + CreationDate(DateTime), + Link(String), + Text(String), +} + +/// A PDF page's dimensions from its `MediaBox`. +/// +/// Note that `w` and `h` given in `UserUnit`, which is by default 1.0 = 1/72 inch. +#[derive(Debug)] +struct Dimensions { + w: f32, + h: f32, + unit: f32, // UserUnit, in points (1/72 of an inch) +} + +impl Dimensions { + pub fn from_media_box(obj: &lopdf::Object, unit: Option) -> lopdf::Result { + let a = obj.as_array()?; + Ok(Dimensions { + w: a[2].as_float()?, + h: a[3].as_float()?, + unit: unit.unwrap_or(1.0), + }) + } + + pub fn width_in_pt(self: &Self) -> f32 { + self.w * self.unit + } + + pub fn height_in_pt(self: &Self) -> f32 { + self.h * self.unit + } +} + +impl fmt::Display for Dimensions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} pt x {} pt", self.width_in_pt(), self.height_in_pt()) + } +} + +impl cmp::PartialEq for Dimensions { + fn eq(&self, other: &Self) -> bool { + approx_eq!( + f32, + self.width_in_pt(), + other.width_in_pt(), + epsilon = 0.0001 + ) && approx_eq!( + f32, + self.height_in_pt(), + other.height_in_pt(), + epsilon = 0.0001 + ) + } +} + +impl cmp::Eq for Dimensions {} + +trait Details { + fn get_page_count(&self) -> usize; + fn get_page_size(&self, idx: usize) -> Option; + fn get_creation_date(&self) -> Option>; + fn get_from_trailer<'a>(self: &'a Self, key: &[u8]) -> lopdf::Result<&'a lopdf::Object>; + fn get_from_page<'a>( + self: &'a Self, + idx: usize, + key: &[u8], + ) -> lopdf::Result<&'a lopdf::Object>; +} + +impl DetailPredicate { + fn eval_doc(&self, doc: &lopdf::Document) -> bool { + match &self.d { + Detail::PageCount(n) => doc.get_page_count() == *n, + Detail::PageSize(d, idx) => doc.get_page_size(*idx).map_or(false, |dim| dim == *d), + Detail::CreationDate(d) => doc.get_creation_date().map_or(false, |date| date == *d), + Detail::Link(link) => document_has_link(doc, &link), + Detail::Text(text) => document_has_text(doc, &text), + } + } + + fn find_case_for_doc<'a>(&'a self, expected: bool, doc: &lopdf::Document) -> Option> { + if self.eval_doc(doc) == expected { + let product = self.product_for_doc(doc); + Some(Case::new(Some(self), false).add_product(product)) + } else { + None + } + } + + fn product_for_doc(&self, doc: &lopdf::Document) -> Product { + match &self.d { + Detail::PageCount(_) => Product::new( + "actual page count", + format!("{} page(s)", doc.get_page_count()), + ), + Detail::PageSize(_, idx) => Product::new( + "actual page size", + match doc.get_page_size(*idx) { + Some(dim) => format!("{}", dim), + None => "None".to_string(), + }, + ), + Detail::CreationDate(_) => Product::new( + "actual creation date", + format!("{:?}", doc.get_creation_date()), + ), + Detail::Link(_) => Product::new( + "actual link contents", + "FIXME: who knows, but it's not what we expected".to_string(), + ), + Detail::Text(_) => { + Product::new("actual text contents", doc.extract_text(&[1]).unwrap()) + } + } + } +} + +// Extensions to lopdf::Object; can be removed after lopdf 0.26 +trait ObjExt { + /// Get the object value as a float. + /// Unlike as_f32() this will also cast an Integer to a Real. + fn as_float(&self) -> lopdf::Result; +} + +impl ObjExt for lopdf::Object { + fn as_float(&self) -> lopdf::Result { + match *self { + lopdf::Object::Integer(ref value) => Ok(*value as f32), + lopdf::Object::Real(ref value) => Ok(*value), + _ => Err(lopdf::Error::Type), + } + } +} + +impl Details for lopdf::Document { + fn get_page_count(self: &Self) -> usize { + self.get_pages().len() + } + + fn get_page_size(self: &Self, idx: usize) -> Option { + match self.get_from_page(idx, b"MediaBox") { + Ok(obj) => { + let unit = self + .get_from_page(idx, b"UserUnit") + .and_then(ObjExt::as_float) + .ok(); + Dimensions::from_media_box(obj, unit).ok() + } + Err(_) => None, + } + } + + fn get_creation_date(self: &Self) -> Option> { + match self.get_from_trailer(b"CreationDate") { + Ok(obj) => obj.as_datetime().map(|date| date.with_timezone(&Utc)), + Err(_) => None, + } + } + + fn get_from_trailer<'a>(self: &'a Self, key: &[u8]) -> lopdf::Result<&'a lopdf::Object> { + let id = self.trailer.get(b"Info")?.as_reference()?; + self.get_object(id)?.as_dict()?.get(key) + } + + fn get_from_page<'a>( + self: &'a Self, + idx: usize, + key: &[u8], + ) -> lopdf::Result<&'a lopdf::Object> { + let mut iter = self.page_iter(); + for _ in 0..idx { + let _ = iter.next(); + } + match iter.next() { + Some(id) => self.get_object(id)?.as_dict()?.get(key), + None => Err(lopdf::Error::ObjectNotFound), + } + } +} + +impl Predicate<[u8]> for DetailPredicate { + fn eval(&self, data: &[u8]) -> bool { + match lopdf::Document::load_mem(data) { + Ok(doc) => self.eval_doc(&doc), + _ => false, + } + } + + fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { + match lopdf::Document::load_mem(data) { + Ok(doc) => self.find_case_for_doc(expected, &doc), + Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), + } + } +} + +impl PredicateReflection for DetailPredicate { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for DetailPredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.d { + Detail::PageCount(n) => write!(f, "is a PDF with {} page(s)", n), + Detail::PageSize(d, _) => write!(f, "is a PDF sized {}", d), + Detail::CreationDate(d) => write!(f, "is a PDF created {:?}", d), + Detail::Link(l) => write!(f, "is a PDF with a link to {}", l), + Detail::Text(t) => write!(f, "is a PDF with \"{}\" in its text content", t), + } + } +} + +// This is an extremely trivial test for a string being present in the document's +// text objects. +fn document_has_text(document: &lopdf::Document, needle: &str) -> bool { + if let Ok(haystack) = text_from_first_page(document) { + haystack.contains(needle) + } else { + false + } +} + +// We do a super simple test that a PDF actually contains an Annotation object +// with a particular link. We don't test that this annotation is actually linked +// from a page; that would be nicer. +fn document_has_link(document: &lopdf::Document, link_text: &str) -> bool { + document + .objects + .iter() + .map(|(_obj_id, object)| object) + .any(|obj| object_is_annotation_with_link(obj, link_text)) +} + +fn object_is_annotation_with_link(object: &Object, link_text: &str) -> bool { + object + .as_dict() + .map(|dict| dict_is_annotation(dict) && dict_has_a_with_link(dict, link_text)) + .unwrap_or(false) +} + +fn dict_is_annotation(dict: &Dictionary) -> bool { + dict.get(b"Type") + .and_then(|type_val| type_val.as_name_str()) + .map(|name| name == "Annot") + .unwrap_or(false) +} + +fn dict_has_a_with_link(dict: &Dictionary, link_text: &str) -> bool { + dict.get(b"A") + .and_then(|obj| obj.as_dict()) + .and_then(|dict| dict.get(b"URI")) + .and_then(|obj| obj.as_str()) + .map(|string| string == link_text.as_bytes()) + .unwrap_or(false) +} + +fn text_from_first_page(doc: &lopdf::Document) -> lopdf::Result { + // This is extremely simplistic; lopdf just concatenates all the text in the page + // into a single string. + doc.extract_text(&[1]) +} diff --git a/rsvg_convert/tests/internal_predicates/png.rs b/rsvg_convert/tests/internal_predicates/png.rs new file mode 100644 index 00000000..f629b510 --- /dev/null +++ b/rsvg_convert/tests/internal_predicates/png.rs @@ -0,0 +1,193 @@ +use png; +use predicates::prelude::*; +use predicates::reflection::{Case, Child, PredicateReflection, Product}; +use std::fmt; +use std::io::BufReader; +use std::path::{Path, PathBuf}; + +use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; + +use rsvg::test_utils::compare_surfaces::BufferDiff; +use rsvg::test_utils::reference_utils::{surface_from_png, Compare, Deviation, Reference}; + +/// Checks that the variable of type [u8] can be parsed as a PNG file. +#[derive(Debug)] +pub struct PngPredicate {} + +impl PngPredicate { + pub fn with_size(self: Self, w: u32, h: u32) -> SizePredicate { + SizePredicate:: { p: self, w, h } + } + + pub fn with_contents>(self: Self, reference: P) -> ReferencePredicate { + let mut path = PathBuf::new(); + path.push(reference); + ReferencePredicate:: { p: self, path } + } +} + +impl Predicate<[u8]> for PngPredicate { + fn eval(&self, data: &[u8]) -> bool { + let decoder = png::Decoder::new(data); + decoder.read_info().is_ok() + } + + fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option> { + let decoder = png::Decoder::new(data); + match decoder.read_info() { + Ok(_) => None, + Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), + } + } +} + +impl PredicateReflection for PngPredicate {} + +impl fmt::Display for PngPredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "is a PNG") + } +} + +/// Extends a PngPredicate by a check for a given size of the PNG file. +#[derive(Debug)] +pub struct SizePredicate { + p: PngPredicate, + w: u32, + h: u32, +} + +impl SizePredicate { + fn eval_info(&self, info: &png::Info) -> bool { + info.width == self.w && info.height == self.h + } + + fn find_case_for_info<'a>(&'a self, expected: bool, info: &png::Info) -> Option> { + if self.eval_info(info) == expected { + let product = self.product_for_info(info); + Some(Case::new(Some(self), false).add_product(product)) + } else { + None + } + } + + fn product_for_info(&self, info: &png::Info) -> Product { + let actual_size = format!("{} x {}", info.width, info.height); + Product::new("actual size", actual_size) + } +} + +impl Predicate<[u8]> for SizePredicate { + fn eval(&self, data: &[u8]) -> bool { + let decoder = png::Decoder::new(data); + match decoder.read_info() { + Ok(reader) => self.eval_info(&reader.info()), + _ => false, + } + } + + fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { + let decoder = png::Decoder::new(data); + match decoder.read_info() { + Ok(reader) => self.find_case_for_info(expected, reader.info()), + Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), + } + } +} + +impl PredicateReflection for SizePredicate { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for SizePredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "is a PNG with size {} x {}", self.w, self.h) + } +} + +/// Extends a PngPredicate by a comparison to the contents of a reference file +#[derive(Debug)] +pub struct ReferencePredicate { + p: PngPredicate, + path: PathBuf, +} + +impl ReferencePredicate { + fn diff_acceptable(diff: &BufferDiff) -> bool { + match diff { + BufferDiff::DifferentSizes => false, + BufferDiff::Diff(diff) => !diff.inacceptable(), + } + } + + fn diff_surface(&self, surface: &SharedImageSurface) -> Option { + let reference = Reference::from_png(&self.path) + .unwrap_or_else(|_| panic!("could not open {:?}", self.path)); + if let Ok(diff) = reference.compare(&surface) { + if !Self::diff_acceptable(&diff) { + return Some(diff); + } + } + None + } + + fn find_case_for_surface<'a>( + &'a self, + expected: bool, + surface: &SharedImageSurface, + ) -> Option> { + let diff = self.diff_surface(&surface); + if diff.is_some() != expected { + let product = self.product_for_diff(&diff.unwrap()); + Some(Case::new(Some(self), false).add_product(product)) + } else { + None + } + } + + fn product_for_diff(&self, diff: &BufferDiff) -> Product { + let difference = format!("{}", diff); + Product::new("images differ", difference) + } +} + +impl Predicate<[u8]> for ReferencePredicate { + fn eval(&self, data: &[u8]) -> bool { + if let Ok(surface) = surface_from_png(&mut BufReader::new(data)) { + let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb).unwrap(); + self.diff_surface(&surface).is_some() + } else { + false + } + } + + fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { + match surface_from_png(&mut BufReader::new(data)) { + Ok(surface) => { + let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb).unwrap(); + self.find_case_for_surface(expected, &surface) + } + Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), + } + } +} + +impl PredicateReflection for ReferencePredicate { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for ReferencePredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "is a PNG that matches the reference {}", + self.path.display() + ) + } +} diff --git a/rsvg_convert/tests/internal_predicates/svg.rs b/rsvg_convert/tests/internal_predicates/svg.rs new file mode 100644 index 00000000..70473812 --- /dev/null +++ b/rsvg_convert/tests/internal_predicates/svg.rs @@ -0,0 +1,179 @@ +use float_cmp::approx_eq; +use gio::glib::Bytes; +use gio::MemoryInputStream; +use predicates::prelude::*; +use predicates::reflection::{Case, Child, PredicateReflection, Product}; +use std::cmp; +use std::fmt; + +use rsvg::{CairoRenderer, Length, Loader, LoadingError, SvgHandle}; + +/// Checks that the variable of type [u8] can be parsed as a SVG file. +#[derive(Debug)] +pub struct SvgPredicate {} + +impl SvgPredicate { + pub fn with_size(self: Self, width: Length, height: Length) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::Size(Dimensions { + w: width, + h: height, + }), + } + } +} + +fn svg_from_bytes(data: &[u8]) -> Result { + let bytes = Bytes::from(data); + let stream = MemoryInputStream::from_bytes(&bytes); + Loader::new().read_stream(&stream, None::<&gio::File>, None::<&gio::Cancellable>) +} + +impl Predicate<[u8]> for SvgPredicate { + fn eval(&self, data: &[u8]) -> bool { + svg_from_bytes(data).is_ok() + } + + fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option> { + match svg_from_bytes(data) { + Ok(_) => None, + Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), + } + } +} + +impl PredicateReflection for SvgPredicate {} + +impl fmt::Display for SvgPredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "is an SVG") + } +} + +/// Extends a SVG Predicate by a check for its size +#[derive(Debug)] +pub struct DetailPredicate { + p: SvgPredicate, + d: Detail, +} + +#[derive(Debug)] +enum Detail { + Size(Dimensions), +} + +/// SVG's dimensions +#[derive(Debug)] +struct Dimensions { + w: Length, + h: Length, +} + +impl Dimensions { + pub fn width(self: &Self) -> f64 { + self.w.length + } + + pub fn height(self: &Self) -> f64 { + self.h.length + } +} + +impl fmt::Display for Dimensions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}{} x {}{}", + self.width(), + self.w.unit, + self.height(), + self.h.unit + ) + } +} + +impl cmp::PartialEq for Dimensions { + fn eq(&self, other: &Self) -> bool { + approx_eq!(f64, self.width(), other.width(), epsilon = 0.000_001) + && approx_eq!(f64, self.height(), other.height(), epsilon = 0.000_001) + && (self.w.unit == self.h.unit) + && (self.h.unit == other.h.unit) + && (other.h.unit == other.w.unit) + } +} + +impl cmp::Eq for Dimensions {} + +trait Details { + fn get_size(&self) -> Option; +} + +impl DetailPredicate { + fn eval_doc(&self, handle: &SvgHandle) -> bool { + match &self.d { + Detail::Size(d) => { + let renderer = CairoRenderer::new(handle); + let dimensions = renderer.intrinsic_dimensions(); + (dimensions.width, dimensions.height) == (d.w, d.h) + } + } + } + + fn find_case_for_doc<'a>(&'a self, expected: bool, handle: &SvgHandle) -> Option> { + if self.eval_doc(handle) == expected { + let product = self.product_for_doc(handle); + Some(Case::new(Some(self), false).add_product(product)) + } else { + None + } + } + + fn product_for_doc(&self, handle: &SvgHandle) -> Product { + match &self.d { + Detail::Size(_) => { + let renderer = CairoRenderer::new(handle); + let dimensions = renderer.intrinsic_dimensions(); + + Product::new( + "actual size", + format!( + "width={:?}, height={:?}", + dimensions.width, dimensions.height + ), + ) + } + } + } +} + +impl Predicate<[u8]> for DetailPredicate { + fn eval(&self, data: &[u8]) -> bool { + match svg_from_bytes(data) { + Ok(handle) => self.eval_doc(&handle), + _ => false, + } + } + + fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option> { + match svg_from_bytes(data) { + Ok(handle) => self.find_case_for_doc(expected, &handle), + Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))), + } + } +} + +impl PredicateReflection for DetailPredicate { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for DetailPredicate { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.d { + Detail::Size(d) => write!(f, "is an SVG sized {}", d), + } + } +} diff --git a/rsvg_convert/tests/rsvg_convert.rs b/rsvg_convert/tests/rsvg_convert.rs new file mode 100644 index 00000000..f58edf6c --- /dev/null +++ b/rsvg_convert/tests/rsvg_convert.rs @@ -0,0 +1,1078 @@ +//use crate::predicates::ends_with_pkg_version; +mod internal_predicates; +use internal_predicates::file; + +use assert_cmd::assert::IntoOutputPredicate; +use assert_cmd::Command; +#[cfg(system_deps_have_cairo_pdf)] +use chrono::{TimeZone, Utc}; +use predicates::boolean::*; +use predicates::prelude::*; +use predicates::str::*; +use rsvg::{Length, LengthUnit}; +use std::path::Path; +use tempfile::Builder; +use url::Url; + +// What should be tested here? +// The goal is to test the code in rsvg-convert, not the entire library. +// +// - command-line options that affect size (width, height, zoom, resolution) ✔ +// - pixel dimensions of the output (should be sufficient to do that for PNG) ✔ +// - limit on output size (32767 pixels) ✔ +// - output formats (PNG, PDF, PS, EPS, SVG) ✔ +// - multi-page output (for PDF) ✔ +// - output file option ✔ +// - SOURCE_DATA_EPOCH environment variable for PDF output ✔ +// - background color option ✔ +// - optional CSS stylesheet ✔ +// - error handling for missing SVG dimensions ✔ +// - error handling for export lookup ID ✔ +// - error handling for invalid input ✔ + +struct RsvgConvert {} + +impl RsvgConvert { + fn new() -> Command { + Command::cargo_bin("rsvg-convert").unwrap() + } + + fn new_with_input

(file: P) -> Command + where + P: AsRef, + { + let mut command = RsvgConvert::new(); + match command.pipe_stdin(&file) { + Ok(_) => command, + Err(e) => panic!("Error opening file '{}': {}", file.as_ref().display(), e), + } + } + + fn accepts_arg(option: &str) { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg(option) + .assert() + .success(); + } + + fn option_yields_output(option: &str, output_pred: I) + where + I: IntoOutputPredicate

, + P: Predicate<[u8]>, + { + RsvgConvert::new() + .arg(option) + .assert() + .success() + .stdout(output_pred); + } +} + +#[test] +fn converts_svg_from_stdin_to_png() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .assert() + .success() + .stdout(file::is_png()); +} + +#[test] +fn argument_is_input_filename() { + let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + RsvgConvert::new() + .arg(input) + .assert() + .success() + .stdout(file::is_png()); +} + +#[test] +fn argument_is_url() { + let path = Path::new("tests/fixtures/bug521-with-viewbox.svg") + .canonicalize() + .unwrap(); + let url = Url::from_file_path(path).unwrap(); + let stringified = url.as_str(); + assert!(stringified.starts_with("file://")); + + RsvgConvert::new() + .arg(stringified) + .assert() + .success() + .stdout(file::is_png()); +} + +#[test] +fn output_format_png() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--format=png") + .assert() + .success() + .stdout(file::is_png()); +} + +#[cfg(system_deps_have_cairo_ps)] +#[test] +fn output_format_ps() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--format=ps") + .assert() + .success() + .stdout(file::is_ps()); +} + +#[cfg(system_deps_have_cairo_ps)] +#[test] +fn output_format_eps() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--format=eps") + .assert() + .success() + .stdout(file::is_eps()); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn output_format_pdf() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--format=pdf") + .assert() + .success() + .stdout(file::is_pdf()); +} + +#[cfg(system_deps_have_cairo_svg)] +#[test] +fn output_format_svg_short_option() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("-f") + .arg("svg") + .assert() + .success() + .stdout(file::is_svg()); +} + +#[cfg(system_deps_have_cairo_svg)] +#[test] +fn user_specified_width_and_height() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--format") + .arg("svg") + .arg("--width") + .arg("42cm") + .arg("--height") + .arg("43cm") + .assert() + .success() + .stdout(file::is_svg().with_size( + Length::new(42.0, LengthUnit::Cm), + Length::new(43.0, LengthUnit::Cm), + )); +} + +#[cfg(system_deps_have_cairo_svg)] +#[test] +fn user_specified_width_and_height_px_output() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--format") + .arg("svg") + .arg("--width") + .arg("1920") + .arg("--height") + .arg("508mm") + .assert() + .success() + .stdout(file::is_svg().with_size( + Length::new(1920.0, LengthUnit::Px), + Length::new(1920.0, LengthUnit::Px), + )); +} + +#[cfg(system_deps_have_cairo_svg)] +#[test] +fn user_specified_width_and_height_a4() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--format") + .arg("svg") + .arg("--page-width") + .arg("210mm") + .arg("--page-height") + .arg("297mm") + .arg("--left") + .arg("1cm") + .arg("--top") + .arg("1cm") + .arg("--width") + .arg("190mm") + .arg("--height") + .arg("277mm") + .assert() + .success() + .stdout(file::is_svg().with_size( + Length::new(210.0, LengthUnit::Mm), + Length::new(297.0, LengthUnit::Mm), + )); +} + +#[test] +fn output_file_option() { + let output = { + let tempfile = Builder::new().suffix(".png").tempfile().unwrap(); + tempfile.path().to_path_buf() + }; + assert!(predicates::path::is_file().not().eval(&output)); + + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg(format!("--output={}", output.display())) + .assert() + .success() + .stdout(is_empty()); + + assert!(predicates::path::is_file().eval(&output)); + std::fs::remove_file(&output).unwrap(); +} + +#[test] +fn output_file_short_option() { + let output = { + let tempfile = Builder::new().suffix(".png").tempfile().unwrap(); + tempfile.path().to_path_buf() + }; + assert!(predicates::path::is_file().not().eval(&output)); + + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("-o") + .arg(format!("{}", output.display())) + .assert() + .success() + .stdout(is_empty()); + + assert!(predicates::path::is_file().eval(&output)); + std::fs::remove_file(&output).unwrap(); +} + +#[test] +fn overwrites_existing_output_file() { + let output = { + let tempfile = Builder::new().suffix(".png").tempfile().unwrap(); + tempfile.path().to_path_buf() + }; + assert!(predicates::path::is_file().not().eval(&output)); + + for _ in 0..2 { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg(format!("--output={}", output.display())) + .assert() + .success() + .stdout(is_empty()); + + assert!(predicates::path::is_file().eval(&output)); + } + + std::fs::remove_file(&output).unwrap(); +} + +#[test] +fn empty_input_yields_error() { + let starts_with = starts_with("Error reading SVG"); + let ends_with = ends_with("Input file is too short").trim(); + RsvgConvert::new() + .assert() + .failure() + .stderr(starts_with.and(ends_with)); +} + +#[test] +fn empty_svg_yields_error() { + RsvgConvert::new_with_input("tests/fixtures/empty.svg") + .assert() + .failure() + .stderr("The SVG stdin has no dimensions\n"); +} + +#[test] +fn multiple_input_files_not_allowed_for_png_output() { + let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); + RsvgConvert::new() + .arg(one) + .arg(two) + .assert() + .failure() + .stderr(contains( + "Multiple SVG files are only allowed for PDF and (E)PS output", + )); +} + +#[cfg(system_deps_have_cairo_ps)] +#[test] +fn multiple_input_files_accepted_for_eps_output() { + let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); + RsvgConvert::new() + .arg("--format=eps") + .arg(one) + .arg(two) + .assert() + .success() + .stdout(file::is_eps()); +} + +#[cfg(system_deps_have_cairo_ps)] +#[test] +fn multiple_input_files_accepted_for_ps_output() { + let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); + RsvgConvert::new() + .arg("--format=ps") + .arg(one) + .arg(two) + .assert() + .success() + .stdout(file::is_ps()); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn multiple_input_files_create_multi_page_pdf_output() { + let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); + let three = Path::new("tests/fixtures/example.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .arg(one) + .arg(two) + .arg(three) + .assert() + .success() + .stdout( + file::is_pdf() + .with_page_count(3) + .and(file::is_pdf().with_page_size(0, 150.0, 75.0)) + .and(file::is_pdf().with_page_size(1, 123.0, 123.0)) + .and(file::is_pdf().with_page_size(2, 75.0, 300.0)), + ); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn multiple_input_files_create_multi_page_pdf_output_fixed_size() { + let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); + let three = Path::new("tests/fixtures/example.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .arg("--page-width=8.5in") + .arg("--page-height=11in") + .arg("--width=7.5in") + .arg("--height=10in") + .arg("--left=0.5in") + .arg("--top=0.5in") + .arg("--keep-aspect-ratio") + .arg(one) + .arg(two) + .arg(three) + .assert() + .success() + .stdout( + file::is_pdf() + .with_page_count(3) + // https://www.wolframalpha.com/input/?i=convert+11+inches+to+desktop+publishing+points + .and(file::is_pdf().with_page_size(0, 612.0, 792.0)) + .and(file::is_pdf().with_page_size(1, 612.0, 792.0)) + .and(file::is_pdf().with_page_size(2, 612.0, 792.0)), + ); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn pdf_has_link() { + let input = Path::new("tests/fixtures/a-link.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .arg(input) + .assert() + .success() + .stdout(file::is_pdf().with_link("https://example.com")); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn pdf_has_link_inside_text() { + let input = Path::new("tests/fixtures/text-a-link.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .arg(input) + .assert() + .success() + .stdout( + file::is_pdf() + .with_link("https://example.com") + .and(file::is_pdf().with_link("https://another.example.com")), + ); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn pdf_has_text() { + let input = Path::new("tests/fixtures/hello-world.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .arg(input) + .assert() + .success() + .stdout( + file::is_pdf() + .with_text("Hello world!") + .and(file::is_pdf().with_text("Hello again!")), + ); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn env_source_data_epoch_controls_pdf_creation_date() { + let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + let date = 1581411039; // seconds since epoch + RsvgConvert::new() + .env("SOURCE_DATE_EPOCH", format!("{}", date)) + .arg("--format=pdf") + .arg(input) + .assert() + .success() + .stdout(file::is_pdf().with_creation_date(Utc.timestamp_opt(date, 0).unwrap())); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn env_source_data_epoch_no_digits() { + // intentionally not testing for the full error string here + let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + RsvgConvert::new() + .env("SOURCE_DATE_EPOCH", "foobar") + .arg("--format=pdf") + .arg(input) + .assert() + .failure() + .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH")); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn env_source_data_epoch_trailing_garbage() { + // intentionally not testing for the full error string here + let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .env("SOURCE_DATE_EPOCH", "1234556+") + .arg(input) + .assert() + .failure() + .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH")); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn env_source_data_epoch_empty() { + // intentionally not testing for the full error string here + let input = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .env("SOURCE_DATE_EPOCH", "") + .arg(input) + .assert() + .failure() + .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH")); +} + +#[test] +fn width_option() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--width=300") + .assert() + .success() + .stdout(file::is_png().with_size(300, 150)); +} + +#[test] +fn height_option() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--height=200") + .assert() + .success() + .stdout(file::is_png().with_size(400, 200)); +} + +#[test] +fn width_and_height_options() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--width=300") + .arg("--height=200") + .assert() + .success() + .stdout(file::is_png().with_size(300, 200)); +} + +#[test] +fn unsupported_unit_in_width_and_height() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--height=200ex") + .assert() + .failure() + .stderr(contains("supported units")); +} + +#[test] +fn invalid_length() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--page-width=foo") + .assert() + .failure() + .stderr(contains("can not be parsed as a length")); +} + +#[test] +fn zoom_factor() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--zoom=0.8") + .assert() + .success() + .stdout(file::is_png().with_size(160, 80)); +} + +#[test] +fn zoom_factor_and_larger_size() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--width=400") + .arg("--height=200") + .arg("--zoom=1.5") + .assert() + .success() + .stdout(file::is_png().with_size(300, 150)); +} + +#[test] +fn zoom_factor_and_smaller_size() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--width=400") + .arg("--height=200") + .arg("--zoom=3.5") + .assert() + .success() + .stdout(file::is_png().with_size(400, 200)); +} + +#[test] +fn x_zoom_option() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--x-zoom=2") + .assert() + .success() + .stdout(file::is_png().with_size(400, 100)); +} + +#[test] +fn x_short_option() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("-x") + .arg("2.0") + .assert() + .success() + .stdout(file::is_png().with_size(400, 100)); +} + +#[test] +fn y_zoom_option() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--y-zoom=2.0") + .assert() + .success() + .stdout(file::is_png().with_size(200, 200)); +} + +#[test] +fn y_short_option() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("-y") + .arg("2") + .assert() + .success() + .stdout(file::is_png().with_size(200, 200)); +} + +#[test] +fn huge_zoom_factor_yields_error() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--zoom=1000") + .assert() + .failure() + .stderr(starts_with( + "The resulting image would be larger than 32767 pixels", + )); +} + +#[test] +fn negative_zoom_factor_yields_error() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--zoom=-2") + .assert() + .failure() + .stderr(contains("Invalid zoom")); +} + +#[test] +fn invalid_zoom_factor_yields_error() { + RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg") + .arg("--zoom=foo") + .assert() + .failure() + .stderr(contains("invalid value")); +} + +#[test] +fn default_resolution_is_96dpi() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .assert() + .success() + .stdout(file::is_png().with_size(96, 384)); +} + +#[test] +fn x_resolution() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--dpi-x=300") + .assert() + .success() + .stdout(file::is_png().with_size(300, 384)); +} + +#[test] +fn x_resolution_short_option() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("-d") + .arg("45") + .assert() + .success() + .stdout(file::is_png().with_size(45, 384)); +} + +#[test] +fn y_resolution() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--dpi-y=300") + .assert() + .success() + .stdout(file::is_png().with_size(96, 1200)); +} + +#[test] +fn y_resolution_short_option() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("-p") + .arg("45") + .assert() + .success() + .stdout(file::is_png().with_size(96, 180)); +} + +#[test] +fn x_and_y_resolution() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--dpi-x=300") + .arg("--dpi-y=150") + .assert() + .success() + .stdout(file::is_png().with_size(300, 600)); +} + +#[test] +fn zero_resolution_is_invalid() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--dpi-x=0") + .arg("--dpi-y=0") + .assert() + .failure() + .stderr(contains("Invalid resolution")); +} + +#[test] +fn negative_resolution_is_invalid() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--dpi-x=-100") + .arg("--dpi-y=-100") + .assert() + .failure() + .stderr(contains("Invalid resolution")); +} + +#[test] +fn zero_offset_png() { + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--page-width=640") + .arg("--page-height=480") + .arg("--width=200") + .arg("--height=100") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/zero-offset-png.png")); +} + +#[test] +fn offset_png() { + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--page-width=640") + .arg("--page-height=480") + .arg("--width=200") + .arg("--height=100") + .arg("--left=100") + .arg("--top=50") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/offset-png.png")); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn unscaled_pdf_size() { + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--format=pdf") + .assert() + .success() + .stdout(file::is_pdf().with_page_size(0, 72.0, 72.0)); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn pdf_size_width_height() { + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--format=pdf") + .arg("--width=2in") + .arg("--height=3in") + .assert() + .success() + .stdout(file::is_pdf().with_page_size(0, 144.0, 216.0)); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn pdf_size_width_height_proportional() { + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--format=pdf") + .arg("--width=2in") + .arg("--height=3in") + .arg("--keep-aspect-ratio") + .assert() + .success() + .stdout(file::is_pdf().with_page_size(0, 144.0, 144.0)); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn pdf_page_size() { + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--format=pdf") + .arg("--page-width=210mm") + .arg("--page-height=297mm") + .assert() + .success() + .stdout(file::is_pdf().with_page_size(0, 210.0 / 25.4 * 72.0, 297.0 / 25.4 * 72.0)); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn multiple_input_files_create_multi_page_pdf_size_override() { + let one = Path::new("tests/fixtures/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/sub-rect-no-unit.svg"); + let three = Path::new("tests/fixtures/example.svg"); + RsvgConvert::new() + .arg("--format=pdf") + .arg("--width=300pt") + .arg("--height=200pt") + .arg(one) + .arg(two) + .arg(three) + .assert() + .success() + .stdout( + file::is_pdf() + .with_page_count(3) + .and(file::is_pdf().with_page_size(0, 300.0, 200.0)) + .and(file::is_pdf().with_page_size(1, 300.0, 200.0)) + .and(file::is_pdf().with_page_size(2, 300.0, 200.0)), + ); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn missing_page_size_yields_error() { + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--format=pdf") + .arg("--page-width=210mm") + .assert() + .failure() + .stderr(contains("both").and(contains("options"))); + + RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg") + .arg("--format=pdf") + .arg("--page-height=297mm") + .assert() + .failure() + .stderr(contains("both").and(contains("options"))); +} + +#[test] +fn does_not_clip_partial_coverage_pixels() { + RsvgConvert::new_with_input("tests/fixtures/bug677-partial-pixel.svg") + .assert() + .success() + .stdout(file::is_png().with_size(2, 2)); +} + +#[test] +fn background_color_option_with_valid_color() { + RsvgConvert::accepts_arg("--background-color=LimeGreen"); +} + +#[test] +fn background_color_option_none() { + RsvgConvert::accepts_arg("--background-color=None"); +} + +#[test] +fn background_color_short_option() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("-b") + .arg("#aabbcc") + .assert() + .success(); +} + +#[test] +fn background_color_option_invalid_color_yields_error() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--background-color=foobar") + .assert() + .failure() + .stderr(contains("Invalid").and(contains("color"))); +} + +#[test] +fn background_color_is_rendered() { + RsvgConvert::new_with_input("tests/fixtures/gimp-wilber.svg") + .arg("--background-color=purple") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/gimp-wilber-ref.png")); +} + +#[test] +fn stylesheet_option() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--stylesheet=tests/fixtures/empty.svg") + .assert() + .success(); +} + +#[test] +fn stylesheet_short_option() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("-s") + .arg("tests/fixtures/empty.svg") + .assert() + .success(); +} + +#[test] +fn stylesheet_option_error() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--stylesheet=foobar") + .assert() + .failure() + .stderr(starts_with("Error reading stylesheet")); +} + +#[test] +fn export_id_option() { + RsvgConvert::new_with_input("tests/fixtures/geometry-element.svg") + .arg("--export-id=foo") + .assert() + .success() + .stdout(file::is_png().with_size(40, 50)); +} + +#[test] +fn export_id_with_zero_stroke_width() { + // https://gitlab.gnome.org/GNOME/librsvg/-/issues/601 + // + // This tests a bug that manifested itself easily with the --export-id option, but it + // is not a bug with the option itself. An object with stroke_width=0 was causing + // an extra point at the origin to be put in the bounding box, so the final image + // spanned the origin to the actual visible bounds of the rendered object. + // + // We can probably test this more cleanly once we have a render tree. + RsvgConvert::new_with_input("tests/fixtures/bug601-zero-stroke-width.svg") + .arg("--export-id=foo") + .assert() + .success() + .stdout( + file::is_png() + .with_contents("tests/fixtures/bug601-zero-stroke-width-render-only-foo.png"), + ); +} + +#[test] +fn export_id_short_option() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("-i") + .arg("two") + .assert() + .success() + .stdout(file::is_png().with_size(100, 200)); +} + +#[test] +fn export_id_with_hash_prefix() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("-i") + .arg("#two") + .assert() + .success() + .stdout(file::is_png().with_size(100, 200)); +} + +#[test] +fn export_id_option_error() { + RsvgConvert::new_with_input("tests/fixtures/dpi.svg") + .arg("--export-id=foobar") + .assert() + .failure() + .stderr(starts_with("File stdin does not have an object with id \"")); +} + +#[test] +fn unlimited_option() { + RsvgConvert::accepts_arg("--unlimited"); +} + +#[test] +fn unlimited_short_option() { + RsvgConvert::accepts_arg("-u"); +} + +#[test] +fn keep_aspect_ratio_option() { + let input = Path::new("tests/fixtures/dpi.svg"); + RsvgConvert::new_with_input(input) + .arg("--width=500") + .arg("--height=1000") + .assert() + .success() + .stdout(file::is_png().with_size(500, 1000)); + RsvgConvert::new_with_input(input) + .arg("--width=500") + .arg("--height=1000") + .arg("--keep-aspect-ratio") + .assert() + .success() + .stdout(file::is_png().with_size(250, 1000)); +} + +#[test] +fn keep_aspect_ratio_short_option() { + let input = Path::new("tests/fixtures/dpi.svg"); + RsvgConvert::new_with_input(input) + .arg("--width=1000") + .arg("--height=500") + .assert() + .success() + .stdout(file::is_png().with_size(1000, 500)); + RsvgConvert::new_with_input(input) + .arg("--width=1000") + .arg("--height=500") + .arg("-a") + .assert() + .success() + .stdout(file::is_png().with_size(125, 500)); +} + +#[test] +fn overflowing_size_is_detected() { + RsvgConvert::new_with_input("tests/fixtures/bug591-vbox-overflow.svg") + .assert() + .failure() + .stderr(starts_with( + "The resulting image would be larger than 32767 pixels", + )); +} + +#[test] +fn accept_language_given() { + RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") + .arg("--accept-language=es-MX") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/accept-language-es.png")); + + RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") + .arg("--accept-language=de") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/accept-language-de.png")); +} + +#[test] +fn accept_language_fallback() { + RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") + .arg("--accept-language=fr") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/accept-language-fallback.png")); +} + +#[test] +fn accept_language_invalid_tag() { + // underscores are not valid in BCP47 language tags + RsvgConvert::new_with_input("tests/fixtures/accept-language.svg") + .arg("--accept-language=foo_bar") + .assert() + .failure() + .stderr(contains("invalid language tag")); +} + +#[test] +fn keep_image_data_option() { + RsvgConvert::accepts_arg("--keep-image-data"); +} + +#[test] +fn no_keep_image_data_option() { + RsvgConvert::accepts_arg("--no-keep-image-data"); +} + +fn is_version_output() -> AndPredicate, str> { + starts_with("rsvg-convert version ") + .and(predicates::str::ends_with(env!("CARGO_PKG_VERSION")).trim()) +} + +#[test] +fn version_option() { + RsvgConvert::option_yields_output("--version", is_version_output()) +} + +#[test] +fn version_short_option() { + RsvgConvert::option_yields_output("-v", is_version_output()) +} + +fn is_usage_output() -> OrPredicate { + contains("Usage:").or(contains("USAGE:")) +} + +#[test] +fn help_option() { + RsvgConvert::option_yields_output("--help", is_usage_output()) +} + +#[test] +fn help_short_option() { + RsvgConvert::option_yields_output("-?", is_usage_output()) +} -- cgit v1.2.1 From c69d8919faf558deb00272efe6b123461e5a82c0 Mon Sep 17 00:00:00 2001 From: Federico Mena Quintero Date: Wed, 3 May 2023 17:40:53 -0600 Subject: Makefile.am: cp the Rust artifacts, don't just mv them from the build directory I wonder if this is confusing "make install"; it's rebuilding rsvg-convert. Part-of: --- Makefile.am | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index e6176e37..01e68f6d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -211,7 +211,7 @@ librsvg_c_api.la: $(librsvg_c_api_la_OBJECTS) $(LIBRSVG_SRC) PKG_CONFIG='$(PKG_CONFIG)' \ CARGO_TARGET_DIR=$(CARGO_TARGET_DIR) \ $(CARGO) --locked build $(CARGO_VERBOSE) $(CARGO_TARGET_ARGS) $(CARGO_RELEASE_ARGS) --package librsvg-c \ - && cd $(LIBRSVG_BUILD_DIR) && $(LINK) $< && mv $(RUST_LIB) .libs/librsvg_c_api.a + && cd $(LIBRSVG_BUILD_DIR) && $(LINK) $< && cp $(RUST_LIB) .libs/librsvg_c_api.a librsvg_@RSVG_API_MAJOR_VERSION@_la_CPPFLAGS = $(AM_CPPFLAGS) @@ -259,7 +259,7 @@ $(RSVG_CONVERT_BIN): $(RSVG_CONVERT_SRC) | librsvg_c_api.la $(CARGO) --locked build $(CARGO_VERBOSE) $(CARGO_TARGET_ARGS) $(CARGO_RELEASE_ARGS) --package rsvg_convert rsvg-convert$(EXEEXT): $(RSVG_CONVERT_BIN) - cd $(LIBRSVG_BUILD_DIR) && mv $(RSVG_CONVERT_BIN) rsvg-convert$(EXEEXT) + cd $(LIBRSVG_BUILD_DIR) && cp $(RSVG_CONVERT_BIN) rsvg-convert$(EXEEXT) rsvg-convert.1: rsvg-convert.rst if HAVE_RST2MAN -- cgit v1.2.1