tikz-gallery-generator

Custum build of stapix for tikz.pablopie.xyz

Commit
bfa14940aed0da732da212d17e8451e0a337deca
Parent
1e444b4fa16b3b2dbdb6da8d2e42495222c262c1
Author
Pablo <pablo-pie@riseup.net>
Date

Merge branch 'no-tikztosvg'

Diffstats

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="&#109;&#97;&#105;&#108;&#116;&#111;:&#116;&#104;&#105;&#97;&#103;&#111;.&#98;&#114;&#101;&#118;&#105;&#100;&#101;&#108;&#108;&#105;_&#103;&#97;&#114;&#99;&#105;&#97;@&#109;&#97;&#116;&#104;.&#117;&#110;&#105;&#118;-&#116;&#111;&#117;&#108;&#111;&#117;&#115;&#101;.&#102;&#114;">&#116;&#104;&#105;&#97;&#103;&#111;.&#98;&#114;&#101;&#118;&#105;&#100;&#101;&#108;&#108;&#105;_&#103;&#97;&#114;&#99;&#105;&#97;@&#109;&#97;&#116;&#104;.&#117;&#110;&#105;&#118;-&#116;&#111;&#117;&#108;&#111;&#117;&#115;&#101;.&#102;&#114;</a>.
 </p>