- Commit
- bfa14940aed0da732da212d17e8451e0a337deca
- Parent
- 1e444b4fa16b3b2dbdb6da8d2e42495222c262c1
- Author
- Pablo <pablo-pie@riseup.net>
- Date
Merge branch 'no-tikztosvg'
Custum build of stapix for tikz.pablopie.xyz
Merge branch 'no-tikztosvg'
16 files changed, 1418 insertions, 212 deletions
| Status | Name | Changes | Insertions | Deletions |
| Added | .gitmodules | 1 file changed | 3 | 0 |
| Modified | Cargo.lock | 2 files changed | 159 | 22 |
| Modified | Cargo.toml | 2 files changed | 1 | 2 |
| Modified | README.md | 2 files changed | 5 | 5 |
| Added | mupdf-sys/.gitignore | 1 file changed | 1 | 0 |
| Added | mupdf-sys/Cargo.lock | 1 file changed | 235 | 0 |
| Added | mupdf-sys/Cargo.toml | 1 file changed | 194 | 0 |
| Added | mupdf-sys/build.rs | 1 file changed | 385 | 0 |
| Added | mupdf-sys/mupdf | 1 file changed | 1 | 0 |
| Added | mupdf-sys/src/lib.rs | 1 file changed | 17 | 0 |
| Added | mupdf-sys/src/wrapper.c | 1 file changed | 89 | 0 |
| Modified | src/image.rs | 2 files changed | 2 | 2 |
| Modified | src/log.rs | 2 files changed | 2 | 2 |
| Modified | src/main.rs | 2 files changed | 212 | 178 |
| Added | src/mupdf.rs | 1 file changed | 111 | 0 |
| Modified | src/outro.html | 2 files changed | 1 | 1 |
diff --git /dev/null b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mupdf-sys/mupdf"] + path = mupdf-sys/mupdf + url = https://github.com/ArtifexSoftware/mupdf.git
diff --git a/Cargo.lock b/Cargo.lock @@ -3,6 +3,39 @@ version = 4 [[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -13,6 +46,38 @@ dependencies = [ ] [[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -31,12 +96,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] name = "indexmap" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -47,6 +106,15 @@ dependencies = [ ] [[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -68,6 +136,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] name = "libwebp-sys" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -79,13 +157,35 @@ dependencies = [ ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mupdf-sys" +version = "1.0.0" dependencies = [ - "hermit-abi", - "libc", + "bindgen", + "cc", + "pkg-config", + "regex", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] @@ -113,6 +213,41 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -152,6 +287,12 @@ dependencies = [ ] [[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -169,23 +310,13 @@ dependencies = [ ] [[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] name = "tikz_gallery_generator" version = "1.0.0" dependencies = [ "libwebp-sys", - "num_cpus", + "mupdf-sys", "serde", "serde_yaml", - "threadpool", "zune-core", "zune-jpeg", "zune-png", @@ -204,6 +335,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] name = "zune-core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml @@ -13,8 +13,7 @@ libwebp-sys = "0.14" zune-core = "0.5" zune-jpeg = "0.5" zune-png = "0.5" -threadpool = "1.8.1" -num_cpus = "1.16.0" +mupdf-sys = { path = "./mupdf-sys/" } [profile.release] debug = true
diff --git a/README.md b/README.md @@ -8,7 +8,7 @@ Custum build of [`stapix`](https://git.pablopie.xyz/stapix) for Run: ```console -$ tikz_gallery_generator config.yml [--full-build] +$ tikz_gallery_generator config.yml [-B] ``` The configuration file `config.yml` should consist of a list of struct entries @@ -58,8 +58,8 @@ should not be the same!** See ### Options -* **`--full-build`:** Disables incremental builds. Re-renders all pages and - thumbnails. +* **`-B`:** Disables incremental builds. Re-renders all pages and + thumbnails. ## Installation @@ -70,9 +70,9 @@ as in: $ cargo install --path . ``` -## External Dependencies +## Runtime Dependencies -* [tikztosvg](https://www.ctan.org/pkg/tikztosvg) +* [LuaLaTeX](https://www.luatex.org/) ## License
diff --git /dev/null b/mupdf-sys/.gitignore @@ -0,0 +1 @@ +/target
diff --git /dev/null b/mupdf-sys/Cargo.lock @@ -0,0 +1,235 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mupdf-sys" +version = "1.0.0" +dependencies = [ + "bindgen", + "cc", + "pkg-config", + "regex", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
diff --git /dev/null b/mupdf-sys/Cargo.toml @@ -0,0 +1,194 @@ +[package] +name = "mupdf-sys" +version = "1.0.0" +edition = "2021" +include = [ + "COPYING*", + "LICENSE*", + "AUTHORS*", + + "*.rs", + "src/wrapper.c", + + "mupdf/resources/hyphen/hyph-all.zip", + "mupdf/resources/hyphen/hyph-std.zip", + "mupdf/generated/resources/hyphen/*.c", + + "mupdf/resources/fonts/urw/*.cff", + "mupdf/generated/resources/fonts/urw/*.c", + + "mupdf/Make*", + "mupdf/platform/win32/*", + + "mupdf/scripts/*.c", + "mupdf/source/helpers/pkcs7/*.c", + "mupdf/include/mupdf/*.h", + "mupdf/include/mupdf/helpers/*.h", + + "mupdf/source/fitz/*.h", + "mupdf/source/fitz/*.c", + "mupdf/source/fitz/*.cpp", + "mupdf/source/fitz/icc/*.icc.h", + "mupdf/include/mupdf/fitz/*.h", + + "mupdf/source/pdf/*.c", + "mupdf/source/pdf/*.h", + "mupdf/source/pdf/cmaps/*.h", + "mupdf/source/pdf/js/*.h", + "mupdf/include/mupdf/pdf/*.h", + + "mupdf/source/xps/*.c", + "mupdf/source/xps/*.h", + + "mupdf/source/svg/*.c", + "mupdf/source/svg/*.h", + + "mupdf/source/html/*.c", + "mupdf/source/html/*.h", + + "mupdf/source/reflow/*.c", + + "mupdf/source/cbz/*.c", + + "mupdf/thirdparty/brotli/c/include/brotli/*.h", + "mupdf/thirdparty/brotli/c/common/*.c", + "mupdf/thirdparty/brotli/c/common/*.h", + "mupdf/thirdparty/brotli/c/dec/*.c", + "mupdf/thirdparty/brotli/c/dec/*.h", + "mupdf/thirdparty/brotli/c/enc/*.c", + "mupdf/thirdparty/brotli/c/enc/*.h", + + "mupdf/thirdparty/freetype/*.c", + "mupdf/thirdparty/freetype/src/base/*.c", + "mupdf/thirdparty/freetype/src/base/*.h", + "mupdf/thirdparty/freetype/src/cff/*.c", + "mupdf/thirdparty/freetype/src/cff/*.h", + "mupdf/thirdparty/freetype/src/cid/*.c", + "mupdf/thirdparty/freetype/src/cid/*.h", + "mupdf/thirdparty/freetype/src/psaux/*.c", + "mupdf/thirdparty/freetype/src/psaux/*.h", + "mupdf/thirdparty/freetype/src/pshinter/*.c", + "mupdf/thirdparty/freetype/src/pshinter/*.h", + "mupdf/thirdparty/freetype/src/psnames/*.c", + "mupdf/thirdparty/freetype/src/psnames/*.h", + "mupdf/thirdparty/freetype/src/raster/*.c", + "mupdf/thirdparty/freetype/src/raster/*.h", + "mupdf/thirdparty/freetype/src/sfnt/*.c", + "mupdf/thirdparty/freetype/src/sfnt/*.h", + "mupdf/thirdparty/freetype/src/smooth/*.c", + "mupdf/thirdparty/freetype/src/smooth/*.h", + "mupdf/thirdparty/freetype/src/truetype/*.c", + "mupdf/thirdparty/freetype/src/truetype/*.h", + "mupdf/thirdparty/freetype/src/type1/*.c", + "mupdf/thirdparty/freetype/src/type1/*.h", + "mupdf/thirdparty/freetype/include", + "mupdf/scripts/freetype/*.h", + + "mupdf/thirdparty/gumbo-parser/src/*.c", + "mupdf/thirdparty/gumbo-parser/src/*.h", + "mupdf/thirdparty/gumbo-parser/visualc/include/*.h", + + "mupdf/thirdparty/harfbuzz/src/*.cc", + "mupdf/thirdparty/harfbuzz/src/*.h", + "mupdf/thirdparty/harfbuzz/src/*.hh", + "mupdf/thirdparty/harfbuzz/src/graph/gsubgpos-context.cc", + "mupdf/thirdparty/harfbuzz/src/graph/*.hh", + "mupdf/thirdparty/harfbuzz/src/OT", + + "mupdf/thirdparty/libjpeg/*.c", + "mupdf/thirdparty/libjpeg/*.h", + "mupdf/scripts/libjpeg/*.h", + + "mupdf/thirdparty/lcms2/src/*.c", + "mupdf/thirdparty/lcms2/src/*.h", + "mupdf/thirdparty/lcms2/include/*.h", + + "mupdf/thirdparty/mujs/*.c", + "mupdf/thirdparty/mujs/*.h", + + "mupdf/thirdparty/zlib/*.c", + "mupdf/thirdparty/zlib/*.h", + + "mupdf/thirdparty/jbig2dec/*.c", + "mupdf/thirdparty/jbig2dec/*.h", + + "mupdf/thirdparty/openjpeg/src/lib/openjp2/*.c", + "mupdf/thirdparty/openjpeg/src/lib/openjp2/*.h", + + "mupdf/thirdparty/leptonica/src/*.c", + "mupdf/thirdparty/leptonica/src/*.h", + + "mupdf/thirdparty/tesseract/src/*.cpp", + "mupdf/thirdparty/tesseract/src/api/*.cpp", + "mupdf/thirdparty/tesseract/src/api/*.h", + "mupdf/thirdparty/tesseract/src/arch/*.cpp", + "mupdf/thirdparty/tesseract/src/arch/*.h", + "mupdf/thirdparty/tesseract/src/ccmain/*.cpp", + "mupdf/thirdparty/tesseract/src/ccmain/*.h", + "mupdf/thirdparty/tesseract/src/ccstruct/*.cpp", + "mupdf/thirdparty/tesseract/src/ccstruct/*.h", + "mupdf/thirdparty/tesseract/src/ccutil/*.cpp", + "mupdf/thirdparty/tesseract/src/ccutil/*.h", + "mupdf/thirdparty/tesseract/src/classify/*.cpp", + "mupdf/thirdparty/tesseract/src/classify/*.h", + "mupdf/thirdparty/tesseract/src/dict/*.cpp", + "mupdf/thirdparty/tesseract/src/dict/*.h", + "mupdf/thirdparty/tesseract/src/lstm/*.cpp", + "mupdf/thirdparty/tesseract/src/lstm/*.h", + "mupdf/thirdparty/tesseract/src/textord/*.cpp", + "mupdf/thirdparty/tesseract/src/textord/*.h", + "mupdf/thirdparty/tesseract/src/viewer/*.cpp", + "mupdf/thirdparty/tesseract/src/viewer/*.h", + "mupdf/thirdparty/tesseract/src/wordrec/*.cpp", + "mupdf/thirdparty/tesseract/src/wordrec/*.h", + "mupdf/thirdparty/tesseract/src/cutil/*.cpp", + "mupdf/thirdparty/tesseract/src/cutil/*.h", + "mupdf/thirdparty/tesseract/include/tesseract/*.h", + "mupdf/scripts/tesseract/*.h", + "mupdf/scripts/tesseract/tesseract/*.h", + + "mupdf/thirdparty/extract/src/*.c", + "mupdf/thirdparty/extract/src/*.h", + "mupdf/thirdparty/extract/include/extract/*.h", + "mupdf/thirdparty/extract/src/template.docx", + "mupdf/thirdparty/extract/src/template.odt", + "mupdf/thirdparty/extract/src/docx_template_build.py", + + "mupdf/thirdparty/zxing-cpp/core/src/*.cpp", + "mupdf/thirdparty/zxing-cpp/core/src/*.h", + "mupdf/thirdparty/zxing-cpp/core/src/aztec/*.cpp", + "mupdf/thirdparty/zxing-cpp/core/src/aztec/*.h", + "mupdf/thirdparty/zxing-cpp/core/src/datamatrix/*.cpp", + "mupdf/thirdparty/zxing-cpp/core/src/datamatrix/*.h", + "mupdf/thirdparty/zxing-cpp/core/src/maxicode/*.cpp", + "mupdf/thirdparty/zxing-cpp/core/src/maxicode/*.h", + "mupdf/thirdparty/zxing-cpp/core/src/oned/*.cpp", + "mupdf/thirdparty/zxing-cpp/core/src/oned/*.h", + "mupdf/thirdparty/zxing-cpp/core/src/pdf417/*.cpp", + "mupdf/thirdparty/zxing-cpp/core/src/pdf417/*.h", + "mupdf/thirdparty/zxing-cpp/core/src/qrcode/*.cpp", + "mupdf/thirdparty/zxing-cpp/core/src/qrcode/*.h", + "mupdf/scripts/zxing-cpp/*.cpp", + "mupdf/scripts/zxing-cpp/*.h", + + "mupdf/thirdparty/zxing-cpp/core/src/libzueci/*.c", + "mupdf/thirdparty/zxing-cpp/core/src/libzueci/*.h", + + "mupdf/thirdparty/zint/backend/*.c", + "mupdf/thirdparty/zint/backend/*.h", + "mupdf/thirdparty/zint/backend/fonts/*.h", +] +description = "Custom Rust FFI binding to MuPDF" +keywords = ["pdf", "mupdf"] +license = "AGPL-3.0" +links="mupdf-wrapper" + +[features] + +[build-dependencies] +bindgen = { version = "0.72", default-features = false, features = ["runtime"] } +cc = "1.0.50" +pkg-config = "0.3" +regex = "1.11" + +[dependencies]
diff --git /dev/null b/mupdf-sys/build.rs @@ -0,0 +1,385 @@ +use std::{ + env::{self, current_dir}, + error::Error, + ffi::{OsStr, OsString}, + io::ErrorKind, + path::{Path, PathBuf}, + process::{Command, exit}, + fs, + result, + thread, +}; + +const SUPPORTED_TARGETS: &[&str] = &["linux", "openbsd", "netbsd", "macos"]; +const BINDINGS_PATH: &str = "src/wrapper.c"; + +type Result<T> = result::Result<T, Box<dyn Error>>; + +fn main() { + if let Err(e) = run() { + eprintln!("\n{e}"); + exit(1); + } +} + +fn run() -> Result<()> { + if fs::read_dir("mupdf").map_or(true, |d| d.count() == 0) { + Err( + "The `mupdf` directory is empty, did you forget to pull the submodules?\n\ + Try `git submodule update --init --recursive`", + )? + } + + let target = Target::from_cargo().map_err(|e| { + format!( + "Unable to detect target: {e}\n\ + Cargo is required to build mupdf" + ) + })?; + + if !SUPPORTED_TARGETS.contains(&target.os.as_str()) { + Err(format!("Target {:?} is unsupported!", target.os))? + } + + let src_dir = current_dir().unwrap().join("mupdf"); + let out_dir = + PathBuf::from(env::var_os("OUT_DIR").ok_or("Missing OUT_DIR environment variable")?); + + let docs = env::var_os("DOCS_RS").is_some(); + if !docs { + let build_dir = out_dir.join("build"); + let build_dir = build_dir.to_str().ok_or_else(|| { + format!("Build dir path is required to be valid UTF-8, got {build_dir:?}") + })?; + + if let Err(e) = fs::remove_dir_all(build_dir) { + if e.kind() != ErrorKind::NotFound { + println!("cargo:warning=Unable to clear {build_dir:?}. This may lead to flaky builds that might not incorporate configurations changes: {e}"); + } + } + + copy_recursive(&src_dir, build_dir.as_ref(), &[".git".as_ref()])?; + + // ======================================================================== + let mut make = Make::default(); + make.define("FZ_ENABLE_PDF", "1"); + make.define("FZ_ENABLE_SVG", "1"); + make.define("FZ_ENABLE_CBZ", "0"); + make.define("FZ_ENABLE_IMG", "0"); + make.define("FZ_ENABLE_HTML", "0"); + make.define("FZ_ENABLE_EPUB", "0"); + make.define("FZ_ENABLE_JS", "0"); + + // NOTE: see https://github.com/ArtifexSoftware/mupdf/blob/master/source/fitz/noto.c + make.define("TOFU", "1"); + make.define("TOFU_CJK", "1"); + make.define("TOFU_NOTO", "1"); + make.define("TOFU_SYMBOL", "1"); + make.define("TOFU_EMOJI", "1"); + make.define("TOFU_SIL", "1"); + + make.build(&target, build_dir)?; + + // ======================================================================== + build_wrapper() + .map_err(|e| format!("Unable to compile mupdf wrapper:\n {e}"))?; + } + + generate_bindings(&out_dir.join("bindings.rs")) + .map_err(|e| format!("Unable to generate mupdf bindings using bindgen:\n {e}"))?; + + Ok(()) +} + +fn copy_recursive(src: &Path, dst: &Path, ignore: &[&OsStr]) -> Result<()> { + if let Err(e) = fs::create_dir(dst) { + if e.kind() != ErrorKind::AlreadyExists { + Err(format!("Unable to create {dst:?}: {e}"))?; + } + } + + for entry in fs::read_dir(src)? { + let entry = entry?; + if ignore.contains(&&*entry.file_name()) { + continue; + } + + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + let file_type = entry.file_type()?; + + if file_type.is_symlink() { + let link = fs::read_link(&src_path) + .map_err(|e| format!("Couldn't read symlink {src_path:?}: {e}"))?; + let err = std::os::unix::fs::symlink(&link, &dst_path); + + match err { + Ok(_) => continue, + Err(e) => println!( + "cargo:warning=Couldn't create symlink {dst_path:?} pointing to {link:?}. This might increase the size of your target folder: {e}" + ), + } + } + + if file_type.is_file() || fs::metadata(&src_path)?.is_file() { + fs::copy(&src_path, &dst_path) + .map_err(|e| format!("Couldn't copy {src_path:?} to {dst_path:?}: {e}"))?; + } else { + copy_recursive(&src_path, &dst_path, ignore)?; + } + } + Ok(()) +} + +fn build_wrapper() -> Result<()> { + let mut build = cc::Build::new(); + + build + .file(BINDINGS_PATH) + .include("mupdf/include") + .extra_warnings(true) + .flag("-Wno-clobbered") // NOTE: remove stupid warnings on src/wrapper.c + .debug(true) + .try_compile("mupdf-wrapper")?; + + Ok(()) +} + +fn generate_bindings(path: &Path) -> Result<()> { + let mut builder = bindgen::builder(); + + builder = builder + .clang_arg("-Imupdf/include") + .header(BINDINGS_PATH); + + builder = builder + .allowlist_recursively(false) + .allowlist_type("wchar_t") + .allowlist_type("FILE") + .opaque_type("FILE") + .allowlist_item("max_align_t") + .opaque_type("max_align_t"); + + builder = builder + .allowlist_item("fz_.*") + .allowlist_item("FZ_.*") + .allowlist_item("pdf_.*") + .allowlist_item("PDF_.*") + .allowlist_type("cmap_splay") + .allowlist_item("ucdn_.*") + .allowlist_item("UCDN_.*") + .allowlist_item("Memento_.*") + .allowlist_item("mupdf_.*"); + + // remove va_list functions as for all of these versions using ... exist + builder = builder + .blocklist_function("Memento_vasprintf") // Memento_asprintf + .blocklist_function("fz_vthrow") // fz_throw + .blocklist_function("fz_vwarn") // fz_warn + .blocklist_function("fz_vlog_error_printf") // fz_log_error_printf + .blocklist_function("fz_append_vprintf") // fz_append_printf + .blocklist_function("fz_write_vprintf") // fz_write_printf + .blocklist_function("fz_vsnprintf") // fz_snprintf + .blocklist_function("fz_format_string"); // mupdf_format_string + + // TODO: make "FZ_VERSION.*" private + // build config + builder = builder + .blocklist_var("FZ_ENABLE_.*") + .blocklist_var("FZ_PLOTTERS_.*"); + + // internal implementation details, considered private + builder = builder + .blocklist_item("fz_jmp_buf") + .blocklist_function("fz_var_imp") + .blocklist_function("fz_push_try") + .blocklist_function("fz_do_.*") + .blocklist_var("FZ_JMPBUF_ALIGN") + .blocklist_type("fz_error_stack_slot") + .blocklist_type("fz_error_context") + .blocklist_type("fz_warn_context") + .blocklist_type("fz_aa_context") + .blocklist_type("fz_activity_.*") + .blocklist_function("fz_register_activity_logger") + .opaque_type("fz_context") + .blocklist_type("fz_new_context_imp") + .blocklist_type("fz_lock") + .blocklist_type("fz_unlock"); + + builder = builder + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())); + + builder + .prepend_enum_name(false) + .use_core() + .generate()? + .write_to_file(path)?; + + Ok(()) +} + +#[derive(Default)] +struct Make { + build: cc::Build, + make_flags: Vec<OsString>, +} + +impl Make { + pub fn define(&mut self, var: &str, val: &str) { + self.build.define(var, val); + } + + fn make_var(&mut self, var: &str, val: impl AsRef<OsStr>) { + let mut flag = OsString::from(var); + flag.push("="); + flag.push(val); + self.make_flags.push(flag); + } + + fn make_bool(&mut self, var: &str, val: bool) { + self.make_var(var, if val { "yes" } else { "no" }); + } + + fn cpu_flags( + &mut self, + target: &Target, + feature: &str, + flag: &str, + make_flag: &str, + define: Option<&str>, + ) { + let contains = target.cpu_features.iter().any(|f| f == feature) + && self.build.is_flag_supported(flag).unwrap_or(true); + if contains { + self.build.flag(flag); + self.make_bool(make_flag, true); + } + + if let Some(define) = define { + self.define(define, if contains { "1" } else { "0" }); + } + } + + pub fn build(mut self, target: &Target, build_dir: &str) -> Result<()> { + self.make_var( + "build", + if target.small_profile() { + "small" + } else if target.debug_profile() { + "debug" + } else { + "release" + }, + ); + + self.make_var("OUT", build_dir); + + self.make_bool("HAVE_X11", false); + self.make_bool("HAVE_GLUT", false); + self.make_bool("HAVE_CURL", false); + + self.make_bool("verbose", true); + + // ======================================================================== + self.make_bool("USE_TESSERACT", false); + self.make_bool("USE_ZXINGCPP", false); + self.make_bool("USE_LIBARCHIVE", false); + + // ======================================================================== + self.cpu_flags( + target, + "sse4.1", + "-msse4.1", + "HAVE_SSE4_1", + Some("ARCH_HAS_SSE"), + ); + self.cpu_flags(target, "avx", "-mavx", "HAVE_AVX", None); + self.cpu_flags(target, "avx2", "-mavx2", "HAVE_AVX2", None); + self.cpu_flags(target, "fma", "-mfma", "HAVE_FMA", None); + + // NOTE: arm + self.cpu_flags( + target, + "neon", + "-mfpu=neon", + "HAVE_NEON", + Some("ARCH_HAS_NEON"), + ); + // ======================================================================== + if let Ok(n) = thread::available_parallelism() { + self.make_flags.push(format!("-j{n}").into()); + } + + self.build.warnings(false); + + let compiler = self.build.get_compiler(); + self.make_var("CC", compiler.path()); + self.make_var("XCFLAGS", compiler.cflags_env()); + + self.build.cpp(true); + let compiler = self.build.get_compiler(); + self.make_var("CXX", compiler.path()); + self.make_var("XCXXFLAGS", compiler.cflags_env()); + + let make = if cfg!(any( + target_os = "freebsd", + target_os = "openbsd", + target_os = "netbsd" + )) { + "gmake" + } else { + "make" + }; + + let status = Command::new(make) + .arg("libs") + .args(&self.make_flags) + .current_dir(build_dir) + .status() + .map_err(|e| format!("Failed to call {make}: {e}"))?; + if !status.success() { + Err(match status.code() { + Some(code) => format!("{make} invocation failed with status {code}"), + None => format!("{make} invocation failed"), + })?; + } + + println!("cargo:rustc-link-search=native={build_dir}"); + println!("cargo:rustc-link-lib=static=mupdf"); + println!("cargo:rustc-link-lib=static=mupdf-third"); + + Ok(()) + } +} + +struct Target { + debug: bool, + opt_level: String, + os: String, + + cpu_features: Vec<String>, +} + +impl Target { + fn from_cargo() -> Result<Self> { + Ok(Self { + debug: env::var_os("DEBUG").is_some_and(|s| s != "0" && s != "false"), + opt_level: env::var("OPT_LEVEL")?, + os: env::var("CARGO_CFG_TARGET_OS")?, + + cpu_features: env::var("CARGO_CFG_TARGET_FEATURE")? + .split(',') + .map(str::to_owned) + .collect(), + }) + } + + fn small_profile(&self) -> bool { + !self.debug && matches!(&*self.opt_level, "s" | "z") + } + + fn debug_profile(&self) -> bool { + self.debug && !matches!(&*self.opt_level, "2" | "3") + } +}
diff --git /dev/null b/mupdf-sys/mupdf @@ -0,0 +1 @@ +Subproject commit 3d40818e511eaf8c49b92d8d3d4dc05fe76ab0f0
diff --git /dev/null b/mupdf-sys/src/lib.rs @@ -0,0 +1,17 @@ +#![no_std] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(clippy::all)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +#[inline] +pub unsafe fn fz_new_context( + alloc: *const fz_alloc_context, + locks: *const fz_locks_context, + max_store: usize, +) -> *mut fz_context { + let version = FZ_VERSION.as_ptr() as *const i8; + fz_new_context_imp(alloc, locks, max_store, version) +}
diff --git /dev/null b/mupdf-sys/src/wrapper.c @@ -0,0 +1,89 @@ +#include <stdlib.h> +#include <stdint.h> + +#include <mupdf/fitz.h> +#include <mupdf/pdf.h> + +typedef struct { + const char* err_msg; + enum fz_error_type type; +} mupdf_result_t; + +mupdf_result_t mupdf_open_doc_from_bytes( + fz_context* ctx, + uint8_t* bytes, size_t size, + fz_document** doc, uint32_t* page_count) +{ + mupdf_result_t result = {0}; + fz_stream* stream = NULL; + + fz_try(ctx) + { + fz_buffer buf = {0}; + buf.data = bytes; + buf.cap = size; + buf.len = size; + + stream = fz_open_buffer(ctx, &buf); + *doc = (fz_document*)pdf_open_document_with_stream(ctx, stream); + if (*doc == NULL) break; + + *page_count = fz_count_pages(ctx, *doc); + } + fz_always(ctx) + { + fz_drop_stream(ctx, stream); + } + fz_catch(ctx) + { + result.err_msg = fz_caught_message(ctx); + result.type = fz_caught(ctx); + } + + return result; +} + +mupdf_result_t mupdf_page_to_svg(fz_context* ctx, fz_document* doc, + size_t page_number, fz_buffer** buf) +{ + mupdf_result_t result = {0}; + fz_page* page = NULL; + fz_output* out = NULL; + fz_device* dev = NULL; + + fz_try(ctx) + { + // TODO: page bound checks when running in debug mode? + page = fz_load_page(ctx, doc, page_number); + if (page == NULL) break; // skip to fz_always + + fz_rect bbox = fz_bound_page(ctx, page); + float width = bbox.x1 - bbox.x0, + height = bbox.y1 - bbox.y0; + + *buf = fz_new_buffer(ctx, 1024); + if (*buf == NULL) break; // skip to fz_always + out = fz_new_output_with_buffer(ctx, *buf); + if (out == NULL) break; // skip to fz_always + + dev = fz_new_svg_device(ctx, out, width, height, FZ_SVG_TEXT_AS_PATH, 0); + if (dev == NULL) break; // skip to fz_always + + fz_run_page(ctx, page, dev, fz_identity, NULL); + fz_close_device(ctx, dev); + fz_close_output(ctx, out); + } + fz_always(ctx) + { + fz_drop_device(ctx, dev); + fz_drop_output(ctx, out); + fz_drop_page(ctx, page); + } + fz_catch(ctx) + { + result.err_msg = fz_caught_message(ctx); + result.type = fz_caught(ctx); + } + + return result; +}
diff --git a/src/image.rs b/src/image.rs @@ -43,8 +43,8 @@ use crate::create_file; #[derive(Debug)] pub struct Image { - width: usize, - height: usize, + width: usize, + height: usize, pixels: PixelBuffer, }
diff --git a/src/log.rs b/src/log.rs @@ -77,8 +77,8 @@ pub fn version(program_name: &str) { } pub fn usage(program: &str) { - use crate::{FULL_BUILD_OPT, N_THREADS_OPT}; - println!(" {BOLD_YELLOW}Usage{RESET} {program} [{FULL_BUILD_OPT}] [{N_THREADS_OPT} <jobs>] <config.yml>"); + use crate::FULL_BUILD_OPT; + println!(" {BOLD_YELLOW}Usage{RESET} {program} [{FULL_BUILD_OPT}] <config.yml>"); } pub fn usage_config() {
diff --git a/src/main.rs b/src/main.rs @@ -1,15 +1,12 @@ use std::{ - cmp, env, fmt::{self, Display}, fs::{self, File}, - io::{self, Write}, + io::{self, Read, Write}, path::{Path, PathBuf}, - process::{ExitCode, Command}, - sync::mpsc, + process::ExitCode, time::Instant, }; -use threadpool::ThreadPool; use gallery_entry::{GalleryEntry, FileFormat, LicenseType}; use escape::Escaped; @@ -17,9 +14,16 @@ use escape::Escaped; #[macro_use] mod log; mod image; +mod mupdf; mod gallery_entry; mod escape; +struct RenderingJob<'a> { + pub path: &'a Path, + pub file_name: &'a str, + pub thumb_path: PathBuf, +} + /// A wrapper for displaying the path for the thumbnail of a given path pub struct ThumbPath<'a>(pub &'a GalleryEntry); @@ -27,8 +31,6 @@ pub struct ThumbPath<'a>(pub &'a GalleryEntry); pub struct HtmlFileName<'a>(pub &'a str); const FULL_BUILD_OPT: &str = "-B"; -const N_THREADS_OPT: &str = "-j"; -const BOTH_OPTS: &str = "-Bj"; const TARGET_PATH: &str = "./site"; const PAGES_PATH: &str = "figures"; @@ -56,39 +58,10 @@ fn main() -> ExitCode { }; let mut full_build = false; - let total_cores = num_cpus::get(); - let mut num_cores = total_cores - 1; - while let Some(arg) = args.next() { - let mut is_valid_arg = false; - - if arg == FULL_BUILD_OPT || arg == BOTH_OPTS { + for arg in args { + if arg == FULL_BUILD_OPT { full_build = true; - is_valid_arg = true; - } - - if arg == N_THREADS_OPT || arg == BOTH_OPTS { - is_valid_arg = true; - - let val = match args.next() { - Some(val) => val, - None => { - errorln!("Expected one more argument, got none"); - log::usage(&program); - return ExitCode::FAILURE; - } - }; - - match val.parse() { - Ok(val) => num_cores = val, - Err(_) => { - errorln!("Expected a number, got {val:?}"); - log::usage(&program); - return ExitCode::FAILURE; - } - } - } - - if !is_valid_arg { + } else { if arg.starts_with("-") { errorln!("Unknown option: {arg:?}"); } else { @@ -111,7 +84,7 @@ fn main() -> ExitCode { log::usage_config(); return ExitCode::FAILURE; } - Ok(Ok(pics)) => if render_gallery(pics, full_build, num_cores, total_cores).is_err() { + Ok(Ok(pics)) => if render_gallery(pics, full_build).is_err() { return ExitCode::FAILURE; }, } @@ -123,18 +96,20 @@ fn main() -> ExitCode { fn render_gallery( pics: Vec<GalleryEntry>, full_build: bool, - num_cores: usize, - total_cores: usize, ) -> Result<(), ()> { struct Job { pic_id: usize, image_path: PathBuf, - thumb_path: PathBuf, page_path: PathBuf, } let start = Instant::now(); + let mut tex_jobs = Vec::with_capacity(pics.len()); + let mut svg_jobs = Vec::new(); + let mut png_jobs = Vec::new(); + let mut jpeg_jobs = Vec::new(); + let mut skipped = 0; let mut jobs = Vec::with_capacity(pics.len()); for (pic_id, pic) in pics.iter().enumerate() { @@ -142,8 +117,6 @@ fn render_gallery( image_path.push(IMAGES_PATH); image_path.push(&pic.file_name); - let thumb_path: PathBuf = ThumbPath(pic).into(); - let mut page_path = PathBuf::from(TARGET_PATH); page_path.push(PAGES_PATH); page_path.push(format!("{}", HtmlFileName(&pic.file_name))); @@ -156,9 +129,39 @@ fn render_gallery( } if full_build || needs_update(pic, &image_path) - || needs_update(pic, &thumb_path) || needs_update(pic, &page_path) { - jobs.push(Job { pic_id, image_path, thumb_path, page_path, }); + jobs.push(Job { pic_id, image_path, page_path, }); + + match pic.file_format { + FileFormat::TeX => { + tex_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}.svg", pic.file_name).into(), + }); + } + FileFormat::Svg => { + svg_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}", pic.file_name).into(), + }); + } + FileFormat::Png => { + png_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}.webp", pic.file_name).into(), + }); + } + FileFormat::Jpeg => { + jpeg_jobs.push(RenderingJob { + path: &pic.path, + file_name: &pic.file_name, + thumb_path: format!("{TARGET_PATH}/{}.webp", pic.file_name).into(), + }); + } + } } else { skipped += 1; } @@ -196,50 +199,30 @@ fn render_gallery( infoln!("Copied image files to the target directory"); // ======================================================================== - let num_cores = cmp::min(num_cores, jobs.len()); - - // NOTE: only spawn the threads if necessary - if num_cores > 1 { - infoln!("Rendering thumbnails... (using {num_cores}/{total_cores} cores)"); - let rendering_pool = ThreadPool::with_name( - String::from("thumbnails renderer"), - num_cores, - ); - let (sender, reciever) = mpsc::channel(); - - for Job { pic_id, thumb_path, .. } in &jobs { - let pic_id = *pic_id; - let thumb_path = thumb_path.clone(); - let pic = pics[pic_id].clone(); - let sender = sender.clone(); - - rendering_pool.execute(move || { - // NOTE: we need to send the picture id back so that the main thread - // knows how to log the fact we finished rendering it - let _ = sender.send( - render_thumbnail(&pic, &thumb_path).map(|()| pic_id) - ); - }); - } + infoln!("Rendering thumbnails..."); - for _ in 0..jobs.len() { - let msg = reciever.recv(); - // propagate the panic to the main thread: reciever.recv should - // only fail if some of the rendering threads panicked - if msg.is_err() { panic!("rendering thread panicked!"); } + // TODO: log something while compiling the TeX? + render_tikz_thumbnails(&tex_jobs)?; - let pic_id = msg.unwrap()?; - let pic = &pics[pic_id]; - log::job_finished(&pic.file_name); - } - } else { - infoln!("Rendering thumbnails... (using 1/{total_cores} core)"); - for Job { pic_id, thumb_path, .. } in &jobs { - let pic = &pics[*pic_id]; + for pic in svg_jobs { + let mut src_path = PathBuf::from(TARGET_PATH); + src_path.push(IMAGES_PATH); + src_path.push(pic.file_name); - render_thumbnail(pic, thumb_path)?; - log::job_finished(&pic.file_name); - } + copy(&src_path, &pic.thumb_path)?; + log::job_finished(&pic.file_name); + } + + const TARGET_HEIGHT: usize = 500; + for pic in png_jobs { + let img = image::parse_and_downsample_png(pic.path, TARGET_HEIGHT)?; + image::encode_webp(&img, &pic.thumb_path)?; + log::job_finished(&pic.file_name); + } + for pic in jpeg_jobs { + let img = image::parse_and_downsample_jpeg(pic.path, TARGET_HEIGHT)?; + image::encode_webp(&img, &pic.thumb_path)?; + log::job_finished(&pic.file_name); } // ========================================================================== @@ -433,80 +416,133 @@ fn write_license(f: &mut File) -> io::Result<()> { writeln!(f, "{}", LICENSE_COMMENT) } -fn render_thumbnail(pic: &GalleryEntry, thumb_path: &Path) -> Result<(), ()> { - const TARGET_HEIGHT: usize = 500; +fn render_tikz_thumbnails(pics: &[RenderingJob<'_>]) -> Result<(), ()> { + let mut tmp_dir = env::temp_dir(); + tmp_dir.push(random_dir_name()); - match pic.file_format { - FileFormat::TeX => { - // TODO: [optimize]: remove the dependency on tikztosvg? - // TODO: [optimize]: use pdflatex instead of lualatex? - // TODO: [optimize]: include the packages+TikZ libraries in a per file - // basis? - // TODO: [optimize]: do the PDF -> SVG conversion in house? - // - // you can use poppler+caire as in pdf2svg: - // https://github.com/dawbarton/pdf2svg/blob/master/pdf2svg.c - // - // you could also try MuPDF, which seems to be more - // efficient: - // https://github.com/ArtifexSoftware/mupdf/blob/master/include/mupdf/fitz/output-svg.h - - // tikztosvg -o thumb_path - // -p relsize - // -p xfrac - // -l matrix - // -l patterns - // -l shapes.geometric - // -l arrows - // -q - // pic.path - let mut tikztosvg_cmd = Command::new("tikztosvg"); - tikztosvg_cmd.arg("-o") - .arg(thumb_path) - .args([ - "-p", "relsize", - "-p", "xfrac", - "-l", "matrix", - "-l", "patterns", - "-l", "shapes.geometric", - "-l", "arrows", - "-q", - ]) - .arg(&pic.path); - - let exit_code = tikztosvg_cmd - .status() - .map_err(|e| errorln!("Failed to run tikztosvg: {e}"))?; - - if !exit_code.success() { - errorln!( - "Failed to run tikztosvg: {tikztosvg_cmd:?} returned exit code {exit_code}" - ); - return Err(()); - } - } - FileFormat::Svg => { - let mut src_path = PathBuf::from(TARGET_PATH); - src_path.push(IMAGES_PATH); - src_path.push(&pic.file_name); + if let Err(e) = fs::create_dir(&tmp_dir) { + errorln!("Could not create {tmp_dir:?}: {e}"); + return Err(()); + } + + let result = render_impl(pics, &tmp_dir); + if let Err(e) = fs::remove_dir_all(&tmp_dir) { + errorln!("Could not delete {tmp_dir:?}: {e}"); + return Err(()); + } + + return result; - copy(&src_path, thumb_path)?; + fn render_impl(pics: &[RenderingJob<'_>], tmp_dir: &Path) -> Result<(), ()> { + use std::process::{Command, Stdio}; + + const TEX_ENGINE: &str = "lualatex"; + const TEX_FILE_PATH: &str = "drawings.tex"; + const PDF_FILE_PATH: &str = "drawings.pdf"; + + // ======================================================================== + let mut tex_path = PathBuf::from(tmp_dir); + tex_path.push(TEX_FILE_PATH); + + let mut tex_f = create_file(&tex_path).map_err(|_| ())?; + + let tex_contents = generate_tex_contents(pics)?; + if let Err(e) = tex_f.write_all(tex_contents.as_bytes()) { + errorln!("Could not write to {tex_path:?}: {e}"); + return Err(()); } - FileFormat::Jpeg => { - // NOTE: even if the picture is no taller than TARGET_HEIGHT * 2, it is - // faster to downsample and then encode - let img = image::parse_and_downsample_jpeg(&pic.path, TARGET_HEIGHT)?; - image::encode_webp(&img, thumb_path)?; + drop(tex_f); + + let mut tex_cmd = Command::new(TEX_ENGINE); + tex_cmd + .arg("-halt-on-error") + .arg(format!("-output-directory={}", tmp_dir.to_string_lossy())) + .arg(&tex_path) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + let exit_code = tex_cmd + .status() + .map_err(|e| errorln!("Failed to run {TEX_ENGINE}: {e}"))?; + + if !exit_code.success() { + errorln!( + "Failed to run {TEX_ENGINE}: {tex_cmd:?} returned exit code {exit_code}" + ); + return Err(()); } - FileFormat::Png => { - // NOTE: even if the picture is no taller than TARGET_HEIGHT * 2, it is - // faster to downsample and then encode - let img = image::parse_and_downsample_png(&pic.path, TARGET_HEIGHT)?; - image::encode_webp(&img, thumb_path)?; + + // ======================================================================== + let mut pdf_path = PathBuf::from(tmp_dir); + pdf_path.push(PDF_FILE_PATH); + + let pdf_contents = fs::read(&pdf_path) + .map_err(|e| { errorln!("Could not read {pdf_path:?}: {e}") })?; + + let pdf_doc = mupdf::Document::open_from_bytes(&pdf_contents) + .map_err(|e| errorln!("Could not parse {pdf_path:?}: {e}"))?; + + for (page, pic) in pics.iter().enumerate() { + let buf = pdf_doc.render_page_to_svg(page) + .map_err(|e| { + errorln!("Could not render {:?} to SVG: {e}", pic.file_name) + })?; + + let mut svg_f = create_file(&pic.thumb_path).map_err(|_| ())?; + if let Err(e) = svg_f.write_all(&buf) { + errorln!("Could not write to {:?}: {e}", pic.thumb_path); + return Err(()); + } + + log::job_finished(&pic.file_name); } + + Ok(()) } +} - Ok(()) +fn generate_tex_contents(pics: &[RenderingJob<'_>]) -> Result<String, ()> { + use std::fmt::Write; + + const LATEX_PACKAGES: &[&str] = &["tikz", "pgfplots", "amsmath", "amssymb"]; + const TIKZ_LIBRARIES: &[&str] = &[ + "matrix", + "patterns", + "shapes.geometric", + "arrows", + ]; + + let mut sb = String::with_capacity(1024); + + let _ = writeln!(&mut sb, "\\documentclass[crop, tikz]{{standalone}}"); + + let _ = write!(&mut sb, "\\usepackage{{"); + for (i, pkg) in LATEX_PACKAGES.iter().enumerate() { + if i != 0 { sb.push_str(", "); } + sb.push_str(pkg); + } + let _ = writeln!(&mut sb, "}}"); + + let _ = write!(&mut sb, "\\usetikzlibrary{{"); + for (i, lib) in TIKZ_LIBRARIES.iter().enumerate() { + if i != 0 { sb.push_str(", "); } + sb.push_str(lib); + } + let _ = writeln!(&mut sb, "}}"); + + let _ = writeln!(&mut sb, "\\pgfplotsset{{compat=1.18}}"); + let _ = writeln!(&mut sb, "\\begin{{document}}"); + + for pic in pics { + sb.push('\n'); + let _ = File::open(pic.path) + .and_then(|mut f| f.read_to_string(&mut sb)) + .map_err(|e| errorln!("Could not read {:?}: {e}", pic.path))?; + } + + let _ = writeln!(&mut sb, "\n\\end{{document}}"); + + Ok(sb) } fn needs_update(pic: &GalleryEntry, dst: &Path) -> bool { @@ -531,6 +567,27 @@ fn copy(from: &Path, to: &Path) -> Result<(), ()> { .map_err(|e| errorln!("Failed to copy {from:?} to {to:?}: {e}")) } +fn random_dir_name() -> String { + use std::time::{self, SystemTime}; + + const RND_DIR_NAME_SIZE: usize = 16; + const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789"; + + let mut sb = String::with_capacity(RND_DIR_NAME_SIZE); + let rng = SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let mut rng = (rng % (usize::MAX as u128)) as usize; + + for _ in 0..RND_DIR_NAME_SIZE { + rng = rng.wrapping_mul(1103515245).wrapping_add(12345) % CHARSET.len(); + sb.push(CHARSET[rng] as char); + } + + sb +} + impl Display for ThumbPath<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{THUMBS_PATH}/{name}", name = Escaped(&self.0.file_name))?; @@ -545,29 +602,6 @@ impl Display for ThumbPath<'_> { } } -impl From<ThumbPath<'_>> for PathBuf { - fn from(thumb_path: ThumbPath<'_>) -> Self { - let pic = thumb_path.0; - - let mut result = PathBuf::from(TARGET_PATH); - result.push(THUMBS_PATH); - - match pic.file_format { - FileFormat::TeX => { - result.push(pic.file_name.clone() + ".svg"); - } - FileFormat::Svg => { - result.push(&pic.file_name); - } - FileFormat::Jpeg | FileFormat::Png => { - result.push(pic.file_name.clone() + ".webp"); - } - } - - result - } -} - impl Display for HtmlFileName<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{}.html", self.0)
diff --git /dev/null b/src/mupdf.rs @@ -0,0 +1,111 @@ +//! Safe MuPDF wrappers +use std::{ptr, slice, ops::Deref, ffi::CStr}; +use mupdf_sys::*; + +#[derive(Debug)] +pub struct Document { + ctx: *mut fz_context, + inner: *mut fz_document, + + page_count: usize, +} + +#[derive(Debug)] +pub struct Buffer<'doc> { + doc: &'doc Document, + inner: *mut fz_buffer, +} + +impl Document { + pub fn open_from_bytes(bytes: &[u8]) -> Result<Self, String> { + let ctx = unsafe { + fz_new_context(ptr::null(), ptr::null(), FZ_STORE_DEFAULT as usize) + }; + assert!(!ctx.is_null()); + + // SAFETY: this should really be wrapped with fz_try, but it can only + // fail if ctx does not contain a document handler list (?) or + // the list is already full. seems safe to assume + // fz_new_documment will handle us a sane ctx + unsafe { fz_register_document_handlers(ctx); } + + unsafe { + fz_set_warning_callback(ctx, None, ptr::null_mut()); + fz_set_error_callback(ctx, None, ptr::null_mut()); + } + + let mut doc = ptr::null_mut(); + let mut page_count = 0; + let result = unsafe { + mupdf_open_doc_from_bytes( + ctx, + bytes.as_ptr() as *mut _, + bytes.len(), + &mut doc, + &mut page_count, + ) + }; + + if result.type_ != FZ_ERROR_NONE { + return Err(unsafe { + CStr::from_ptr(result.err_msg).to_string_lossy().to_string() + }); + } + + assert!(!doc.is_null()); + let page_count = page_count as usize; + Ok(Self { ctx, inner: doc, page_count, }) + } + + pub fn render_page_to_svg( + &self, + page: usize, + ) -> Result<Buffer<'_>, String> { + assert!(page < self.page_count, + "page {page} is out of bounds: document only has {} pages", + self.page_count); + + let mut buf = ptr::null_mut(); + let result = unsafe { + mupdf_page_to_svg(self.ctx, self.inner, page, &mut buf) + }; + + if result.type_ != FZ_ERROR_NONE { + return Err(unsafe { + CStr::from_ptr(result.err_msg).to_string_lossy().to_string() + }); + } + + assert!(!buf.is_null()); + Ok(Buffer { doc: self, inner: buf, }) + } +} + +impl Drop for Document { + fn drop(&mut self) { + unsafe { + fz_drop_document(self.ctx, self.inner); + fz_drop_context(self.ctx); + } + } +} + +impl<'doc> Deref for Buffer<'doc> { + type Target = [u8]; + + fn deref(&self) -> &[u8] { + unsafe { + slice::from_raw_parts( + (*self.inner).data as *const _, + (*self.inner).len + ) + } + } +} + +impl<'doc> Drop for Buffer<'doc> { + fn drop(&mut self) { + unsafe { fz_drop_buffer(self.doc.ctx, self.inner); } + } +} +
diff --git a/src/outro.html b/src/outro.html @@ -19,7 +19,7 @@ instructions on how to include a given picture in your documents. <section id="contributing"> <h2>contributing</h2> <p> -As of now, this gallery is run by <a href="https://https://www.math.univ-toulouse.fr/~tbrevide/">Thiago Brevidelli</a>. If +As of now, this gallery is run by <a href="https://www.math.univ-toulouse.fr/~tbrevide/">Thiago Brevidelli</a>. If you would like to add your drawings to here please contact <a href="mailto:thiago.brevidelli_garcia@math.univ-toulouse.fr">thiago.brevidelli_garcia@math.univ-toulouse.fr</a>. </p>