2023-07-09 19:50:58 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests;
|
|
|
|
|
2021-09-14 01:22:42 +00:00
|
|
|
use crate::{
|
2022-10-01 01:00:14 +00:00
|
|
|
config::{ImageFormat, VideoCodec},
|
2023-07-10 20:29:41 +00:00
|
|
|
process::{Process, ProcessError},
|
2022-03-26 21:49:23 +00:00
|
|
|
repo::Alias,
|
2023-07-10 20:29:41 +00:00
|
|
|
store::Store,
|
2021-09-14 01:22:42 +00:00
|
|
|
};
|
2021-09-04 00:53:53 +00:00
|
|
|
use actix_web::web::Bytes;
|
2023-07-10 20:29:41 +00:00
|
|
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
pub(crate) enum MagickError {
|
|
|
|
#[error("Error in imagemagick process")]
|
|
|
|
Process(#[source] ProcessError),
|
|
|
|
|
|
|
|
#[error("Error parsing details")]
|
|
|
|
ParseDetails(#[source] ParseDetailsError),
|
|
|
|
|
|
|
|
#[error("Media details are invalid")]
|
|
|
|
ValidateDetails(#[source] ValidateDetailsError),
|
|
|
|
|
|
|
|
#[error("Invalid output format")]
|
|
|
|
Json(#[source] serde_json::Error),
|
|
|
|
|
|
|
|
#[error("Error reading bytes")]
|
|
|
|
Read(#[source] std::io::Error),
|
|
|
|
|
|
|
|
#[error("Error writing bytes")]
|
|
|
|
Write(#[source] std::io::Error),
|
|
|
|
|
|
|
|
#[error("Error creating file")]
|
|
|
|
CreateFile(#[source] std::io::Error),
|
|
|
|
|
|
|
|
#[error("Error creating directory")]
|
|
|
|
CreateDir(#[source] crate::store::file_store::FileError),
|
|
|
|
|
|
|
|
#[error("Error reading file")]
|
|
|
|
Store(#[source] crate::store::StoreError),
|
|
|
|
|
|
|
|
#[error("Error closing file")]
|
|
|
|
CloseFile(#[source] std::io::Error),
|
|
|
|
|
|
|
|
#[error("Error removing file")]
|
|
|
|
RemoveFile(#[source] std::io::Error),
|
|
|
|
|
|
|
|
#[error("Invalid file path")]
|
|
|
|
Path,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MagickError {
|
|
|
|
pub(crate) fn is_client_error(&self) -> bool {
|
|
|
|
// Failing validation or imagemagick bailing probably means bad input
|
|
|
|
matches!(self, Self::ValidateDetails(_))
|
|
|
|
|| matches!(self, Self::Process(ProcessError::Status(_)))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn is_not_found(&self) -> bool {
|
|
|
|
if let Self::Store(e) = self {
|
|
|
|
return e.is_not_found();
|
|
|
|
}
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
2021-08-28 22:15:14 +00:00
|
|
|
|
2022-03-26 21:49:23 +00:00
|
|
|
pub(crate) fn details_hint(alias: &Alias) -> Option<ValidInputType> {
|
|
|
|
let ext = alias.extension()?;
|
|
|
|
if ext.ends_with(".mp4") {
|
2021-10-23 04:48:56 +00:00
|
|
|
Some(ValidInputType::Mp4)
|
2022-09-25 23:16:37 +00:00
|
|
|
} else if ext.ends_with(".webm") {
|
|
|
|
Some(ValidInputType::Webm)
|
2021-10-23 04:48:56 +00:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-21 22:05:35 +00:00
|
|
|
fn image_avif() -> mime::Mime {
|
|
|
|
"image/avif".parse().unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn image_jxl() -> mime::Mime {
|
|
|
|
"image/jxl".parse().unwrap()
|
|
|
|
}
|
|
|
|
|
2022-09-25 23:16:37 +00:00
|
|
|
fn image_webp() -> mime::Mime {
|
2021-10-23 17:35:07 +00:00
|
|
|
"image/webp".parse().unwrap()
|
|
|
|
}
|
|
|
|
|
2022-09-26 01:39:09 +00:00
|
|
|
pub(crate) fn video_mp4() -> mime::Mime {
|
2021-10-23 17:35:07 +00:00
|
|
|
"video/mp4".parse().unwrap()
|
|
|
|
}
|
|
|
|
|
2022-09-26 01:39:09 +00:00
|
|
|
pub(crate) fn video_webm() -> mime::Mime {
|
2022-09-25 23:16:37 +00:00
|
|
|
"video/webm".parse().unwrap()
|
|
|
|
}
|
|
|
|
|
2021-10-23 17:35:07 +00:00
|
|
|
#[derive(Copy, Clone, Debug)]
|
2021-08-29 01:37:53 +00:00
|
|
|
pub(crate) enum ValidInputType {
|
|
|
|
Mp4,
|
2022-09-25 23:16:37 +00:00
|
|
|
Webm,
|
2021-08-29 01:37:53 +00:00
|
|
|
Gif,
|
2023-06-21 22:05:35 +00:00
|
|
|
Avif,
|
2021-08-29 01:37:53 +00:00
|
|
|
Jpeg,
|
2023-06-21 22:05:35 +00:00
|
|
|
Jxl,
|
|
|
|
Png,
|
2021-08-28 22:15:14 +00:00
|
|
|
Webp,
|
|
|
|
}
|
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
impl ValidInputType {
|
2023-02-04 23:32:36 +00:00
|
|
|
const fn as_str(self) -> &'static str {
|
2021-10-23 04:48:56 +00:00
|
|
|
match self {
|
|
|
|
Self::Mp4 => "MP4",
|
2022-09-25 23:16:37 +00:00
|
|
|
Self::Webm => "WEBM",
|
2021-10-23 04:48:56 +00:00
|
|
|
Self::Gif => "GIF",
|
2023-06-21 22:05:35 +00:00
|
|
|
Self::Avif => "AVIF",
|
2021-10-23 04:48:56 +00:00
|
|
|
Self::Jpeg => "JPEG",
|
2023-06-21 22:05:35 +00:00
|
|
|
Self::Jxl => "JXL",
|
|
|
|
Self::Png => "PNG",
|
2021-10-23 04:48:56 +00:00
|
|
|
Self::Webp => "WEBP",
|
|
|
|
}
|
|
|
|
}
|
2021-10-23 17:35:07 +00:00
|
|
|
|
2023-02-04 23:32:36 +00:00
|
|
|
pub(crate) const fn as_ext(self) -> &'static str {
|
2021-10-23 17:35:07 +00:00
|
|
|
match self {
|
|
|
|
Self::Mp4 => ".mp4",
|
2022-09-25 23:16:37 +00:00
|
|
|
Self::Webm => ".webm",
|
2021-10-23 17:35:07 +00:00
|
|
|
Self::Gif => ".gif",
|
2023-06-21 22:05:35 +00:00
|
|
|
Self::Avif => ".avif",
|
2021-10-23 17:35:07 +00:00
|
|
|
Self::Jpeg => ".jpeg",
|
2023-06-21 22:05:35 +00:00
|
|
|
Self::Jxl => ".jxl",
|
|
|
|
Self::Png => ".png",
|
2021-10-23 17:35:07 +00:00
|
|
|
Self::Webp => ".webp",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-04 23:32:36 +00:00
|
|
|
pub(crate) const fn is_video(self) -> bool {
|
2022-09-26 01:44:24 +00:00
|
|
|
matches!(self, Self::Mp4 | Self::Webm | Self::Gif)
|
2022-09-26 01:39:09 +00:00
|
|
|
}
|
|
|
|
|
2023-02-04 23:32:36 +00:00
|
|
|
const fn video_hint(self) -> Option<&'static str> {
|
2022-09-25 23:16:37 +00:00
|
|
|
match self {
|
|
|
|
Self::Mp4 => Some(".mp4"),
|
|
|
|
Self::Webm => Some(".webm"),
|
|
|
|
Self::Gif => Some(".gif"),
|
|
|
|
_ => None,
|
|
|
|
}
|
2021-10-23 19:14:12 +00:00
|
|
|
}
|
|
|
|
|
2023-02-04 23:32:36 +00:00
|
|
|
pub(crate) const fn from_video_codec(codec: VideoCodec) -> Self {
|
2022-10-01 01:00:14 +00:00
|
|
|
match codec {
|
|
|
|
VideoCodec::Av1 | VideoCodec::Vp8 | VideoCodec::Vp9 => Self::Webm,
|
|
|
|
VideoCodec::H264 | VideoCodec::H265 => Self::Mp4,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-04 23:32:36 +00:00
|
|
|
pub(crate) const fn from_format(format: ImageFormat) -> Self {
|
2021-10-23 17:35:07 +00:00
|
|
|
match format {
|
2023-06-21 22:05:35 +00:00
|
|
|
ImageFormat::Avif => ValidInputType::Avif,
|
2022-03-28 04:27:07 +00:00
|
|
|
ImageFormat::Jpeg => ValidInputType::Jpeg,
|
2023-06-21 22:05:35 +00:00
|
|
|
ImageFormat::Jxl => ValidInputType::Jxl,
|
2022-03-28 04:27:07 +00:00
|
|
|
ImageFormat::Png => ValidInputType::Png,
|
|
|
|
ImageFormat::Webp => ValidInputType::Webp,
|
2021-10-23 17:35:07 +00:00
|
|
|
}
|
|
|
|
}
|
2022-09-25 20:17:33 +00:00
|
|
|
|
2023-02-04 23:32:36 +00:00
|
|
|
pub(crate) const fn to_format(self) -> Option<ImageFormat> {
|
2022-09-25 20:17:33 +00:00
|
|
|
match self {
|
2023-06-21 22:05:35 +00:00
|
|
|
Self::Avif => Some(ImageFormat::Avif),
|
2022-09-25 20:17:33 +00:00
|
|
|
Self::Jpeg => Some(ImageFormat::Jpeg),
|
2023-06-21 22:05:35 +00:00
|
|
|
Self::Jxl => Some(ImageFormat::Jxl),
|
2022-09-25 20:17:33 +00:00
|
|
|
Self::Png => Some(ImageFormat::Png),
|
|
|
|
Self::Webp => Some(ImageFormat::Webp),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
2021-10-23 04:48:56 +00:00
|
|
|
}
|
|
|
|
|
2023-07-09 19:50:58 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
2021-08-29 03:05:49 +00:00
|
|
|
pub(crate) struct Details {
|
|
|
|
pub(crate) mime_type: mime::Mime,
|
|
|
|
pub(crate) width: usize,
|
|
|
|
pub(crate) height: usize,
|
2022-09-25 22:36:07 +00:00
|
|
|
pub(crate) frames: Option<usize>,
|
2021-08-29 03:05:49 +00:00
|
|
|
}
|
|
|
|
|
2022-10-02 02:17:18 +00:00
|
|
|
#[tracing::instrument(level = "debug", skip(input))]
|
2021-10-23 19:14:12 +00:00
|
|
|
pub(crate) fn convert_bytes_read(
|
|
|
|
input: Bytes,
|
2022-03-28 04:27:07 +00:00
|
|
|
format: ImageFormat,
|
2023-07-10 20:29:41 +00:00
|
|
|
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
2021-10-23 19:14:12 +00:00
|
|
|
let process = Process::run(
|
|
|
|
"magick",
|
|
|
|
&[
|
|
|
|
"convert",
|
|
|
|
"-",
|
2022-10-15 16:13:24 +00:00
|
|
|
"-auto-orient",
|
2021-10-23 19:14:12 +00:00
|
|
|
"-strip",
|
2021-10-28 04:06:03 +00:00
|
|
|
format!("{}:-", format.as_magick_format()).as_str(),
|
2021-10-23 19:14:12 +00:00
|
|
|
],
|
2023-07-10 20:29:41 +00:00
|
|
|
)
|
|
|
|
.map_err(MagickError::Process)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
Ok(process.bytes_read(input))
|
2021-10-23 19:14:12 +00:00
|
|
|
}
|
|
|
|
|
2022-10-02 02:17:18 +00:00
|
|
|
#[tracing::instrument(skip(input))]
|
2021-10-23 04:48:56 +00:00
|
|
|
pub(crate) async fn details_bytes(
|
|
|
|
input: Bytes,
|
|
|
|
hint: Option<ValidInputType>,
|
2023-07-10 20:29:41 +00:00
|
|
|
) -> Result<Details, MagickError> {
|
2022-09-25 23:16:37 +00:00
|
|
|
if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
|
|
|
|
let input_file = crate::tmp_file::tmp_file(Some(hint));
|
2023-07-10 20:29:41 +00:00
|
|
|
let input_file_str = input_file.to_str().ok_or(MagickError::Path)?;
|
2023-06-20 20:59:08 +00:00
|
|
|
crate::store::file_store::safe_create_parent(&input_file)
|
|
|
|
.await
|
2023-07-10 20:29:41 +00:00
|
|
|
.map_err(MagickError::CreateDir)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
let mut tmp_one = crate::file::File::create(&input_file)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::CreateFile)?;
|
|
|
|
tmp_one
|
|
|
|
.write_from_bytes(input)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::Write)?;
|
|
|
|
tmp_one.close().await.map_err(MagickError::CloseFile)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
|
|
|
|
return details_file(input_file_str).await;
|
|
|
|
}
|
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
let last_arg = if let Some(expected_format) = hint {
|
2021-10-28 04:06:03 +00:00
|
|
|
format!("{}:-", expected_format.as_str())
|
2021-10-23 04:48:56 +00:00
|
|
|
} else {
|
|
|
|
"-".to_owned()
|
|
|
|
};
|
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])
|
|
|
|
.map_err(MagickError::Process)?;
|
2021-09-04 00:53:53 +00:00
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
let mut reader = process.bytes_read(input);
|
2021-09-04 00:53:53 +00:00
|
|
|
|
|
|
|
let mut bytes = Vec::new();
|
2023-07-10 20:29:41 +00:00
|
|
|
reader
|
|
|
|
.read_to_end(&mut bytes)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::Read)?;
|
2021-09-04 00:53:53 +00:00
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
let details_output: Vec<DetailsOutput> =
|
|
|
|
serde_json::from_slice(&bytes).map_err(MagickError::Json)?;
|
2023-07-09 19:50:58 +00:00
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
parse_details(details_output).map_err(MagickError::ParseDetails)
|
2023-07-09 19:50:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct DetailsOutput {
|
|
|
|
image: Image,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct Image {
|
|
|
|
format: String,
|
|
|
|
geometry: Geometry,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, serde::Deserialize)]
|
|
|
|
struct Geometry {
|
|
|
|
width: usize,
|
|
|
|
height: usize,
|
2021-08-31 02:19:47 +00:00
|
|
|
}
|
2021-08-29 19:16:55 +00:00
|
|
|
|
2022-03-27 01:45:12 +00:00
|
|
|
#[tracing::instrument(skip(store))]
|
2022-04-01 21:51:12 +00:00
|
|
|
pub(crate) async fn details_store<S: Store + 'static>(
|
2021-10-23 04:48:56 +00:00
|
|
|
store: S,
|
|
|
|
identifier: S::Identifier,
|
2021-10-23 19:14:12 +00:00
|
|
|
hint: Option<ValidInputType>,
|
2023-07-10 20:29:41 +00:00
|
|
|
) -> Result<Details, MagickError> {
|
2022-09-25 23:16:37 +00:00
|
|
|
if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
|
|
|
|
let input_file = crate::tmp_file::tmp_file(Some(hint));
|
2023-07-10 20:29:41 +00:00
|
|
|
let input_file_str = input_file.to_str().ok_or(MagickError::Path)?;
|
2023-06-20 20:59:08 +00:00
|
|
|
crate::store::file_store::safe_create_parent(&input_file)
|
|
|
|
.await
|
2023-07-10 20:29:41 +00:00
|
|
|
.map_err(MagickError::CreateDir)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
let mut tmp_one = crate::file::File::create(&input_file)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::CreateFile)?;
|
|
|
|
let stream = store
|
|
|
|
.to_stream(&identifier, None, None)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::Store)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
tmp_one
|
2023-07-10 20:29:41 +00:00
|
|
|
.write_from_stream(stream)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::Write)?;
|
|
|
|
tmp_one.close().await.map_err(MagickError::CloseFile)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
|
|
|
|
return details_file(input_file_str).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
let last_arg = if let Some(expected_format) = hint {
|
2021-10-28 04:06:03 +00:00
|
|
|
format!("{}:-", expected_format.as_str())
|
2021-10-23 04:48:56 +00:00
|
|
|
} else {
|
|
|
|
"-".to_owned()
|
|
|
|
};
|
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])
|
|
|
|
.map_err(MagickError::Process)?;
|
2021-10-23 04:48:56 +00:00
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
let mut reader = process.store_read(store, identifier);
|
2021-09-14 01:22:42 +00:00
|
|
|
|
2021-10-23 04:48:56 +00:00
|
|
|
let mut output = Vec::new();
|
2023-07-10 20:29:41 +00:00
|
|
|
reader
|
|
|
|
.read_to_end(&mut output)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::Read)?;
|
2021-08-29 03:05:49 +00:00
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
let details_output: Vec<DetailsOutput> =
|
|
|
|
serde_json::from_slice(&output).map_err(MagickError::Json)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
parse_details(details_output).map_err(MagickError::ParseDetails)
|
2021-10-23 19:14:12 +00:00
|
|
|
}
|
|
|
|
|
2022-03-27 01:45:12 +00:00
|
|
|
#[tracing::instrument]
|
2023-07-10 20:29:41 +00:00
|
|
|
pub(crate) async fn details_file(path_str: &str) -> Result<Details, MagickError> {
|
|
|
|
let process = Process::run("magick", &["convert", "-ping", path_str, "JSON:"])
|
|
|
|
.map_err(MagickError::Process)?;
|
2021-10-23 19:14:12 +00:00
|
|
|
|
2022-04-07 02:40:49 +00:00
|
|
|
let mut reader = process.read();
|
2021-10-23 19:14:12 +00:00
|
|
|
|
|
|
|
let mut output = Vec::new();
|
2023-07-10 20:29:41 +00:00
|
|
|
reader
|
|
|
|
.read_to_end(&mut output)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::Read)?;
|
|
|
|
tokio::fs::remove_file(path_str)
|
|
|
|
.await
|
|
|
|
.map_err(MagickError::RemoveFile)?;
|
|
|
|
|
|
|
|
let details_output: Vec<DetailsOutput> =
|
|
|
|
serde_json::from_slice(&output).map_err(MagickError::Json)?;
|
|
|
|
|
|
|
|
parse_details(details_output).map_err(MagickError::ParseDetails)
|
2023-07-09 19:50:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
pub(crate) enum ParseDetailsError {
|
|
|
|
#[error("No frames present in image")]
|
|
|
|
NoFrames,
|
|
|
|
|
|
|
|
#[error("Multiple image formats used in same file")]
|
|
|
|
MixedFormats,
|
2021-08-29 03:05:49 +00:00
|
|
|
|
2023-07-09 19:50:58 +00:00
|
|
|
#[error("Format is unsupported: {0}")]
|
|
|
|
Unsupported(String),
|
2023-07-09 20:07:49 +00:00
|
|
|
|
|
|
|
#[error("Could not parse frame count from {0}")]
|
|
|
|
ParseFrames(String),
|
2021-08-31 02:19:47 +00:00
|
|
|
}
|
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
fn parse_details(details_output: Vec<DetailsOutput>) -> Result<Details, ParseDetailsError> {
|
2023-07-09 19:50:58 +00:00
|
|
|
let frames = details_output.len();
|
|
|
|
|
|
|
|
if frames == 0 {
|
2023-07-10 20:29:41 +00:00
|
|
|
return Err(ParseDetailsError::NoFrames);
|
2023-07-09 19:50:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let width = details_output
|
|
|
|
.iter()
|
|
|
|
.map(|details| details.image.geometry.width)
|
|
|
|
.max()
|
|
|
|
.expect("Nonempty vector");
|
|
|
|
let height = details_output
|
|
|
|
.iter()
|
|
|
|
.map(|details| details.image.geometry.height)
|
|
|
|
.max()
|
|
|
|
.expect("Nonempty vector");
|
|
|
|
|
|
|
|
let format = details_output[0].image.format.as_str();
|
2021-08-29 03:05:49 +00:00
|
|
|
tracing::debug!("format: {}", format);
|
|
|
|
|
2023-07-09 19:50:58 +00:00
|
|
|
if !details_output
|
|
|
|
.iter()
|
2023-07-09 20:07:49 +00:00
|
|
|
.all(|details| details.image.format == format)
|
2023-07-09 19:50:58 +00:00
|
|
|
{
|
2023-07-10 20:29:41 +00:00
|
|
|
return Err(ParseDetailsError::MixedFormats);
|
2021-08-29 03:05:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let mime_type = match format {
|
2021-10-23 17:35:07 +00:00
|
|
|
"MP4" => video_mp4(),
|
2022-09-25 23:16:37 +00:00
|
|
|
"WEBM" => video_webm(),
|
2021-08-29 03:05:49 +00:00
|
|
|
"GIF" => mime::IMAGE_GIF,
|
2023-06-21 22:05:35 +00:00
|
|
|
"AVIF" => image_avif(),
|
2021-08-29 03:05:49 +00:00
|
|
|
"JPEG" => mime::IMAGE_JPEG,
|
2023-06-21 22:05:35 +00:00
|
|
|
"JXL" => image_jxl(),
|
|
|
|
"PNG" => mime::IMAGE_PNG,
|
2021-10-23 17:35:07 +00:00
|
|
|
"WEBP" => image_webp(),
|
2023-07-10 20:29:41 +00:00
|
|
|
e => return Err(ParseDetailsError::Unsupported(String::from(e))),
|
2021-08-29 03:05:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(Details {
|
|
|
|
mime_type,
|
|
|
|
width,
|
|
|
|
height,
|
2022-09-26 01:39:09 +00:00
|
|
|
frames: if frames > 1 { Some(frames) } else { None },
|
2021-08-29 03:05:49 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
pub(crate) async fn input_type_bytes(
|
|
|
|
input: Bytes,
|
|
|
|
) -> Result<(Details, ValidInputType), MagickError> {
|
2023-02-04 23:32:36 +00:00
|
|
|
let details = details_bytes(input, None).await?;
|
2023-07-10 20:29:41 +00:00
|
|
|
let input_type = details
|
|
|
|
.validate_input()
|
|
|
|
.map_err(MagickError::ValidateDetails)?;
|
2023-02-04 23:32:36 +00:00
|
|
|
Ok((details, input_type))
|
2021-08-28 22:15:14 +00:00
|
|
|
}
|
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
fn process_image(process_args: Vec<String>, format: ImageFormat) -> Result<Process, ProcessError> {
|
2021-09-14 01:22:42 +00:00
|
|
|
let command = "magick";
|
|
|
|
let convert_args = ["convert", "-"];
|
2021-10-28 04:06:03 +00:00
|
|
|
let last_arg = format!("{}:-", format.as_magick_format());
|
2021-09-14 01:22:42 +00:00
|
|
|
|
2023-07-10 20:29:41 +00:00
|
|
|
let mut args = Vec::with_capacity(process_args.len() + 3);
|
|
|
|
args.extend_from_slice(&convert_args[..]);
|
|
|
|
args.extend(process_args.iter().map(|s| s.as_str()));
|
|
|
|
args.push(&last_arg);
|
|
|
|
|
|
|
|
Process::run(command, &args)
|
2022-09-25 20:17:33 +00:00
|
|
|
}
|
2021-08-31 02:19:47 +00:00
|
|
|
|
2022-09-25 20:17:33 +00:00
|
|
|
pub(crate) fn process_image_store_read<S: Store + 'static>(
|
|
|
|
store: S,
|
|
|
|
identifier: S::Identifier,
|
|
|
|
args: Vec<String>,
|
|
|
|
format: ImageFormat,
|
2023-07-10 20:29:41 +00:00
|
|
|
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
|
|
|
Ok(process_image(args, format)
|
|
|
|
.map_err(MagickError::Process)?
|
|
|
|
.store_read(store, identifier))
|
2022-09-25 20:17:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn process_image_async_read<A: AsyncRead + Unpin + 'static>(
|
|
|
|
async_read: A,
|
|
|
|
args: Vec<String>,
|
|
|
|
format: ImageFormat,
|
2023-07-10 20:29:41 +00:00
|
|
|
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
|
|
|
Ok(process_image(args, format)
|
|
|
|
.map_err(MagickError::Process)?
|
|
|
|
.pipe_async_read(async_read))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
pub(crate) enum ValidateDetailsError {
|
|
|
|
#[error("Exceeded maximum dimensions")]
|
|
|
|
ExceededDimensions,
|
|
|
|
|
|
|
|
#[error("Exceeded maximum frame count")]
|
|
|
|
TooManyFrames,
|
|
|
|
|
|
|
|
#[error("Unsupported media type: {0}")]
|
|
|
|
UnsupportedMediaType(String),
|
2021-08-26 02:46:11 +00:00
|
|
|
}
|
|
|
|
|
2021-09-09 19:16:12 +00:00
|
|
|
impl Details {
|
2022-10-02 02:17:18 +00:00
|
|
|
#[tracing::instrument(level = "debug", name = "Validating input type")]
|
2023-07-10 20:29:41 +00:00
|
|
|
pub(crate) fn validate_input(&self) -> Result<ValidInputType, ValidateDetailsError> {
|
2022-03-28 04:27:07 +00:00
|
|
|
if self.width > crate::CONFIG.media.max_width
|
|
|
|
|| self.height > crate::CONFIG.media.max_height
|
|
|
|
|| self.width * self.height > crate::CONFIG.media.max_area
|
2021-10-28 04:06:03 +00:00
|
|
|
{
|
2023-07-10 20:29:41 +00:00
|
|
|
return Err(ValidateDetailsError::ExceededDimensions);
|
2021-09-09 19:16:12 +00:00
|
|
|
}
|
|
|
|
|
2022-09-25 22:36:07 +00:00
|
|
|
if let Some(frames) = self.frames {
|
|
|
|
if frames > crate::CONFIG.media.max_frame_count {
|
2023-07-10 20:29:41 +00:00
|
|
|
return Err(ValidateDetailsError::TooManyFrames);
|
2022-09-25 22:36:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-09 19:16:12 +00:00
|
|
|
let input_type = match (self.mime_type.type_(), self.mime_type.subtype()) {
|
|
|
|
(mime::VIDEO, mime::MP4 | mime::MPEG) => ValidInputType::Mp4,
|
2022-09-25 23:16:37 +00:00
|
|
|
(mime::VIDEO, subtype) if subtype.as_str() == "webm" => ValidInputType::Webm,
|
2021-09-09 19:16:12 +00:00
|
|
|
(mime::IMAGE, mime::GIF) => ValidInputType::Gif,
|
2023-06-21 22:05:35 +00:00
|
|
|
(mime::IMAGE, subtype) if subtype.as_str() == "avif" => ValidInputType::Avif,
|
2021-09-09 19:16:12 +00:00
|
|
|
(mime::IMAGE, mime::JPEG) => ValidInputType::Jpeg,
|
2023-06-21 22:05:35 +00:00
|
|
|
(mime::IMAGE, subtype) if subtype.as_str() == "jxl" => ValidInputType::Jxl,
|
|
|
|
(mime::IMAGE, mime::PNG) => ValidInputType::Png,
|
2021-09-09 19:16:12 +00:00
|
|
|
(mime::IMAGE, subtype) if subtype.as_str() == "webp" => ValidInputType::Webp,
|
2023-07-10 20:29:41 +00:00
|
|
|
_ => {
|
|
|
|
return Err(ValidateDetailsError::UnsupportedMediaType(
|
|
|
|
self.mime_type.to_string(),
|
|
|
|
))
|
|
|
|
}
|
2021-09-09 19:16:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(input_type)
|
|
|
|
}
|
|
|
|
}
|