mirror of
https://git.asonix.dog/asonix/pict-rs.git
synced 2024-12-27 04:40:31 +00:00
Avoid shelling out to imagemagick for some blurhash operations
This commit is contained in:
parent
31e17b4d62
commit
7ba67cff22
9 changed files with 244 additions and 26 deletions
112
Cargo.lock
generated
112
Cargo.lock
generated
|
@ -521,12 +521,24 @@ version = "3.16.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.9.0"
|
||||
|
@ -643,6 +655,12 @@ dependencies = [
|
|||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
|
@ -1016,6 +1034,15 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flagset"
|
||||
version = "0.4.6"
|
||||
|
@ -1184,6 +1211,16 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.28.1"
|
||||
|
@ -1610,6 +1647,33 @@ dependencies = [
|
|||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"image-webp",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image-webp"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
|
||||
dependencies = [
|
||||
"byteorder-lite",
|
||||
"quick-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "impl-more"
|
||||
version = "0.1.8"
|
||||
|
@ -1864,6 +1928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2168,6 +2233,7 @@ dependencies = [
|
|||
"diesel-derive-enum",
|
||||
"futures-core",
|
||||
"hex",
|
||||
"image",
|
||||
"md-5",
|
||||
"metrics",
|
||||
"metrics-exporter-prometheus",
|
||||
|
@ -2244,6 +2310,19 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.10.0"
|
||||
|
@ -2368,6 +2447,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.36.2"
|
||||
|
@ -2982,6 +3067,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
|
@ -3876,6 +3967,12 @@ dependencies = [
|
|||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.2"
|
||||
|
@ -4190,3 +4287,18 @@ dependencies = [
|
|||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
|
|
@ -96,6 +96,7 @@ url = { version = "2.5.2", features = ["serde"] }
|
|||
uuid = { version = "1.10.0", features = ["serde", "std", "v4", "v7"] }
|
||||
# pinned to rustls
|
||||
webpki-roots = "0.26.3"
|
||||
image = { version = "0.25.5", default-features = false, features = ["gif", "jpeg", "png", "webp"] }
|
||||
|
||||
[dependencies.tracing-actix-web]
|
||||
version = "0.7.15"
|
||||
|
|
118
src/blurhash.rs
118
src/blurhash.rs
|
@ -1,11 +1,16 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
use std::{
|
||||
ffi::{OsStr, OsString},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::{
|
||||
bytes_stream::BytesStream,
|
||||
details::Details,
|
||||
error::{Error, UploadError},
|
||||
formats::ProcessableFormat,
|
||||
future::{WithMetrics, WithTimeout},
|
||||
magick::{MagickError, MAGICK_CONFIGURE_PATH, MAGICK_TEMPORARY_PATH},
|
||||
process::Process,
|
||||
repo::Hash,
|
||||
|
@ -18,6 +23,27 @@ pub(crate) async fn generate<S>(
|
|||
hash: Hash,
|
||||
original_details: &Details,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
S: Store + 'static,
|
||||
{
|
||||
let permit = crate::process_semaphore().acquire().await?;
|
||||
|
||||
let blurhash = do_generate(state, hash, original_details)
|
||||
.with_timeout(Duration::from_secs(state.config.media.process_timeout * 4))
|
||||
.with_metrics(crate::init_metrics::GENERATE_BLURHASH)
|
||||
.await
|
||||
.map_err(|_| UploadError::ProcessTimeout)??;
|
||||
|
||||
drop(permit);
|
||||
|
||||
Ok(blurhash)
|
||||
}
|
||||
|
||||
pub(crate) async fn do_generate<S>(
|
||||
state: &State<S>,
|
||||
hash: Hash,
|
||||
original_details: &Details,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
S: Store + 'static,
|
||||
{
|
||||
|
@ -35,38 +61,78 @@ where
|
|||
|
||||
let stream = state.store.to_stream(&identifier, None, None).await?;
|
||||
|
||||
let blurhash = read_rgba_command(
|
||||
state,
|
||||
input_details
|
||||
.internal_format()
|
||||
.processable_format()
|
||||
.expect("not a video"),
|
||||
)
|
||||
.await?
|
||||
.drive_with_stream(stream)
|
||||
.with_stdout(|mut stdout| async move {
|
||||
let mut encoder = blurhash_update::Encoder::auto(blurhash_update::ImageBounds {
|
||||
width: input_details.width() as _,
|
||||
height: input_details.height() as _,
|
||||
});
|
||||
match input_details.internal_format().image_rs_format() {
|
||||
// supported pure-rust image decoders
|
||||
Some(
|
||||
format @ image::ImageFormat::Gif
|
||||
| format @ image::ImageFormat::Jpeg
|
||||
| format @ image::ImageFormat::Png
|
||||
| format @ image::ImageFormat::WebP,
|
||||
) => {
|
||||
let bytes_stream = BytesStream::try_from_stream(stream).await?;
|
||||
|
||||
let mut buf = [0u8; 1024 * 8];
|
||||
let blurhash = crate::sync::spawn_blocking("image-blurhash", move || {
|
||||
let mut vec = Vec::with_capacity(bytes_stream.len());
|
||||
|
||||
loop {
|
||||
let n = stdout.read(&mut buf).await?;
|
||||
for bytes in bytes_stream {
|
||||
vec.extend(bytes);
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
let raw_image = image::ImageReader::with_format(std::io::Cursor::new(vec), format)
|
||||
.decode()
|
||||
.map_err(UploadError::Decode)?
|
||||
.into_rgba8();
|
||||
|
||||
encoder.update(&buf[..n]);
|
||||
let blurhash = blurhash_update::auto_encode(
|
||||
blurhash_update::ImageBounds {
|
||||
width: raw_image.width(),
|
||||
height: raw_image.height(),
|
||||
},
|
||||
raw_image.as_raw(),
|
||||
);
|
||||
|
||||
Ok(blurhash) as Result<_, UploadError>
|
||||
})
|
||||
.await
|
||||
.map_err(|_| UploadError::Canceled)??;
|
||||
|
||||
Ok(blurhash)
|
||||
}
|
||||
_ => {
|
||||
let blurhash = read_rgba_command(
|
||||
state,
|
||||
input_details
|
||||
.internal_format()
|
||||
.processable_format()
|
||||
.expect("not a video"),
|
||||
)
|
||||
.await?
|
||||
.drive_with_stream(stream)
|
||||
.with_stdout(|mut stdout| async move {
|
||||
let mut encoder = blurhash_update::Encoder::auto(blurhash_update::ImageBounds {
|
||||
width: input_details.width() as _,
|
||||
height: input_details.height() as _,
|
||||
});
|
||||
|
||||
Ok(encoder.finalize()) as std::io::Result<String>
|
||||
})
|
||||
.await??;
|
||||
let mut buf = [0u8; 1024 * 8];
|
||||
|
||||
Ok(blurhash)
|
||||
loop {
|
||||
let n = stdout.read(&mut buf).await?;
|
||||
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
encoder.update(&buf[..n]);
|
||||
}
|
||||
|
||||
Ok(encoder.finalize()) as std::io::Result<String>
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(blurhash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_rgba_command<S>(
|
||||
|
|
|
@ -174,6 +174,9 @@ pub(crate) enum UploadError {
|
|||
#[error("Failed external validation")]
|
||||
FailedExternalValidation,
|
||||
|
||||
#[error("Failed to decode image")]
|
||||
Decode(#[source] image::error::ImageError),
|
||||
|
||||
#[cfg(feature = "random-errors")]
|
||||
#[error("Randomly generated error for testing purposes")]
|
||||
RandomError,
|
||||
|
@ -217,6 +220,7 @@ impl UploadError {
|
|||
Self::InvalidJob(_, _) => ErrorCode::INVALID_JOB,
|
||||
Self::InvalidQuery(_) => ErrorCode::INVALID_QUERY,
|
||||
Self::InvalidJson(_) => ErrorCode::INVALID_JSON,
|
||||
Self::Decode(_) => ErrorCode::DECODE_IMAGE,
|
||||
#[cfg(feature = "random-errors")]
|
||||
Self::RandomError => ErrorCode::RANDOM_ERROR,
|
||||
}
|
||||
|
|
|
@ -153,6 +153,9 @@ impl ErrorCode {
|
|||
pub(crate) const INVALID_JSON: ErrorCode = ErrorCode {
|
||||
code: "invalid-json",
|
||||
};
|
||||
pub(crate) const DECODE_IMAGE: ErrorCode = ErrorCode {
|
||||
code: "decode-image",
|
||||
};
|
||||
#[cfg(feature = "random-errors")]
|
||||
pub(crate) const RANDOM_ERROR: ErrorCode = ErrorCode {
|
||||
code: "random-error",
|
||||
|
|
|
@ -138,6 +138,14 @@ impl InternalFormat {
|
|||
Self::Video(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn image_rs_format(self) -> Option<::image::ImageFormat> {
|
||||
match self {
|
||||
Self::Image(format) => format.image_rs_format(),
|
||||
Self::Animation(format) => Some(format.image_rs_format()),
|
||||
Self::Video(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessableFormat {
|
||||
|
|
|
@ -81,4 +81,13 @@ impl AnimationFormat {
|
|||
Self::Webp => super::mimes::image_webp(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn image_rs_format(self) -> image::ImageFormat {
|
||||
match self {
|
||||
Self::Apng => image::ImageFormat::Png,
|
||||
Self::Avif => image::ImageFormat::Avif,
|
||||
Self::Gif => image::ImageFormat::Gif,
|
||||
Self::Webp => image::ImageFormat::WebP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,4 +99,14 @@ impl ImageFormat {
|
|||
Self::Webp => super::mimes::image_webp(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn image_rs_format(self) -> Option<image::ImageFormat> {
|
||||
match self {
|
||||
Self::Avif => Some(image::ImageFormat::Avif),
|
||||
Self::Jpeg => Some(image::ImageFormat::Jpeg),
|
||||
Self::Jxl => None,
|
||||
Self::Png => Some(image::ImageFormat::Png),
|
||||
Self::Webp => Some(image::ImageFormat::WebP),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -558,12 +558,17 @@ fn describe_generate() {
|
|||
GENERATE_PROCESS,
|
||||
"Timings for processing media or waiting for media to be processed"
|
||||
);
|
||||
metrics::describe_histogram!(
|
||||
GENERATE_BLURHASH,
|
||||
"Timings for computing blurhashes for media"
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) const GENERATE_START: &str = "pict-rs.generate.start";
|
||||
pub(crate) const GENERATE_DURATION: &str = "pict-rs.generate.duration";
|
||||
pub(crate) const GENERATE_END: &str = "pict-rs.generate.end";
|
||||
pub(crate) const GENERATE_PROCESS: &str = "pict-rs.generate.process";
|
||||
pub(crate) const GENERATE_BLURHASH: &str = "pict-rs.generate.blurhash";
|
||||
|
||||
fn describe_object_storage() {
|
||||
metrics::describe_histogram!(
|
||||
|
|
Loading…
Reference in a new issue