pict-rs/src/magick.rs

395 lines
11 KiB
Rust
Raw Normal View History

#[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},
2021-09-14 01:22:42 +00:00
error::{Error, UploadError},
2021-10-21 00:00:41 +00:00
process::Process,
2022-03-26 21:49:23 +00:00
repo::Alias,
store::{Store, StoreError},
2021-09-14 01:22:42 +00:00
};
use actix_web::web::Bytes;
use tokio::{
io::{AsyncRead, AsyncReadExt},
process::Command,
};
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)
} 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()
}
fn image_webp() -> mime::Mime {
2021-10-23 17:35:07 +00:00
"image/webp".parse().unwrap()
}
pub(crate) fn video_mp4() -> mime::Mime {
2021-10-23 17:35:07 +00:00
"video/mp4".parse().unwrap()
}
pub(crate) fn video_webm() -> mime::Mime {
"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,
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,
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",
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",
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)
}
2023-02-04 23:32:36 +00:00
const fn video_hint(self) -> Option<&'static str> {
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
}
}
2023-02-04 23:32:36 +00:00
pub(crate) const fn to_format(self) -> Option<ImageFormat> {
match self {
2023-06-21 22:05:35 +00:00
Self::Avif => Some(ImageFormat::Avif),
Self::Jpeg => Some(ImageFormat::Jpeg),
2023-06-21 22:05:35 +00:00
Self::Jxl => Some(ImageFormat::Jxl),
Self::Png => Some(ImageFormat::Png),
Self::Webp => Some(ImageFormat::Webp),
_ => None,
}
}
2021-10-23 04:48:56 +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
}
#[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,
2021-10-23 19:14:12 +00:00
) -> std::io::Result<impl AsyncRead + Unpin> {
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",
format!("{}:-", format.as_magick_format()).as_str(),
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
}
#[tracing::instrument(skip(input))]
2021-10-23 04:48:56 +00:00
pub(crate) async fn details_bytes(
input: Bytes,
hint: Option<ValidInputType>,
) -> Result<Details, Error> {
if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
let input_file = crate::tmp_file::tmp_file(Some(hint));
2021-10-23 19:14:12 +00:00
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
crate::store::file_store::safe_create_parent(&input_file)
.await
.map_err(StoreError::from)?;
2021-10-23 19:14:12 +00:00
let mut tmp_one = crate::file::File::create(&input_file).await?;
tmp_one.write_from_bytes(input).await?;
tmp_one.close().await?;
return details_file(input_file_str).await;
}
2021-10-23 04:48:56 +00:00
let last_arg = if let Some(expected_format) = hint {
format!("{}:-", expected_format.as_str())
2021-10-23 04:48:56 +00:00
} else {
"-".to_owned()
};
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])?;
2022-04-07 02:40:49 +00:00
let mut reader = process.bytes_read(input);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let details_output: Vec<DetailsOutput> = serde_json::from_slice(&bytes)?;
parse_details(details_output)
}
#[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,
}
#[tracing::instrument(skip(store))]
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>,
) -> Result<Details, Error> {
if let Some(hint) = hint.and_then(|hint| hint.video_hint()) {
let input_file = crate::tmp_file::tmp_file(Some(hint));
2021-10-23 19:14:12 +00:00
let input_file_str = input_file.to_str().ok_or(UploadError::Path)?;
crate::store::file_store::safe_create_parent(&input_file)
.await
.map_err(StoreError::from)?;
2021-10-23 19:14:12 +00:00
let mut tmp_one = crate::file::File::create(&input_file).await?;
tmp_one
.write_from_stream(store.to_stream(&identifier, None, None).await?)
.await?;
tmp_one.close().await?;
return details_file(input_file_str).await;
}
let last_arg = if let Some(expected_format) = hint {
format!("{}:-", expected_format.as_str())
2021-10-23 04:48:56 +00:00
} else {
"-".to_owned()
};
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])?;
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();
reader.read_to_end(&mut output).await?;
2021-08-29 03:05:49 +00:00
let details_output: Vec<DetailsOutput> = serde_json::from_slice(&output)?;
2021-10-23 19:14:12 +00:00
parse_details(details_output)
2021-10-23 19:14:12 +00:00
}
#[tracing::instrument]
2021-10-23 19:14:12 +00:00
pub(crate) async fn details_file(path_str: &str) -> Result<Details, Error> {
let process = Process::run("magick", &["convert", "-ping", path_str, "JSON:"])?;
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();
reader.read_to_end(&mut output).await?;
tokio::fs::remove_file(path_str).await?;
let details_output: Vec<DetailsOutput> = serde_json::from_slice(&output)?;
parse_details(details_output)
}
#[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
#[error("Format is unsupported: {0}")]
Unsupported(String),
#[error("Could not parse frame count from {0}")]
ParseFrames(String),
}
fn parse_details(details_output: Vec<DetailsOutput>) -> Result<Details, Error> {
let frames = details_output.len();
if frames == 0 {
return Err(ParseDetailsError::NoFrames.into());
}
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);
if !details_output
.iter()
.all(|details| details.image.format == format)
{
return Err(ParseDetailsError::MixedFormats.into());
2021-08-29 03:05:49 +00:00
}
let mime_type = match format {
2021-10-23 17:35:07 +00:00
"MP4" => video_mp4(),
"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(),
e => return Err(ParseDetailsError::Unsupported(String::from(e)).into()),
2021-08-29 03:05:49 +00:00
};
Ok(Details {
mime_type,
width,
height,
frames: if frames > 1 { Some(frames) } else { None },
2021-08-29 03:05:49 +00:00
})
}
2023-02-04 23:32:36 +00:00
pub(crate) async fn input_type_bytes(input: Bytes) -> Result<(Details, ValidInputType), Error> {
let details = details_bytes(input, None).await?;
let input_type = details.validate_input()?;
Ok((details, input_type))
}
fn process_image(args: Vec<String>, format: ImageFormat) -> std::io::Result<Process> {
2021-09-14 01:22:42 +00:00
let command = "magick";
let convert_args = ["convert", "-"];
let last_arg = format!("{}:-", format.as_magick_format());
2021-09-14 01:22:42 +00:00
Process::spawn(
2021-09-14 01:22:42 +00:00
Command::new(command)
.args(convert_args)
.args(args)
2021-09-14 01:22:42 +00:00
.arg(last_arg),
)
}
pub(crate) fn process_image_store_read<S: Store + 'static>(
store: S,
identifier: S::Identifier,
args: Vec<String>,
format: ImageFormat,
) -> std::io::Result<impl AsyncRead + Unpin> {
Ok(process_image(args, format)?.store_read(store, identifier))
}
pub(crate) fn process_image_async_read<A: AsyncRead + Unpin + 'static>(
async_read: A,
args: Vec<String>,
format: ImageFormat,
) -> std::io::Result<impl AsyncRead + Unpin> {
Ok(process_image(args, format)?.pipe_async_read(async_read))
}
impl Details {
#[tracing::instrument(level = "debug", name = "Validating input type")]
pub(crate) fn validate_input(&self) -> Result<ValidInputType, Error> {
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-09-14 01:22:42 +00:00
return Err(UploadError::Dimensions.into());
}
2022-09-25 22:36:07 +00:00
if let Some(frames) = self.frames {
if frames > crate::CONFIG.media.max_frame_count {
return Err(UploadError::Frames.into());
}
}
let input_type = match (self.mime_type.type_(), self.mime_type.subtype()) {
(mime::VIDEO, mime::MP4 | mime::MPEG) => ValidInputType::Mp4,
(mime::VIDEO, subtype) if subtype.as_str() == "webm" => ValidInputType::Webm,
(mime::IMAGE, mime::GIF) => ValidInputType::Gif,
2023-06-21 22:05:35 +00:00
(mime::IMAGE, subtype) if subtype.as_str() == "avif" => ValidInputType::Avif,
(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,
(mime::IMAGE, subtype) if subtype.as_str() == "webp" => ValidInputType::Webp,
_ => return Err(ParseDetailsError::Unsupported(self.mime_type.to_string()).into()),
};
Ok(input_type)
}
}