From da3d3e4c8e16d56ff1ccadd43ae5e89af3ae7c42 Mon Sep 17 00:00:00 2001
From: "Dustin C. Hatch"
Date: Fri, 14 Mar 2025 20:22:14 -0500
Subject: [PATCH] receipts/list: Generate thumbnails for receipts
Instead of sending the whole image file for every receipt shown on the
list page, we now generate thumbnails for them on the fly. This
dramatically reduces the amount of bytes sent for each image, especially
very large, high-quality photographs. It also improves support for
non-image attachments like PDFs, by rendering image previews in the
grid view instead of a broken image placeholder.
We use GraphicsMagic to do the conversion. Its `MagickWand` API is
pretty straightforward and convenient, and it supports a plethora of
image and image-like formats.
---
Cargo.lock | 119 +++++++++++++++++++++++++++++++
Cargo.toml | 1 +
Containerfile | 11 +++
src/imaging.rs | 46 ++++++++++++
src/main.rs | 3 +
src/receipts.rs | 31 ++++++--
src/routes/receipts.rs | 27 +++++++
templates/receipt-form.html.tera | 7 +-
templates/receipt-list.html.tera | 2 +-
9 files changed, 239 insertions(+), 8 deletions(-)
create mode 100644 src/imaging.rs
diff --git a/Cargo.lock b/Cargo.lock
index 8264c79..268eba6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -71,6 +71,12 @@ dependencies = [
"libc",
]
+[[package]]
+name = "anyhow"
+version = "1.0.97"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -185,6 +191,24 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
+[[package]]
+name = "bindgen"
+version = "0.71.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+dependencies = [
+ "bitflags 2.9.0",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 2.0.99",
+]
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -309,6 +333,15 @@ dependencies = [
"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.0"
@@ -356,6 +389,17 @@ dependencies = [
"phf_codegen",
]
+[[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 = "const-oid"
version = "0.9.6"
@@ -862,6 +906,28 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "graphicsmagick"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58c4ba437067d42af001dcda9d7188f368539b0731c2f5f4a6ba3765f3b64518"
+dependencies = [
+ "graphicsmagick-sys",
+ "null-terminated-str",
+ "num_enum",
+ "thiserror 2.0.12",
+]
+
+[[package]]
+name = "graphicsmagick-sys"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9004437800b450090fdd1045edc43647209601b68320a4be450de541d0be16f6"
+dependencies = [
+ "anyhow",
+ "bindgen",
+]
+
[[package]]
name = "h2"
version = "0.4.8"
@@ -1373,6 +1439,15 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[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.15"
@@ -1424,6 +1499,16 @@ version = "0.2.170"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
+[[package]]
+name = "libloading"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "libm"
version = "0.2.11"
@@ -1648,6 +1733,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "null-terminated-str"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "594ec098b589ef9bcf24ff9d2a1ed9295851ecb4009ce1df58557c239cf250bd"
+
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@@ -1711,6 +1802,27 @@ dependencies = [
"libc",
]
+[[package]]
+name = "num_enum"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.99",
+]
+
[[package]]
name = "object"
version = "0.36.7"
@@ -2090,6 +2202,7 @@ name = "receipts"
version = "0.1.0"
dependencies = [
"chrono",
+ "graphicsmagick",
"reqwest",
"rocket",
"rocket_db_pools",
@@ -2430,6 +2543,12 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
[[package]]
name = "rustix"
version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index 2273fee..a293ce9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ keywords = ["personal-finance", "receipts"]
[dependencies]
chrono = { version = "0.4.40", default-features = false, features = ["serde"] }
+graphicsmagick = { version = "0.6.1", features = ["v1_3_38"] }
reqwest = { version = "0.12.12", features = ["json"] }
rocket = { version = "0.5.1", default-features = false, features = ["json"] }
rocket_db_pools = { version = "0.2.0", features = ["sqlx_macros", "sqlx_postgres"] }
diff --git a/Containerfile b/Containerfile
index 838340a..eb80518 100644
--- a/Containerfile
+++ b/Containerfile
@@ -4,7 +4,9 @@ RUN --mount=type=cache,target=/var/cache \
microdnf install -y \
--setopt persistdir=/var/cache/dnf \
--setopt install_weak_deps=0 \
+ GraphicsMagick-devel \
cargo \
+ clang-devel \
openssl-devel \
&& :
@@ -38,6 +40,15 @@ RUN --mount=type=cache,target=/root/.cargo \
FROM git.pyrocufflink.net/containerimages/dch-base
+RUN --mount=type=cache,target=/var/cache \
+ microdnf install -y \
+ --setopt persistdir=/var/cache/dnf \
+ --setopt install_weak_deps=0 \
+ GraphicsMagick \
+ clang-libs \
+ ghostscript \
+ && :
+
COPY --from=build /build/target/release/receipts /usr/local/bin
COPY --from=esbuild /build/dist /usr/local/share/receipts/static
diff --git a/src/imaging.rs b/src/imaging.rs
new file mode 100644
index 0000000..b3f3457
--- /dev/null
+++ b/src/imaging.rs
@@ -0,0 +1,46 @@
+use graphicsmagick::types::FilterTypes;
+use graphicsmagick::wand::MagickWand;
+
+pub fn thumbnail(
+ image: &[u8],
+) -> Result
-
+
Choose File
diff --git a/templates/receipt-list.html.tera b/templates/receipt-list.html.tera
index 4b9f275..1f61e7f 100644
--- a/templates/receipt-list.html.tera
+++ b/templates/receipt-list.html.tera
@@ -94,7 +94,7 @@