diff --git a/Cargo.lock b/Cargo.lock index 872011d..e48c01c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,6 +961,7 @@ dependencies = [ "usearch", "uuid", "walkdir", + "webp", ] [[package]] @@ -5209,6 +5210,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libwebp-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733" +dependencies = [ + "cc", + "glob", +] + [[package]] name = "libxdo" version = "0.6.0" @@ -10882,6 +10893,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c071456adef4aca59bf6a583c46b90ff5eb0b4f758fc347cea81290288f37ce1" +dependencies = [ + "libwebp-sys", +] + [[package]] name = "webpki-roots" version = "1.0.9" diff --git a/Cargo.toml b/Cargo.toml index 8aa9f3a..1262828 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ axum = "0.8" walkdir = "2" futures = "0.3" image = "0.25" +webp = { version = "0.3.1", default-features = false } rust-stemmers = "1" sys-locale = "0.3" fluent-bundle = "0.16" diff --git a/README.md b/README.md index 77fc43c..7cbca9c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The project is under active development. Core blogging workflows are broadly ava - Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars, relative-dated entity lists with row deletion, dialogs, tasks, embedded Wry previews, and a live Pico CSS theme editor. - Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, live link/backlink graphs, media, and batch gallery-image import. -- Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors. +- Media import, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. - Post, Liquid template, and Lua script editing with dedicated syntax highlighting, explicit syntax-check feedback, change-aware published/draft lifecycle, normalized collision-safe template/script slug changes, reference-safe template renames, and publish-time enforcement of the bDS2 Liquid tag/filter/operator subset, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync. - SQLite and filesystem persistence with frontmatter, relationship-safe media sidecars, rebuild, bidirectional metadata diff/repair including project categories and publishing preferences, stale post-path cleanup on republish, bDS2-compatible checksums and NFD slug generation, and FTS5 search. diff --git a/crates/bds-core/Cargo.toml b/crates/bds-core/Cargo.toml index bd18857..bd7bacc 100644 --- a/crates/bds-core/Cargo.toml +++ b/crates/bds-core/Cargo.toml @@ -19,6 +19,7 @@ unicode-normalization = { workspace = true } thiserror = { workspace = true } walkdir = { workspace = true } image = { workspace = true } +webp = { workspace = true } rust-stemmers = { workspace = true } sys-locale = { workspace = true } fluent-bundle = { workspace = true } diff --git a/crates/bds-core/src/util/thumbnail.rs b/crates/bds-core/src/util/thumbnail.rs index 50b309f..288119f 100644 --- a/crates/bds-core/src/util/thumbnail.rs +++ b/crates/bds-core/src/util/thumbnail.rs @@ -1,5 +1,5 @@ use image::imageops::FilterType; -use image::{DynamicImage, GenericImageView, ImageFormat, ImageReader}; +use image::{DynamicImage, GenericImageView, ImageReader}; use std::fs; use std::io::Cursor; use std::path::Path; @@ -101,13 +101,11 @@ pub fn generate_thumbnail( match size.format { ThumbnailFormat::Webp => { - // Encode as WebP via image crate - let mut buf = Cursor::new(Vec::new()); - resized - .write_to(&mut buf, ImageFormat::WebP) - .map_err(|e| format!("WebP encode: {e}"))?; - let _ = quality; // image crate WebP encoder doesn't expose quality directly - fs::write(dest, buf.into_inner()).map_err(|e| format!("write: {e}"))?; + let rgba = resized.to_rgba8(); + let encoded = webp::Encoder::from_rgba(rgba.as_raw(), rgba.width(), rgba.height()) + .encode_simple(false, f32::from(quality)) + .map_err(|e| format!("WebP encode: {e:?}"))?; + fs::write(dest, &*encoded).map_err(|e| format!("write: {e}"))?; } ThumbnailFormat::Jpeg => { let rgb = resized.to_rgb8(); @@ -325,6 +323,8 @@ mod tests { // 100x80 is smaller than 150 wide, no enlargement assert_eq!(w, 100); assert_eq!(h, 80); + let encoded = fs::read(&dest).unwrap(); + assert_eq!(&encoded[12..16], b"VP8 ", "thumbnail must use lossy WebP"); } #[test]