mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-22 19:31:35 +00:00
VERY BROKEN: start replacing parts of pict-rs
This commit is contained in:
parent
58d9765594
commit
ad1837f9dd
23 changed files with 1748 additions and 648 deletions
|
@ -1,12 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
ffmpeg::VideoFormat,
|
formats::{InternalFormat, InternalVideoFormat},
|
||||||
magick::{video_mp4, video_webm, ValidInputType},
|
magick::{video_mp4, video_webm, ValidInputType},
|
||||||
serde_str::Serde,
|
serde_str::Serde,
|
||||||
store::Store,
|
store::Store,
|
||||||
};
|
};
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use time::format_description::well_known::Rfc3339;
|
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Copy, Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
|
@ -22,6 +22,8 @@ pub(crate) struct Details {
|
||||||
frames: Option<usize>,
|
frames: Option<usize>,
|
||||||
content_type: Serde<mime::Mime>,
|
content_type: Serde<mime::Mime>,
|
||||||
created_at: MaybeHumanDate,
|
created_at: MaybeHumanDate,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
format: Option<InternalFormat>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Details {
|
impl Details {
|
||||||
|
@ -88,6 +90,7 @@ impl Details {
|
||||||
frames,
|
frames,
|
||||||
content_type: Serde::new(content_type),
|
content_type: Serde::new(content_type),
|
||||||
created_at: MaybeHumanDate::HumanDate(time::OffsetDateTime::now_utc()),
|
created_at: MaybeHumanDate::HumanDate(time::OffsetDateTime::now_utc()),
|
||||||
|
format: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,21 +102,33 @@ impl Details {
|
||||||
self.created_at.into()
|
self.created_at.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_input_format(&self) -> Option<VideoFormat> {
|
pub(crate) fn input_format(&self) -> Option<InternalVideoFormat> {
|
||||||
if *self.content_type == mime::IMAGE_GIF {
|
|
||||||
return Some(VideoFormat::Gif);
|
|
||||||
}
|
|
||||||
|
|
||||||
if *self.content_type == video_mp4() {
|
if *self.content_type == video_mp4() {
|
||||||
return Some(VideoFormat::Mp4);
|
return Some(InternalVideoFormat::Mp4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if *self.content_type == video_webm() {
|
if *self.content_type == video_webm() {
|
||||||
return Some(VideoFormat::Webm);
|
return Some(InternalVideoFormat::Webm);
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_parts(
|
||||||
|
format: InternalFormat,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
frames: Option<u32>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
width: width.into(),
|
||||||
|
height: height.into(),
|
||||||
|
frames: frames.map(|f| f.try_into().expect("Reasonable size")),
|
||||||
|
content_type: Serde::new(format.media_type()),
|
||||||
|
created_at: MaybeHumanDate::HumanDate(OffsetDateTime::now_utc()),
|
||||||
|
format: Some(format),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MaybeHumanDate> for std::time::SystemTime {
|
impl From<MaybeHumanDate> for std::time::SystemTime {
|
||||||
|
|
40
src/discover.rs
Normal file
40
src/discover.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
mod exiftool;
|
||||||
|
mod ffmpeg;
|
||||||
|
mod magick;
|
||||||
|
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
|
||||||
|
use crate::formats::{AnimationInput, ImageInput, InputFile, InternalFormat, InternalVideoFormat};
|
||||||
|
|
||||||
|
pub(crate) struct Discovery {
|
||||||
|
pub(crate) input: InputFile,
|
||||||
|
pub(crate) width: u16,
|
||||||
|
pub(crate) height: u16,
|
||||||
|
pub(crate) frames: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Discovery {
|
||||||
|
pub(crate) fn internal_format(&self) -> InternalFormat {
|
||||||
|
match self.input {
|
||||||
|
InputFile::Image(ImageInput { format, .. }) => InternalFormat::Image(format),
|
||||||
|
InputFile::Animation(AnimationInput { format }) => InternalFormat::Animation(format),
|
||||||
|
// we're making up bs now lol
|
||||||
|
InputFile::Video(crate::formats::VideoFormat::Mp4) => {
|
||||||
|
InternalFormat::Video(InternalVideoFormat::Mp4)
|
||||||
|
}
|
||||||
|
InputFile::Video(crate::formats::VideoFormat::Webm { .. }) => {
|
||||||
|
InternalFormat::Video(InternalVideoFormat::Webm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn discover_bytes(bytes: Bytes) -> Result<Discovery, crate::error::Error> {
|
||||||
|
let discovery = ffmpeg::discover_bytes(bytes.clone()).await?;
|
||||||
|
|
||||||
|
let discovery = magick::confirm_bytes(discovery, bytes.clone()).await?;
|
||||||
|
|
||||||
|
let discovery = exiftool::check_reorient(discovery, bytes).await?;
|
||||||
|
|
||||||
|
Ok(discovery)
|
||||||
|
}
|
54
src/discover/exiftool.rs
Normal file
54
src/discover/exiftool.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
exiftool::ExifError,
|
||||||
|
formats::{ImageInput, InputFile},
|
||||||
|
process::Process,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Discovery;
|
||||||
|
|
||||||
|
pub(super) async fn check_reorient(
|
||||||
|
Discovery {
|
||||||
|
input,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}: Discovery,
|
||||||
|
bytes: Bytes,
|
||||||
|
) -> Result<Discovery, ExifError> {
|
||||||
|
let input = match input {
|
||||||
|
InputFile::Image(ImageInput { format, .. }) => {
|
||||||
|
let needs_reorient = needs_reorienting(bytes).await?;
|
||||||
|
|
||||||
|
InputFile::Image(ImageInput {
|
||||||
|
format,
|
||||||
|
needs_reorient,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
otherwise => otherwise,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Discovery {
|
||||||
|
input,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(input))]
|
||||||
|
async fn needs_reorienting(input: Bytes) -> Result<bool, ExifError> {
|
||||||
|
let process =
|
||||||
|
Process::run("exiftool", &["-n", "-Orientation", "-"]).map_err(ExifError::Process)?;
|
||||||
|
let mut reader = process.bytes_read(input);
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
reader
|
||||||
|
.read_to_string(&mut buf)
|
||||||
|
.await
|
||||||
|
.map_err(ExifError::Read)?;
|
||||||
|
|
||||||
|
Ok(!buf.is_empty())
|
||||||
|
}
|
297
src/discover/ffmpeg.rs
Normal file
297
src/discover/ffmpeg.rs
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
use std::{collections::HashSet, sync::OnceLock};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ffmpeg::FfMpegError,
|
||||||
|
formats::{AnimationFormat, AnimationInput, ImageFormat, ImageInput, InputFile, VideoFormat},
|
||||||
|
process::Process,
|
||||||
|
};
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
use super::Discovery;
|
||||||
|
|
||||||
|
const FFMPEG_FORMAT_MAPPINGS: &[(&str, InputFile)] = &[
|
||||||
|
(
|
||||||
|
"apng",
|
||||||
|
InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Apng,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"gif",
|
||||||
|
InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Gif,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
("mp4", InputFile::Video(VideoFormat::Mp4)),
|
||||||
|
(
|
||||||
|
"png_pipe",
|
||||||
|
InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Png,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
("webm", InputFile::Video(VideoFormat::Webm { alpha: false })),
|
||||||
|
(
|
||||||
|
"webp_pipe",
|
||||||
|
InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Webp,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct FfMpegDiscovery {
|
||||||
|
streams: [FfMpegStream; 1],
|
||||||
|
format: FfMpegFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct FfMpegStream {
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
nb_read_frames: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct FfMpegFormat {
|
||||||
|
format_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct PixelFormatOutput {
|
||||||
|
pixel_formats: Vec<PixelFormat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct PixelFormat {
|
||||||
|
name: String,
|
||||||
|
flags: Flags,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct Flags {
|
||||||
|
alpha: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn discover_bytes(bytes: Bytes) -> Result<Option<Discovery>, FfMpegError> {
|
||||||
|
discover_file(move |mut file| async move {
|
||||||
|
file.write_from_bytes(bytes)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Write)?;
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(f))]
|
||||||
|
async fn discover_file<F, Fut>(f: F) -> Result<Option<Discovery>, FfMpegError>
|
||||||
|
where
|
||||||
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<crate::file::File, FfMpegError>>,
|
||||||
|
{
|
||||||
|
let input_file = crate::tmp_file::tmp_file(None);
|
||||||
|
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
||||||
|
crate::store::file_store::safe_create_parent(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::CreateDir)?;
|
||||||
|
|
||||||
|
let tmp_one = crate::file::File::create(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::CreateFile)?;
|
||||||
|
let tmp_one = (f)(tmp_one).await?;
|
||||||
|
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
||||||
|
|
||||||
|
let process = Process::run(
|
||||||
|
"ffprobe",
|
||||||
|
&[
|
||||||
|
"-v",
|
||||||
|
"quiet",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-count_frames",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=width,height,nb_read_frames:format=format_name",
|
||||||
|
"-of",
|
||||||
|
"default=noprint_wrappers=1:nokey=1",
|
||||||
|
"-print_format",
|
||||||
|
"json",
|
||||||
|
input_file_str,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(FfMpegError::Process)?;
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
process
|
||||||
|
.read()
|
||||||
|
.read_to_end(&mut output)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Read)?;
|
||||||
|
tokio::fs::remove_file(input_file_str)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::RemoveFile)?;
|
||||||
|
|
||||||
|
let output: FfMpegDiscovery = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
||||||
|
|
||||||
|
let Some(discovery) = parse_discovery_ffmpeg(output)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
match discovery {
|
||||||
|
Discovery {
|
||||||
|
input: InputFile::Video(VideoFormat::Webm { .. }),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
} => {
|
||||||
|
static ALPHA_PIXEL_FORMATS: OnceLock<HashSet<String>> = OnceLock::new();
|
||||||
|
|
||||||
|
let format = pixel_format(input_file_str).await?;
|
||||||
|
|
||||||
|
let alpha = match ALPHA_PIXEL_FORMATS.get() {
|
||||||
|
Some(alpha_pixel_formats) => alpha_pixel_formats.contains(&format),
|
||||||
|
None => {
|
||||||
|
let pixel_formats = alpha_pixel_formats().await?;
|
||||||
|
let alpha = pixel_formats.contains(&format);
|
||||||
|
let _ = ALPHA_PIXEL_FORMATS.set(pixel_formats);
|
||||||
|
alpha
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(Discovery {
|
||||||
|
input: InputFile::Video(VideoFormat::Webm { alpha }),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
otherwise => Ok(Some(otherwise)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pixel_format(input_file: &str) -> Result<String, FfMpegError> {
|
||||||
|
let process = Process::run(
|
||||||
|
"ffprobe",
|
||||||
|
&[
|
||||||
|
"-v",
|
||||||
|
"0",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=pix_fmt",
|
||||||
|
"-of",
|
||||||
|
"compact=p=0:nk=1",
|
||||||
|
input_file,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(FfMpegError::Process)?;
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
process
|
||||||
|
.read()
|
||||||
|
.read_to_end(&mut output)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Read)?;
|
||||||
|
Ok(String::from_utf8_lossy(&output).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn alpha_pixel_formats() -> Result<HashSet<String>, FfMpegError> {
|
||||||
|
let process = Process::run(
|
||||||
|
"ffprobe",
|
||||||
|
&[
|
||||||
|
"-v",
|
||||||
|
"0",
|
||||||
|
"-show_entries",
|
||||||
|
"pixel_format=name:flags=alpha",
|
||||||
|
"-of",
|
||||||
|
"compact=p=0",
|
||||||
|
"-print_format",
|
||||||
|
"json",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(FfMpegError::Process)?;
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
process
|
||||||
|
.read()
|
||||||
|
.read_to_end(&mut output)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Read)?;
|
||||||
|
|
||||||
|
let formats: PixelFormatOutput = serde_json::from_slice(&output).map_err(FfMpegError::Json)?;
|
||||||
|
|
||||||
|
Ok(parse_pixel_formats(formats))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
|
||||||
|
formats
|
||||||
|
.pixel_formats
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|PixelFormat { name, flags }| {
|
||||||
|
if flags.alpha == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(name)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_discovery_ffmpeg(discovery: FfMpegDiscovery) -> Result<Option<Discovery>, FfMpegError> {
|
||||||
|
let FfMpegDiscovery {
|
||||||
|
streams:
|
||||||
|
[FfMpegStream {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
nb_read_frames,
|
||||||
|
}],
|
||||||
|
format: FfMpegFormat { format_name },
|
||||||
|
} = discovery;
|
||||||
|
|
||||||
|
if let Some((name, value)) = FFMPEG_FORMAT_MAPPINGS
|
||||||
|
.iter()
|
||||||
|
.find(|(name, _)| format_name.contains(name))
|
||||||
|
{
|
||||||
|
let frames = nb_read_frames.and_then(|frames| frames.parse().ok());
|
||||||
|
|
||||||
|
if *name == "mp4" && frames.map(|nb| nb == 1).unwrap_or(false) {
|
||||||
|
// Might be AVIF, ffmpeg incorrectly detects AVIF as single-framed mp4 even when
|
||||||
|
// animated
|
||||||
|
|
||||||
|
return Ok(Some(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Avif,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if *name == "webp" && (frames.is_none() || width == 0 || height == 0) {
|
||||||
|
// Might be Animated Webp, ffmpeg incorrectly detects animated webp as having no frames
|
||||||
|
// and 0 dimensions
|
||||||
|
|
||||||
|
return Ok(Some(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Webp,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(Some(Discovery {
|
||||||
|
input: value.clone(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("No matching format mapping for {format_name}");
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
307
src/discover/magick.rs
Normal file
307
src/discover/magick.rs
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
formats::{AnimationFormat, AnimationInput, ImageFormat, ImageInput, InputFile, VideoFormat},
|
||||||
|
magick::MagickError,
|
||||||
|
process::Process,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Discovery;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct MagickDiscovery {
|
||||||
|
image: Image,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct Image {
|
||||||
|
format: String,
|
||||||
|
geometry: Geometry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct Geometry {
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn confirm_bytes(
|
||||||
|
discovery: Option<Discovery>,
|
||||||
|
bytes: Bytes,
|
||||||
|
) -> Result<Discovery, MagickError> {
|
||||||
|
match discovery {
|
||||||
|
Some(Discovery {
|
||||||
|
input:
|
||||||
|
InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Avif,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
let frames = count_avif_frames(move |mut file| async move {
|
||||||
|
file.write_from_bytes(bytes)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::Write)?;
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Avif,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: Some(frames),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(Discovery {
|
||||||
|
input:
|
||||||
|
InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Webp,
|
||||||
|
}),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
Some(otherwise) => return Ok(otherwise),
|
||||||
|
None => {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
discover_file(move |mut file| async move {
|
||||||
|
file.write_from_bytes(bytes)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::Write)?;
|
||||||
|
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn count_avif_frames<F, Fut>(f: F) -> Result<u32, MagickError>
|
||||||
|
where
|
||||||
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<crate::file::File, MagickError>>,
|
||||||
|
{
|
||||||
|
let input_file = crate::tmp_file::tmp_file(None);
|
||||||
|
let input_file_str = input_file.to_str().ok_or(MagickError::Path)?;
|
||||||
|
crate::store::file_store::safe_create_parent(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::CreateDir)?;
|
||||||
|
|
||||||
|
let tmp_one = crate::file::File::create(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::CreateFile)?;
|
||||||
|
let tmp_one = (f)(tmp_one).await?;
|
||||||
|
tmp_one.close().await.map_err(MagickError::CloseFile)?;
|
||||||
|
|
||||||
|
let process = Process::run("magick", &["convert", "-ping", input_file_str, "INFO:"])
|
||||||
|
.map_err(MagickError::Process)?;
|
||||||
|
|
||||||
|
let mut output = String::new();
|
||||||
|
process
|
||||||
|
.read()
|
||||||
|
.read_to_string(&mut output)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::Read)?;
|
||||||
|
tokio::fs::remove_file(input_file_str)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::RemoveFile)?;
|
||||||
|
|
||||||
|
let lines: u32 = output
|
||||||
|
.lines()
|
||||||
|
.count()
|
||||||
|
.try_into()
|
||||||
|
.expect("Reasonable frame count");
|
||||||
|
|
||||||
|
if lines == 0 {
|
||||||
|
todo!("Error");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn discover_file<F, Fut>(f: F) -> Result<Discovery, MagickError>
|
||||||
|
where
|
||||||
|
F: FnOnce(crate::file::File) -> Fut,
|
||||||
|
Fut: std::future::Future<Output = Result<crate::file::File, MagickError>>,
|
||||||
|
{
|
||||||
|
let input_file = crate::tmp_file::tmp_file(None);
|
||||||
|
let input_file_str = input_file.to_str().ok_or(MagickError::Path)?;
|
||||||
|
crate::store::file_store::safe_create_parent(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::CreateDir)?;
|
||||||
|
|
||||||
|
let tmp_one = crate::file::File::create(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::CreateFile)?;
|
||||||
|
let tmp_one = (f)(tmp_one).await?;
|
||||||
|
tmp_one.close().await.map_err(MagickError::CloseFile)?;
|
||||||
|
|
||||||
|
let process = Process::run("magick", &["convert", "-ping", input_file_str, "JSON:"])
|
||||||
|
.map_err(MagickError::Process)?;
|
||||||
|
|
||||||
|
let mut output = Vec::new();
|
||||||
|
process
|
||||||
|
.read()
|
||||||
|
.read_to_end(&mut output)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::Read)?;
|
||||||
|
tokio::fs::remove_file(input_file_str)
|
||||||
|
.await
|
||||||
|
.map_err(MagickError::RemoveFile)?;
|
||||||
|
|
||||||
|
let output: Vec<MagickDiscovery> =
|
||||||
|
serde_json::from_slice(&output).map_err(MagickError::Json)?;
|
||||||
|
|
||||||
|
parse_discovery(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_discovery(output: Vec<MagickDiscovery>) -> Result<Discovery, MagickError> {
|
||||||
|
let frames = output.len();
|
||||||
|
|
||||||
|
if frames == 0 {
|
||||||
|
todo!("Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = output
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|MagickDiscovery {
|
||||||
|
image:
|
||||||
|
Image {
|
||||||
|
geometry: Geometry { width, .. },
|
||||||
|
..
|
||||||
|
},
|
||||||
|
}| *width,
|
||||||
|
)
|
||||||
|
.max()
|
||||||
|
.expect("Nonempty vector");
|
||||||
|
|
||||||
|
let height = output
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|MagickDiscovery {
|
||||||
|
image:
|
||||||
|
Image {
|
||||||
|
geometry: Geometry { height, .. },
|
||||||
|
..
|
||||||
|
},
|
||||||
|
}| *height,
|
||||||
|
)
|
||||||
|
.max()
|
||||||
|
.expect("Nonempty vector");
|
||||||
|
|
||||||
|
let first_format = &output[0].image.format;
|
||||||
|
|
||||||
|
if output.iter().any(
|
||||||
|
|MagickDiscovery {
|
||||||
|
image: Image { format, .. },
|
||||||
|
}| format != first_format,
|
||||||
|
) {
|
||||||
|
todo!("Error")
|
||||||
|
}
|
||||||
|
|
||||||
|
let frames: u32 = frames.try_into().expect("Reasonable frame count");
|
||||||
|
|
||||||
|
match first_format.as_str() {
|
||||||
|
"AVIF" => {
|
||||||
|
if frames > 1 {
|
||||||
|
Ok(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Avif,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: Some(frames),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Avif,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"APNG" => Ok(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Apng,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: Some(frames),
|
||||||
|
}),
|
||||||
|
"GIF" => Ok(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Gif,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: Some(frames),
|
||||||
|
}),
|
||||||
|
"JPEG" => Ok(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jpeg,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: None,
|
||||||
|
}),
|
||||||
|
"JXL" => Ok(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Jxl,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: None,
|
||||||
|
}),
|
||||||
|
"MP4" => Ok(Discovery {
|
||||||
|
input: InputFile::Video(VideoFormat::Mp4),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: Some(frames),
|
||||||
|
}),
|
||||||
|
"PNG" => Ok(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Png,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: None,
|
||||||
|
}),
|
||||||
|
"WEBP" => {
|
||||||
|
if frames > 1 {
|
||||||
|
Ok(Discovery {
|
||||||
|
input: InputFile::Animation(AnimationInput {
|
||||||
|
format: AnimationFormat::Webp,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: Some(frames),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(Discovery {
|
||||||
|
input: InputFile::Image(ImageInput {
|
||||||
|
format: ImageFormat::Webp,
|
||||||
|
needs_reorient: false,
|
||||||
|
}),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
frames: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
otherwise => todo!("Error {otherwise}"),
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ mod tests;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AudioCodec, ImageFormat, MediaConfiguration, VideoCodec},
|
config::{AudioCodec, ImageFormat, MediaConfiguration, VideoCodec},
|
||||||
|
formats::InternalVideoFormat,
|
||||||
magick::{Details, ParseDetailsError, ValidInputType, ValidateDetailsError},
|
magick::{Details, ParseDetailsError, ValidInputType, ValidateDetailsError},
|
||||||
process::{Process, ProcessError},
|
process::{Process, ProcessError},
|
||||||
store::{Store, StoreError},
|
store::{Store, StoreError},
|
||||||
|
@ -716,10 +717,10 @@ pub(crate) async fn transcode_bytes(
|
||||||
pub(crate) async fn thumbnail<S: Store>(
|
pub(crate) async fn thumbnail<S: Store>(
|
||||||
store: S,
|
store: S,
|
||||||
from: S::Identifier,
|
from: S::Identifier,
|
||||||
input_format: VideoFormat,
|
input_format: InternalVideoFormat,
|
||||||
format: ThumbnailFormat,
|
format: ThumbnailFormat,
|
||||||
) -> Result<impl AsyncRead + Unpin, FfMpegError> {
|
) -> Result<impl AsyncRead + Unpin, FfMpegError> {
|
||||||
let input_file = crate::tmp_file::tmp_file(Some(input_format.to_file_extension()));
|
let input_file = crate::tmp_file::tmp_file(Some(input_format.file_extension()));
|
||||||
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
||||||
crate::store::file_store::safe_create_parent(&input_file)
|
crate::store::file_store::safe_create_parent(&input_file)
|
||||||
.await
|
.await
|
||||||
|
|
15
src/ffmpeg/ffprobe_6_0_apng_details.json
Normal file
15
src/ffmpeg/ffprobe_6_0_apng_details.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"programs": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"width": 112,
|
||||||
|
"height": 112,
|
||||||
|
"nb_read_frames": "27"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"format_name": "apng"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,16 @@
|
||||||
use super::{Details, DetailsOutput, PixelFormatOutput};
|
use super::{Details, DetailsOutput, PixelFormatOutput};
|
||||||
|
|
||||||
fn details_tests() -> [(&'static str, Option<Details>); 9] {
|
fn details_tests() -> [(&'static str, Option<Details>); 10] {
|
||||||
[
|
[
|
||||||
|
(
|
||||||
|
"apng",
|
||||||
|
Some(Details {
|
||||||
|
mime_type: crate::formats::mimes::image_apng(),
|
||||||
|
width: 112,
|
||||||
|
height: 112,
|
||||||
|
frames: Some(27),
|
||||||
|
}),
|
||||||
|
),
|
||||||
("avif", None),
|
("avif", None),
|
||||||
(
|
(
|
||||||
"gif",
|
"gif",
|
||||||
|
@ -17,7 +26,7 @@ fn details_tests() -> [(&'static str, Option<Details>); 9] {
|
||||||
(
|
(
|
||||||
"mp4",
|
"mp4",
|
||||||
Some(Details {
|
Some(Details {
|
||||||
mime_type: crate::magick::video_mp4(),
|
mime_type: crate::formats::mimes::video_mp4(),
|
||||||
width: 852,
|
width: 852,
|
||||||
height: 480,
|
height: 480,
|
||||||
frames: Some(35364),
|
frames: Some(35364),
|
||||||
|
@ -27,7 +36,7 @@ fn details_tests() -> [(&'static str, Option<Details>); 9] {
|
||||||
(
|
(
|
||||||
"webm",
|
"webm",
|
||||||
Some(Details {
|
Some(Details {
|
||||||
mime_type: crate::magick::video_webm(),
|
mime_type: crate::formats::mimes::video_webm(),
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 480,
|
height: 480,
|
||||||
frames: Some(34650),
|
frames: Some(34650),
|
||||||
|
@ -36,7 +45,7 @@ fn details_tests() -> [(&'static str, Option<Details>); 9] {
|
||||||
(
|
(
|
||||||
"webm_av1",
|
"webm_av1",
|
||||||
Some(Details {
|
Some(Details {
|
||||||
mime_type: crate::magick::video_webm(),
|
mime_type: crate::formats::mimes::video_webm(),
|
||||||
width: 112,
|
width: 112,
|
||||||
height: 112,
|
height: 112,
|
||||||
frames: Some(27),
|
frames: Some(27),
|
||||||
|
|
670
src/formats.rs
670
src/formats.rs
|
@ -1,558 +1,100 @@
|
||||||
fn image_apng() -> mime::Mime {
|
mod animation;
|
||||||
"image/apng".parse().unwrap()
|
mod image;
|
||||||
}
|
pub(crate) mod mimes;
|
||||||
|
mod video;
|
||||||
|
|
||||||
fn image_avif() -> mime::Mime {
|
use std::str::FromStr;
|
||||||
"image/avif".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_jxl() -> mime::Mime {
|
pub(crate) use animation::{AnimationFormat, AnimationInput, AnimationOutput};
|
||||||
"image/jxl".parse().unwrap()
|
pub(crate) use image::{ImageFormat, ImageInput, ImageOutput};
|
||||||
}
|
pub(crate) use video::{InternalVideoFormat, OutputVideoFormat, VideoFormat};
|
||||||
|
|
||||||
fn image_webp() -> mime::Mime {
|
|
||||||
"image/webp".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn video_mp4() -> mime::Mime {
|
|
||||||
"video/mp4".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn video_webm() -> mime::Mime {
|
|
||||||
"video/webm".parse().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct PrescribedFormats {
|
pub(crate) struct PrescribedFormats {
|
||||||
image: Option<ImageFormat>,
|
pub(crate) image: Option<ImageFormat>,
|
||||||
animation: Option<AnimationFormat>,
|
pub(crate) animation: Option<AnimationFormat>,
|
||||||
video: Option<OutputVideoFormat>,
|
pub(crate) video: Option<OutputVideoFormat>,
|
||||||
allow_audio: bool,
|
pub(crate) allow_audio: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum ImageFormat {
|
pub(crate) enum InputFile {
|
||||||
#[serde(rename = "avif")]
|
Image(ImageInput),
|
||||||
Avif,
|
Animation(AnimationInput),
|
||||||
#[serde(rename = "png")]
|
Video(VideoFormat),
|
||||||
Png,
|
|
||||||
#[serde(rename = "jpeg")]
|
|
||||||
Jpeg,
|
|
||||||
#[serde(rename = "jxl")]
|
|
||||||
Jxl,
|
|
||||||
#[serde(rename = "webp")]
|
|
||||||
Webp,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum AnimationFormat {
|
pub(crate) enum OutputFile {
|
||||||
|
Image(ImageOutput),
|
||||||
|
Animation(AnimationOutput),
|
||||||
|
Video(OutputVideoFormat),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(crate) enum InternalFormat {
|
||||||
|
Image(ImageFormat),
|
||||||
|
Animation(AnimationFormat),
|
||||||
|
Video(InternalVideoFormat),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub(crate) enum ProcessableFormat {
|
||||||
|
Image(ImageFormat),
|
||||||
|
Animation(AnimationFormat),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum InputProcessableFormat {
|
||||||
#[serde(rename = "apng")]
|
#[serde(rename = "apng")]
|
||||||
Apng,
|
Apng,
|
||||||
#[serde(rename = "avif")]
|
#[serde(rename = "avif")]
|
||||||
Avif,
|
Avif,
|
||||||
#[serde(rename = "gif")]
|
#[serde(rename = "gif")]
|
||||||
Gif,
|
Gif,
|
||||||
|
#[serde(rename = "jpeg")]
|
||||||
|
Jpeg,
|
||||||
|
#[serde(rename = "jxl")]
|
||||||
|
Jxl,
|
||||||
|
#[serde(rename = "png")]
|
||||||
|
Png,
|
||||||
#[serde(rename = "webp")]
|
#[serde(rename = "webp")]
|
||||||
Webp,
|
Webp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub(crate) enum VideoFormat {
|
|
||||||
Mp4,
|
|
||||||
Webp { alpha: bool },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
|
||||||
pub(crate) enum VideoCodec {
|
|
||||||
#[serde(rename = "av1")]
|
|
||||||
Av1,
|
|
||||||
#[serde(rename = "h264")]
|
|
||||||
H264,
|
|
||||||
#[serde(rename = "h265")]
|
|
||||||
H265,
|
|
||||||
#[serde(rename = "vp8")]
|
|
||||||
Vp8,
|
|
||||||
#[serde(rename = "vp9")]
|
|
||||||
Vp9,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
|
||||||
pub(crate) enum AudioCodec {
|
|
||||||
#[serde(rename = "aac")]
|
|
||||||
Aac,
|
|
||||||
#[serde(rename = "opus")]
|
|
||||||
Opus,
|
|
||||||
#[serde(rename = "vorbis")]
|
|
||||||
Vorbis,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub(crate) enum OutputVideoFormat {
|
|
||||||
Mp4 {
|
|
||||||
video_codec: Mp4Codec,
|
|
||||||
audio_codec: Option<Mp4AudioCodec>,
|
|
||||||
},
|
|
||||||
Webm {
|
|
||||||
video_codec: WebmCodec,
|
|
||||||
audio_codec: Option<WebmAudioCodec>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
|
||||||
pub(crate) enum Mp4Codec {
|
|
||||||
#[serde(rename = "h264")]
|
|
||||||
H264,
|
|
||||||
#[serde(rename = "h265")]
|
|
||||||
H265,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
|
||||||
pub(crate) enum WebmAlphaCodec {
|
|
||||||
#[serde(rename = "vp8")]
|
|
||||||
Vp8,
|
|
||||||
#[serde(rename = "vp9")]
|
|
||||||
Vp9,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub(crate) struct AlphaCodec {
|
|
||||||
pub(crate) alpha: bool,
|
|
||||||
pub(crate) codec: WebmAlphaCodec,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub(crate) enum WebmCodec {
|
|
||||||
Av1,
|
|
||||||
Alpha(AlphaCodec),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub(crate) enum Mp4AudioCodec {
|
|
||||||
Aac,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub(crate) enum WebmAudioCodec {
|
|
||||||
Opus,
|
|
||||||
Vorbis,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) enum InputFile {
|
|
||||||
Image {
|
|
||||||
format: ImageFormat,
|
|
||||||
needs_reorient: bool,
|
|
||||||
},
|
|
||||||
Animation(AnimationFormat),
|
|
||||||
Video(VideoFormat),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) enum OutputFile {
|
|
||||||
Image {
|
|
||||||
format: ImageFormat,
|
|
||||||
needs_transcode: bool,
|
|
||||||
},
|
|
||||||
Animation {
|
|
||||||
format: AnimationFormat,
|
|
||||||
needs_transcode: bool,
|
|
||||||
},
|
|
||||||
Video(OutputVideoFormat),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputFile {
|
impl InputFile {
|
||||||
const fn file_extension(&self) -> &'static str {
|
const fn file_extension(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Image { format, .. } => format.file_extension(),
|
Self::Image(ImageInput { format, .. }) => format.file_extension(),
|
||||||
Self::Animation(format) => format.file_extension(),
|
Self::Animation(AnimationInput { format }) => format.file_extension(),
|
||||||
Self::Video(format) => format.file_extension(),
|
Self::Video(format) => format.file_extension(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn build_output(&self, prescribed: &PrescribedFormats) -> OutputFile {
|
const fn build_output(&self, prescribed: &PrescribedFormats) -> OutputFile {
|
||||||
match (self, prescribed) {
|
match (self, prescribed) {
|
||||||
(
|
(InputFile::Image(input), PrescribedFormats { image, .. }) => {
|
||||||
InputFile::Image {
|
OutputFile::Image(input.build_output(*image))
|
||||||
format,
|
}
|
||||||
needs_reorient,
|
(InputFile::Animation(input), PrescribedFormats { animation, .. }) => {
|
||||||
},
|
OutputFile::Animation(input.build_output(*animation))
|
||||||
PrescribedFormats {
|
}
|
||||||
image: Some(prescribed),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) => OutputFile::Image {
|
|
||||||
format: *prescribed,
|
|
||||||
needs_transcode: *needs_reorient || !format.const_eq(*prescribed),
|
|
||||||
},
|
|
||||||
(
|
|
||||||
InputFile::Animation(format),
|
|
||||||
PrescribedFormats {
|
|
||||||
animation: Some(prescribed),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) => OutputFile::Animation {
|
|
||||||
format: *prescribed,
|
|
||||||
needs_transcode: !format.const_eq(*prescribed),
|
|
||||||
},
|
|
||||||
(
|
|
||||||
InputFile::Video(VideoFormat::Webp { alpha }),
|
|
||||||
PrescribedFormats {
|
|
||||||
video:
|
|
||||||
Some(OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec { codec, .. }),
|
|
||||||
audio_codec,
|
|
||||||
}),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) => OutputFile::Video(OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: *alpha,
|
|
||||||
codec: *codec,
|
|
||||||
}),
|
|
||||||
audio_codec: *audio_codec,
|
|
||||||
}),
|
|
||||||
(
|
|
||||||
InputFile::Video(_),
|
|
||||||
PrescribedFormats {
|
|
||||||
video: Some(prescribed),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) => OutputFile::Video(*prescribed),
|
|
||||||
(
|
|
||||||
InputFile::Image {
|
|
||||||
format,
|
|
||||||
needs_reorient,
|
|
||||||
},
|
|
||||||
PrescribedFormats { image: None, .. },
|
|
||||||
) => OutputFile::Image {
|
|
||||||
format: *format,
|
|
||||||
needs_transcode: *needs_reorient,
|
|
||||||
},
|
|
||||||
(
|
|
||||||
InputFile::Animation(input),
|
|
||||||
PrescribedFormats {
|
|
||||||
animation: None, ..
|
|
||||||
},
|
|
||||||
) => OutputFile::Animation {
|
|
||||||
format: *input,
|
|
||||||
needs_transcode: false,
|
|
||||||
},
|
|
||||||
(
|
(
|
||||||
InputFile::Video(input),
|
InputFile::Video(input),
|
||||||
PrescribedFormats {
|
PrescribedFormats {
|
||||||
video: None,
|
video, allow_audio, ..
|
||||||
allow_audio: true,
|
|
||||||
..
|
|
||||||
},
|
},
|
||||||
) => match input {
|
) => OutputFile::Video(input.build_output(*video, *allow_audio)),
|
||||||
VideoFormat::Mp4 => OutputFile::Video(OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H264,
|
|
||||||
audio_codec: Some(Mp4AudioCodec::Aac),
|
|
||||||
}),
|
|
||||||
VideoFormat::Webp { alpha } => OutputFile::Video(OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: *alpha,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Opus),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
(
|
|
||||||
InputFile::Video(input),
|
|
||||||
PrescribedFormats {
|
|
||||||
video: None,
|
|
||||||
allow_audio: false,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) => match input {
|
|
||||||
VideoFormat::Mp4 => OutputFile::Video(OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H264,
|
|
||||||
audio_codec: None,
|
|
||||||
}),
|
|
||||||
VideoFormat::Webp { alpha } => OutputFile::Video(OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: *alpha,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: None,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImageFormat {
|
|
||||||
const fn const_eq(self, rhs: Self) -> bool {
|
|
||||||
match (self, rhs) {
|
|
||||||
(Self::Avif, Self::Avif)
|
|
||||||
| (Self::Jpeg, Self::Jpeg)
|
|
||||||
| (Self::Jxl, Self::Jxl)
|
|
||||||
| (Self::Png, Self::Png)
|
|
||||||
| (Self::Webp, Self::Webp) => true,
|
|
||||||
(Self::Avif, _)
|
|
||||||
| (Self::Jpeg, _)
|
|
||||||
| (Self::Jxl, _)
|
|
||||||
| (Self::Png, _)
|
|
||||||
| (Self::Webp, _) => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn file_extension(self) -> &'static str {
|
pub(crate) const fn internal_format(&self) -> InternalFormat {
|
||||||
match self {
|
match self {
|
||||||
Self::Avif => ".avif",
|
Self::Image(ImageInput { format, .. }) => InternalFormat::Image(*format),
|
||||||
Self::Jpeg => ".jpeg",
|
Self::Animation(AnimationInput { format }) => InternalFormat::Animation(*format),
|
||||||
Self::Jxl => ".jxl",
|
Self::Video(format) => InternalFormat::Video(format.internal_format()),
|
||||||
Self::Png => ".png",
|
|
||||||
Self::Webp => ".webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn magick_format(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Avif => "AVIF",
|
|
||||||
Self::Jpeg => "JPEG",
|
|
||||||
Self::Jxl => "JXL",
|
|
||||||
Self::Png => "PNG",
|
|
||||||
Self::Webp => "Webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn media_type(self) -> mime::Mime {
|
|
||||||
match self {
|
|
||||||
Self::Avif => image_avif(),
|
|
||||||
Self::Jpeg => mime::IMAGE_JPEG,
|
|
||||||
Self::Jxl => image_jxl(),
|
|
||||||
Self::Png => mime::IMAGE_PNG,
|
|
||||||
Self::Webp => image_webp(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AnimationFormat {
|
|
||||||
const fn const_eq(self, rhs: Self) -> bool {
|
|
||||||
match (self, rhs) {
|
|
||||||
(Self::Apng, Self::Apng)
|
|
||||||
| (Self::Avif, Self::Avif)
|
|
||||||
| (Self::Gif, Self::Gif)
|
|
||||||
| (Self::Webp, Self::Webp) => true,
|
|
||||||
(Self::Apng, _) | (Self::Avif, _) | (Self::Gif, _) | (Self::Webp, _) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn file_extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Apng => ".apng",
|
|
||||||
Self::Avif => ".avif",
|
|
||||||
Self::Gif => ".gif",
|
|
||||||
Self::Webp => ".webp",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn magick_format(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Apng => "APNG",
|
|
||||||
Self::Avif => "AVIF",
|
|
||||||
Self::Gif => "GIF",
|
|
||||||
Self::Webp => "WEBP",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn media_type(self) -> mime::Mime {
|
|
||||||
match self {
|
|
||||||
Self::Apng => image_apng(),
|
|
||||||
Self::Avif => image_avif(),
|
|
||||||
Self::Gif => mime::IMAGE_GIF,
|
|
||||||
Self::Webp => image_webp(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VideoFormat {
|
|
||||||
const fn file_extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 => ".mp4",
|
|
||||||
Self::Webp { .. } => ".webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputVideoFormat {
|
|
||||||
const fn from_parts(
|
|
||||||
video_codec: VideoCodec,
|
|
||||||
audio_codec: Option<AudioCodec>,
|
|
||||||
allow_audio: bool,
|
|
||||||
) -> Self {
|
|
||||||
match (video_codec, audio_codec) {
|
|
||||||
(VideoCodec::Av1, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Av1,
|
|
||||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
|
||||||
},
|
|
||||||
(VideoCodec::Av1, _) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Av1,
|
|
||||||
audio_codec: Some(WebmAudioCodec::Opus),
|
|
||||||
},
|
|
||||||
(VideoCodec::Av1, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Av1,
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::H264, _) if allow_audio => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H264,
|
|
||||||
audio_codec: Some(Mp4AudioCodec::Aac),
|
|
||||||
},
|
|
||||||
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H264,
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::H265, _) if allow_audio => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H265,
|
|
||||||
audio_codec: Some(Mp4AudioCodec::Aac),
|
|
||||||
},
|
|
||||||
(VideoCodec::H265, _) => OutputVideoFormat::Mp4 {
|
|
||||||
video_codec: Mp4Codec::H265,
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp8, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp8,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp8, _) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp8,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Opus),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp8, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp8,
|
|
||||||
}),
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp9, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Vorbis),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp9, _) if allow_audio => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: Some(WebmAudioCodec::Opus),
|
|
||||||
},
|
|
||||||
(VideoCodec::Vp9, _) => OutputVideoFormat::Webm {
|
|
||||||
video_codec: WebmCodec::Alpha(AlphaCodec {
|
|
||||||
alpha: false,
|
|
||||||
codec: WebmAlphaCodec::Vp9,
|
|
||||||
}),
|
|
||||||
audio_codec: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn file_extension(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 { .. } => ".mp4",
|
|
||||||
Self::Webm { .. } => ".webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn ffmpeg_format(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 { .. } => "mp4",
|
|
||||||
Self::Webm { .. } => "webm",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn ffmpeg_video_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 { video_codec, .. } => video_codec.ffmpeg_codec(),
|
|
||||||
Self::Webm { video_codec, .. } => video_codec.ffmpeg_codec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn ffmpeg_audio_codec(self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 {
|
|
||||||
audio_codec: Some(audio_codec),
|
|
||||||
..
|
|
||||||
} => Some(audio_codec.ffmpeg_codec()),
|
|
||||||
Self::Webm {
|
|
||||||
audio_codec: Some(audio_codec),
|
|
||||||
..
|
|
||||||
} => Some(audio_codec.ffmpeg_codec()),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn pix_fmt(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 { .. } => "yuv420p",
|
|
||||||
Self::Webm { video_codec, .. } => video_codec.pix_fmt(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn media_type(self) -> mime::Mime {
|
|
||||||
match self {
|
|
||||||
Self::Mp4 { .. } => video_mp4(),
|
|
||||||
Self::Webm { .. } => video_webm(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mp4Codec {
|
|
||||||
const fn ffmpeg_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::H264 => "h264",
|
|
||||||
Self::H265 => "hevc",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebmAlphaCodec {
|
|
||||||
const fn ffmpeg_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Vp8 => "vp8",
|
|
||||||
Self::Vp9 => "vp9",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebmCodec {
|
|
||||||
const fn ffmpeg_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Av1 => "av1",
|
|
||||||
Self::Alpha(AlphaCodec { codec, .. }) => codec.ffmpeg_codec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn pix_fmt(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Alpha(AlphaCodec { alpha: true, .. }) => "yuva420p",
|
|
||||||
_ => "yuv420p",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mp4AudioCodec {
|
|
||||||
const fn ffmpeg_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Aac => "aac",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebmAudioCodec {
|
|
||||||
const fn ffmpeg_codec(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Opus => "libopus",
|
|
||||||
Self::Vorbis => "vorbis",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -560,17 +102,95 @@ impl WebmAudioCodec {
|
||||||
impl OutputFile {
|
impl OutputFile {
|
||||||
const fn file_extension(&self) -> &'static str {
|
const fn file_extension(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Image { format, .. } => format.file_extension(),
|
Self::Image(ImageOutput { format, .. }) => format.file_extension(),
|
||||||
Self::Animation { format, .. } => format.file_extension(),
|
Self::Animation(AnimationOutput { format, .. }) => format.file_extension(),
|
||||||
Self::Video(format) => format.file_extension(),
|
Self::Video(format) => format.file_extension(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn media_type(&self) -> mime::Mime {
|
pub(crate) const fn internal_format(&self) -> InternalFormat {
|
||||||
match self {
|
match self {
|
||||||
Self::Image { format, .. } => format.media_type(),
|
Self::Image(ImageOutput { format, .. }) => InternalFormat::Image(*format),
|
||||||
Self::Animation { format, .. } => format.media_type(),
|
Self::Animation(AnimationOutput { format, .. }) => InternalFormat::Animation(*format),
|
||||||
Self::Video(format) => format.media_type(),
|
Self::Video(format) => InternalFormat::Video(format.internal_format()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InternalFormat {
|
||||||
|
pub(crate) fn media_type(self) -> mime::Mime {
|
||||||
|
match self {
|
||||||
|
Self::Image(format) => format.media_type(),
|
||||||
|
Self::Animation(format) => format.media_type(),
|
||||||
|
Self::Video(format) => format.media_type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn file_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Image(format) => format.file_extension(),
|
||||||
|
Self::Animation(format) => format.file_extension(),
|
||||||
|
Self::Video(format) => format.file_extension(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn processable_format(self) -> Option<ProcessableFormat> {
|
||||||
|
match self {
|
||||||
|
Self::Image(format) => Some(ProcessableFormat::Image(format)),
|
||||||
|
Self::Animation(format) => Some(ProcessableFormat::Animation(format)),
|
||||||
|
Self::Video(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessableFormat {
|
||||||
|
pub(crate) const fn file_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Image(format) => format.file_extension(),
|
||||||
|
Self::Animation(format) => format.file_extension(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn coalesce(self) -> bool {
|
||||||
|
matches!(self, Self::Animation(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn magick_format(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Image(format) => format.magick_format(),
|
||||||
|
Self::Animation(format) => format.magick_format(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for InputProcessableFormat {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"apng" => Ok(Self::Apng),
|
||||||
|
"avif" => Ok(Self::Avif),
|
||||||
|
"gif" => Ok(Self::Gif),
|
||||||
|
"jpeg" => Ok(Self::Jpeg),
|
||||||
|
"jpg" => Ok(Self::Jpeg),
|
||||||
|
"jxl" => Ok(Self::Jxl),
|
||||||
|
"png" => Ok(Self::Png),
|
||||||
|
"webp" => Ok(Self::Webp),
|
||||||
|
otherwise => Err(format!("Invalid format: {otherwise}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for InputProcessableFormat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Apng => write!(f, "apng"),
|
||||||
|
Self::Avif => write!(f, "avif"),
|
||||||
|
Self::Gif => write!(f, "gif"),
|
||||||
|
Self::Jpeg => write!(f, "jpeg"),
|
||||||
|
Self::Jxl => write!(f, "jxl"),
|
||||||
|
Self::Png => write!(f, "png"),
|
||||||
|
Self::Webp => write!(f, "webp"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
84
src/formats/animation.rs
Normal file
84
src/formats/animation.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum AnimationFormat {
|
||||||
|
#[serde(rename = "apng")]
|
||||||
|
Apng,
|
||||||
|
#[serde(rename = "avif")]
|
||||||
|
Avif,
|
||||||
|
#[serde(rename = "gif")]
|
||||||
|
Gif,
|
||||||
|
#[serde(rename = "webp")]
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||||
|
pub(crate) struct AnimationInput {
|
||||||
|
pub(crate) format: AnimationFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||||
|
pub(crate) struct AnimationOutput {
|
||||||
|
pub(crate) format: AnimationFormat,
|
||||||
|
pub(crate) needs_transcode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationInput {
|
||||||
|
pub(crate) const fn build_output(
|
||||||
|
&self,
|
||||||
|
prescribed: Option<AnimationFormat>,
|
||||||
|
) -> AnimationOutput {
|
||||||
|
if let Some(prescribed) = prescribed {
|
||||||
|
let needs_transcode = !self.format.const_eq(prescribed);
|
||||||
|
|
||||||
|
return AnimationOutput {
|
||||||
|
format: prescribed,
|
||||||
|
needs_transcode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationOutput {
|
||||||
|
format: self.format,
|
||||||
|
needs_transcode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationFormat {
|
||||||
|
const fn const_eq(self, rhs: Self) -> bool {
|
||||||
|
match (self, rhs) {
|
||||||
|
(Self::Apng, Self::Apng)
|
||||||
|
| (Self::Avif, Self::Avif)
|
||||||
|
| (Self::Gif, Self::Gif)
|
||||||
|
| (Self::Webp, Self::Webp) => true,
|
||||||
|
(Self::Apng, _) | (Self::Avif, _) | (Self::Gif, _) | (Self::Webp, _) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) const fn file_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Apng => ".apng",
|
||||||
|
Self::Avif => ".avif",
|
||||||
|
Self::Gif => ".gif",
|
||||||
|
Self::Webp => ".webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn magick_format(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Apng => "APNG",
|
||||||
|
Self::Avif => "AVIF",
|
||||||
|
Self::Gif => "GIF",
|
||||||
|
Self::Webp => "WEBP",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn media_type(self) -> mime::Mime {
|
||||||
|
match self {
|
||||||
|
Self::Apng => super::mimes::image_apng(),
|
||||||
|
Self::Avif => super::mimes::image_avif(),
|
||||||
|
Self::Gif => mime::IMAGE_GIF,
|
||||||
|
Self::Webp => super::mimes::image_webp(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
src/formats/image.rs
Normal file
92
src/formats/image.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum ImageFormat {
|
||||||
|
#[serde(rename = "avif")]
|
||||||
|
Avif,
|
||||||
|
#[serde(rename = "png")]
|
||||||
|
Png,
|
||||||
|
#[serde(rename = "jpeg")]
|
||||||
|
Jpeg,
|
||||||
|
#[serde(rename = "jxl")]
|
||||||
|
Jxl,
|
||||||
|
#[serde(rename = "webp")]
|
||||||
|
Webp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||||
|
pub(crate) struct ImageInput {
|
||||||
|
pub(crate) format: ImageFormat,
|
||||||
|
pub(crate) needs_reorient: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||||
|
pub(crate) struct ImageOutput {
|
||||||
|
pub(crate) format: ImageFormat,
|
||||||
|
pub(crate) needs_transcode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageInput {
|
||||||
|
pub(crate) const fn build_output(self, prescribed: Option<ImageFormat>) -> ImageOutput {
|
||||||
|
if let Some(prescribed) = prescribed {
|
||||||
|
let needs_transcode = self.needs_reorient || !self.format.const_eq(prescribed);
|
||||||
|
|
||||||
|
return ImageOutput {
|
||||||
|
format: prescribed,
|
||||||
|
needs_transcode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageOutput {
|
||||||
|
format: self.format,
|
||||||
|
needs_transcode: self.needs_reorient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageFormat {
|
||||||
|
pub(super) const fn const_eq(self, rhs: Self) -> bool {
|
||||||
|
match (self, rhs) {
|
||||||
|
(Self::Avif, Self::Avif)
|
||||||
|
| (Self::Jpeg, Self::Jpeg)
|
||||||
|
| (Self::Jxl, Self::Jxl)
|
||||||
|
| (Self::Png, Self::Png)
|
||||||
|
| (Self::Webp, Self::Webp) => true,
|
||||||
|
(Self::Avif, _)
|
||||||
|
| (Self::Jpeg, _)
|
||||||
|
| (Self::Jxl, _)
|
||||||
|
| (Self::Png, _)
|
||||||
|
| (Self::Webp, _) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) const fn file_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Avif => ".avif",
|
||||||
|
Self::Jpeg => ".jpeg",
|
||||||
|
Self::Jxl => ".jxl",
|
||||||
|
Self::Png => ".png",
|
||||||
|
Self::Webp => ".webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn magick_format(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Avif => "AVIF",
|
||||||
|
Self::Jpeg => "JPEG",
|
||||||
|
Self::Jxl => "JXL",
|
||||||
|
Self::Png => "PNG",
|
||||||
|
Self::Webp => "Webp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn media_type(self) -> mime::Mime {
|
||||||
|
match self {
|
||||||
|
Self::Avif => super::mimes::image_avif(),
|
||||||
|
Self::Jpeg => mime::IMAGE_JPEG,
|
||||||
|
Self::Jxl => super::mimes::image_jxl(),
|
||||||
|
Self::Png => mime::IMAGE_PNG,
|
||||||
|
Self::Webp => super::mimes::image_webp(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/formats/mimes.rs
Normal file
23
src/formats/mimes.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
pub(crate) fn image_apng() -> mime::Mime {
|
||||||
|
"image/apng".parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn image_avif() -> mime::Mime {
|
||||||
|
"image/avif".parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn image_jxl() -> mime::Mime {
|
||||||
|
"image/jxl".parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn image_webp() -> mime::Mime {
|
||||||
|
"image/webp".parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn video_mp4() -> mime::Mime {
|
||||||
|
"video/mp4".parse().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn video_webm() -> mime::Mime {
|
||||||
|
"video/webm".parse().unwrap()
|
||||||
|
}
|
372
src/formats/video.rs
Normal file
372
src/formats/video.rs
Normal file
|
@ -0,0 +1,372 @@
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub(crate) enum VideoFormat {
|
||||||
|
Mp4,
|
||||||
|
Webm { alpha: bool },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum OutputVideoFormat {
|
||||||
|
Mp4 {
|
||||||
|
video_codec: Mp4Codec,
|
||||||
|
audio_codec: Option<Mp4AudioCodec>,
|
||||||
|
},
|
||||||
|
Webm {
|
||||||
|
video_codec: WebmCodec,
|
||||||
|
audio_codec: Option<WebmAudioCodec>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize)]
|
||||||
|
pub(crate) enum VideoCodec {
|
||||||
|
#[serde(rename = "av1")]
|
||||||
|
Av1,
|
||||||
|
#[serde(rename = "h264")]
|
||||||
|
H264,
|
||||||
|
#[serde(rename = "h265")]
|
||||||
|
H265,
|
||||||
|
#[serde(rename = "vp8")]
|
||||||
|
Vp8,
|
||||||
|
#[serde(rename = "vp9")]
|
||||||
|
Vp9,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum AudioCodec {
|
||||||
|
#[serde(rename = "aac")]
|
||||||
|
Aac,
|
||||||
|
#[serde(rename = "opus")]
|
||||||
|
Opus,
|
||||||
|
#[serde(rename = "vorbis")]
|
||||||
|
Vorbis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum Mp4Codec {
|
||||||
|
#[serde(rename = "h264")]
|
||||||
|
H264,
|
||||||
|
#[serde(rename = "h265")]
|
||||||
|
H265,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum WebmAlphaCodec {
|
||||||
|
#[serde(rename = "vp8")]
|
||||||
|
Vp8,
|
||||||
|
#[serde(rename = "vp9")]
|
||||||
|
Vp9,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) struct AlphaCodec {
|
||||||
|
pub(crate) alpha: bool,
|
||||||
|
pub(crate) codec: WebmAlphaCodec,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum WebmCodec {
|
||||||
|
Av1,
|
||||||
|
Alpha(AlphaCodec),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum Mp4AudioCodec {
|
||||||
|
Aac,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum WebmAudioCodec {
|
||||||
|
Opus,
|
||||||
|
Vorbis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||||
|
)]
|
||||||
|
pub(crate) enum InternalVideoFormat {
|
||||||
|
Mp4,
|
||||||
|
Webm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoFormat {
|
||||||
|
pub(crate) const fn file_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 => ".mp4",
|
||||||
|
Self::Webm { .. } => ".webm",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 => "mp4",
|
||||||
|
Self::Webm { .. } => "webm",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn internal_format(self) -> InternalVideoFormat {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 => InternalVideoFormat::Mp4,
|
||||||
|
Self::Webm { .. } => InternalVideoFormat::Webm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn build_output(
|
||||||
|
self,
|
||||||
|
prescribed: Option<OutputVideoFormat>,
|
||||||
|
allow_audio: bool,
|
||||||
|
) -> OutputVideoFormat {
|
||||||
|
match (prescribed, self) {
|
||||||
|
(
|
||||||
|
Some(OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec { codec, .. }),
|
||||||
|
audio_codec,
|
||||||
|
}),
|
||||||
|
Self::Webm { alpha },
|
||||||
|
) => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec { alpha, codec }),
|
||||||
|
audio_codec,
|
||||||
|
},
|
||||||
|
(Some(prescribed), _) => prescribed,
|
||||||
|
(None, format) => match format {
|
||||||
|
VideoFormat::Mp4 => OutputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::H264,
|
||||||
|
audio_codec: if allow_audio {
|
||||||
|
Some(Mp4AudioCodec::Aac)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
},
|
||||||
|
VideoFormat::Webm { alpha } => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha,
|
||||||
|
codec: WebmAlphaCodec::Vp9,
|
||||||
|
}),
|
||||||
|
audio_codec: if allow_audio {
|
||||||
|
Some(WebmAudioCodec::Opus)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputVideoFormat {
|
||||||
|
pub(super) const fn from_parts(
|
||||||
|
video_codec: VideoCodec,
|
||||||
|
audio_codec: Option<AudioCodec>,
|
||||||
|
allow_audio: bool,
|
||||||
|
) -> Self {
|
||||||
|
match (video_codec, audio_codec) {
|
||||||
|
(VideoCodec::Av1, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Av1,
|
||||||
|
audio_codec: Some(WebmAudioCodec::Vorbis),
|
||||||
|
},
|
||||||
|
(VideoCodec::Av1, _) if allow_audio => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Av1,
|
||||||
|
audio_codec: Some(WebmAudioCodec::Opus),
|
||||||
|
},
|
||||||
|
(VideoCodec::Av1, _) => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Av1,
|
||||||
|
audio_codec: None,
|
||||||
|
},
|
||||||
|
(VideoCodec::H264, _) if allow_audio => OutputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::H264,
|
||||||
|
audio_codec: Some(Mp4AudioCodec::Aac),
|
||||||
|
},
|
||||||
|
(VideoCodec::H264, _) => OutputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::H264,
|
||||||
|
audio_codec: None,
|
||||||
|
},
|
||||||
|
(VideoCodec::H265, _) if allow_audio => OutputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::H265,
|
||||||
|
audio_codec: Some(Mp4AudioCodec::Aac),
|
||||||
|
},
|
||||||
|
(VideoCodec::H265, _) => OutputVideoFormat::Mp4 {
|
||||||
|
video_codec: Mp4Codec::H265,
|
||||||
|
audio_codec: None,
|
||||||
|
},
|
||||||
|
(VideoCodec::Vp8, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp8,
|
||||||
|
}),
|
||||||
|
audio_codec: Some(WebmAudioCodec::Vorbis),
|
||||||
|
},
|
||||||
|
(VideoCodec::Vp8, _) if allow_audio => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp8,
|
||||||
|
}),
|
||||||
|
audio_codec: Some(WebmAudioCodec::Opus),
|
||||||
|
},
|
||||||
|
(VideoCodec::Vp8, _) => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp8,
|
||||||
|
}),
|
||||||
|
audio_codec: None,
|
||||||
|
},
|
||||||
|
(VideoCodec::Vp9, Some(AudioCodec::Vorbis)) if allow_audio => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp9,
|
||||||
|
}),
|
||||||
|
audio_codec: Some(WebmAudioCodec::Vorbis),
|
||||||
|
},
|
||||||
|
(VideoCodec::Vp9, _) if allow_audio => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp9,
|
||||||
|
}),
|
||||||
|
audio_codec: Some(WebmAudioCodec::Opus),
|
||||||
|
},
|
||||||
|
(VideoCodec::Vp9, _) => OutputVideoFormat::Webm {
|
||||||
|
video_codec: WebmCodec::Alpha(AlphaCodec {
|
||||||
|
alpha: false,
|
||||||
|
codec: WebmAlphaCodec::Vp9,
|
||||||
|
}),
|
||||||
|
audio_codec: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) const fn file_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 { .. } => ".mp4",
|
||||||
|
Self::Webm { .. } => ".webm",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn ffmpeg_format(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 { .. } => "mp4",
|
||||||
|
Self::Webm { .. } => "webm",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn ffmpeg_video_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 { video_codec, .. } => video_codec.ffmpeg_codec(),
|
||||||
|
Self::Webm { video_codec, .. } => video_codec.ffmpeg_codec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn ffmpeg_audio_codec(self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 {
|
||||||
|
audio_codec: Some(audio_codec),
|
||||||
|
..
|
||||||
|
} => Some(audio_codec.ffmpeg_codec()),
|
||||||
|
Self::Webm {
|
||||||
|
audio_codec: Some(audio_codec),
|
||||||
|
..
|
||||||
|
} => Some(audio_codec.ffmpeg_codec()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn pix_fmt(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 { .. } => "yuv420p",
|
||||||
|
Self::Webm { video_codec, .. } => video_codec.pix_fmt(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn media_type(self) -> mime::Mime {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 { .. } => super::mimes::video_mp4(),
|
||||||
|
Self::Webm { .. } => super::mimes::video_webm(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn internal_format(self) -> InternalVideoFormat {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 { .. } => InternalVideoFormat::Mp4,
|
||||||
|
Self::Webm { .. } => InternalVideoFormat::Webm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mp4Codec {
|
||||||
|
const fn ffmpeg_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::H264 => "h264",
|
||||||
|
Self::H265 => "hevc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebmAlphaCodec {
|
||||||
|
const fn ffmpeg_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Vp8 => "vp8",
|
||||||
|
Self::Vp9 => "vp9",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebmCodec {
|
||||||
|
const fn ffmpeg_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Av1 => "av1",
|
||||||
|
Self::Alpha(AlphaCodec { codec, .. }) => codec.ffmpeg_codec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn pix_fmt(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Alpha(AlphaCodec { alpha: true, .. }) => "yuva420p",
|
||||||
|
_ => "yuv420p",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mp4AudioCodec {
|
||||||
|
const fn ffmpeg_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Aac => "aac",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebmAudioCodec {
|
||||||
|
const fn ffmpeg_codec(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Opus => "libopus",
|
||||||
|
Self::Vorbis => "vorbis",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InternalVideoFormat {
|
||||||
|
pub(crate) const fn file_extension(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 => ".mp4",
|
||||||
|
Self::Webm => ".webm",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn media_type(self) -> mime::Mime {
|
||||||
|
match self {
|
||||||
|
Self::Mp4 => super::mimes::video_mp4(),
|
||||||
|
Self::Webm => super::mimes::video_webm(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
concurrent_processor::CancelSafeProcessor,
|
concurrent_processor::CancelSafeProcessor,
|
||||||
config::ImageFormat,
|
|
||||||
details::Details,
|
details::Details,
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
ffmpeg::{ThumbnailFormat, VideoFormat},
|
ffmpeg::ThumbnailFormat,
|
||||||
|
formats::{InputProcessableFormat, InternalVideoFormat},
|
||||||
repo::{Alias, FullRepo},
|
repo::{Alias, FullRepo},
|
||||||
store::Store,
|
store::Store,
|
||||||
};
|
};
|
||||||
|
@ -17,11 +17,11 @@ use tracing::Instrument;
|
||||||
pub(crate) async fn generate<R: FullRepo, S: Store + 'static>(
|
pub(crate) async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
repo: &R,
|
repo: &R,
|
||||||
store: &S,
|
store: &S,
|
||||||
format: ImageFormat,
|
format: InputProcessableFormat,
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
thumbnail_path: PathBuf,
|
thumbnail_path: PathBuf,
|
||||||
thumbnail_args: Vec<String>,
|
thumbnail_args: Vec<String>,
|
||||||
input_format: Option<VideoFormat>,
|
input_format: Option<InternalVideoFormat>,
|
||||||
thumbnail_format: Option<ThumbnailFormat>,
|
thumbnail_format: Option<ThumbnailFormat>,
|
||||||
hash: R::Bytes,
|
hash: R::Bytes,
|
||||||
) -> Result<(Details, Bytes), Error> {
|
) -> Result<(Details, Bytes), Error> {
|
||||||
|
@ -48,11 +48,11 @@ pub(crate) async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
async fn process<R: FullRepo, S: Store + 'static>(
|
async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
repo: &R,
|
repo: &R,
|
||||||
store: &S,
|
store: &S,
|
||||||
format: ImageFormat,
|
format: InputProcessableFormat,
|
||||||
alias: Alias,
|
alias: Alias,
|
||||||
thumbnail_path: PathBuf,
|
thumbnail_path: PathBuf,
|
||||||
thumbnail_args: Vec<String>,
|
thumbnail_args: Vec<String>,
|
||||||
input_format: Option<VideoFormat>,
|
input_format: Option<InternalVideoFormat>,
|
||||||
thumbnail_format: Option<ThumbnailFormat>,
|
thumbnail_format: Option<ThumbnailFormat>,
|
||||||
hash: R::Bytes,
|
hash: R::Bytes,
|
||||||
) -> Result<(Details, Bytes), Error> {
|
) -> Result<(Details, Bytes), Error> {
|
||||||
|
@ -71,7 +71,7 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
let reader = crate::ffmpeg::thumbnail(
|
let reader = crate::ffmpeg::thumbnail(
|
||||||
store.clone(),
|
store.clone(),
|
||||||
identifier,
|
identifier,
|
||||||
input_format.unwrap_or(VideoFormat::Mp4),
|
input_format.unwrap_or(InternalVideoFormat::Mp4),
|
||||||
thumbnail_format.unwrap_or(ThumbnailFormat::Jpeg),
|
thumbnail_format.unwrap_or(ThumbnailFormat::Jpeg),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -95,7 +95,14 @@ async fn process<R: FullRepo, S: Store + 'static>(
|
||||||
|
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
|
||||||
let details = Details::from_bytes(bytes.clone(), format.as_hint()).await?;
|
let discovery = crate::discover::discover_bytes(bytes.clone()).await?;
|
||||||
|
|
||||||
|
let details = Details::from_parts(
|
||||||
|
discovery.input.internal_format(),
|
||||||
|
discovery.width,
|
||||||
|
discovery.height,
|
||||||
|
discovery.frames,
|
||||||
|
);
|
||||||
|
|
||||||
let identifier = store.save_bytes(bytes.clone()).await?;
|
let identifier = store.save_bytes(bytes.clone()).await?;
|
||||||
repo.relate_details(&identifier, &details).await?;
|
repo.relate_details(&identifier, &details).await?;
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
||||||
bytes_stream::BytesStream,
|
bytes_stream::BytesStream,
|
||||||
either::Either,
|
either::Either,
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
magick::ValidInputType,
|
formats::{InternalFormat, PrescribedFormats},
|
||||||
repo::{Alias, AliasRepo, AlreadyExists, DeleteToken, FullRepo, HashRepo},
|
repo::{Alias, AliasRepo, AlreadyExists, DeleteToken, FullRepo, HashRepo},
|
||||||
store::Store,
|
store::Store,
|
||||||
CONFIG,
|
CONFIG,
|
||||||
|
@ -47,7 +47,6 @@ pub(crate) async fn ingest<R, S>(
|
||||||
store: &S,
|
store: &S,
|
||||||
stream: impl Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
|
stream: impl Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
|
||||||
declared_alias: Option<Alias>,
|
declared_alias: Option<Alias>,
|
||||||
should_validate: bool,
|
|
||||||
) -> Result<Session<R, S>, Error>
|
) -> Result<Session<R, S>, Error>
|
||||||
where
|
where
|
||||||
R: FullRepo + 'static,
|
R: FullRepo + 'static,
|
||||||
|
@ -57,13 +56,22 @@ where
|
||||||
|
|
||||||
let bytes = aggregate(stream).await?;
|
let bytes = aggregate(stream).await?;
|
||||||
|
|
||||||
|
// TODO: load from config
|
||||||
|
let prescribed = PrescribedFormats {
|
||||||
|
image: None,
|
||||||
|
animation: None,
|
||||||
|
video: None,
|
||||||
|
allow_audio: true,
|
||||||
|
};
|
||||||
|
|
||||||
tracing::trace!("Validating bytes");
|
tracing::trace!("Validating bytes");
|
||||||
let (input_type, validated_reader) =
|
let (input_type, validated_reader) =
|
||||||
crate::validate::validate_bytes(bytes, &CONFIG.media, should_validate).await?;
|
crate::validate::validate_bytes(bytes, &prescribed).await?;
|
||||||
|
|
||||||
let processed_reader = if let Some(operations) = CONFIG.media.preprocess_steps() {
|
let processed_reader = if let Some(operations) = CONFIG.media.preprocess_steps() {
|
||||||
if let Some(format) = input_type.to_format() {
|
if let Some(format) = input_type.processable_format() {
|
||||||
let (_, magick_args) = crate::processor::build_chain(operations, format.as_ext())?;
|
let (_, magick_args) =
|
||||||
|
crate::processor::build_chain(operations, format.file_extension())?;
|
||||||
|
|
||||||
let processed_reader =
|
let processed_reader =
|
||||||
crate::magick::process_image_async_read(validated_reader, magick_args, format)?;
|
crate::magick::process_image_async_read(validated_reader, magick_args, format)?;
|
||||||
|
@ -180,9 +188,9 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self, hash))]
|
#[tracing::instrument(level = "debug", skip(self, hash))]
|
||||||
async fn create_alias(&mut self, hash: &[u8], input_type: ValidInputType) -> Result<(), Error> {
|
async fn create_alias(&mut self, hash: &[u8], input_type: InternalFormat) -> Result<(), Error> {
|
||||||
loop {
|
loop {
|
||||||
let alias = Alias::generate(input_type.as_ext().to_string());
|
let alias = Alias::generate(input_type.file_extension().to_string());
|
||||||
|
|
||||||
if AliasRepo::create(&self.repo, &alias).await?.is_ok() {
|
if AliasRepo::create(&self.repo, &alias).await?.is_ok() {
|
||||||
self.alias = Some(alias.clone());
|
self.alias = Some(alias.clone());
|
||||||
|
|
20
src/lib.rs
20
src/lib.rs
|
@ -3,6 +3,7 @@ mod bytes_stream;
|
||||||
mod concurrent_processor;
|
mod concurrent_processor;
|
||||||
mod config;
|
mod config;
|
||||||
mod details;
|
mod details;
|
||||||
|
mod discover;
|
||||||
mod either;
|
mod either;
|
||||||
mod error;
|
mod error;
|
||||||
mod exiftool;
|
mod exiftool;
|
||||||
|
@ -32,6 +33,7 @@ use actix_web::{
|
||||||
web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer,
|
web, App, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer,
|
||||||
};
|
};
|
||||||
use awc::{Client, Connector};
|
use awc::{Client, Connector};
|
||||||
|
use formats::{InputProcessableFormat, ProcessableFormat};
|
||||||
use futures_util::{
|
use futures_util::{
|
||||||
stream::{empty, once},
|
stream::{empty, once},
|
||||||
Stream, StreamExt, TryStreamExt,
|
Stream, StreamExt, TryStreamExt,
|
||||||
|
@ -163,7 +165,7 @@ impl<R: FullRepo, S: Store + 'static> FormData for Upload<R, S> {
|
||||||
let stream = stream.map_err(Error::from);
|
let stream = stream.map_err(Error::from);
|
||||||
|
|
||||||
Box::pin(
|
Box::pin(
|
||||||
async move { ingest::ingest(&**repo, &**store, stream, None, true).await }
|
async move { ingest::ingest(&**repo, &**store, stream, None).await }
|
||||||
.instrument(span),
|
.instrument(span),
|
||||||
)
|
)
|
||||||
})),
|
})),
|
||||||
|
@ -215,7 +217,6 @@ impl<R: FullRepo, S: Store + 'static> FormData for Import<R, S> {
|
||||||
&**store,
|
&**store,
|
||||||
stream,
|
stream,
|
||||||
Some(Alias::from_existing(&filename)),
|
Some(Alias::from_existing(&filename)),
|
||||||
!CONFIG.media.skip_validate_imports,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -371,7 +372,7 @@ async fn upload_backgrounded<R: FullRepo, S: Store>(
|
||||||
.expect("Identifier exists")
|
.expect("Identifier exists")
|
||||||
.to_bytes()?;
|
.to_bytes()?;
|
||||||
|
|
||||||
queue::queue_ingest(&repo, identifier, upload_id, None, true).await?;
|
queue::queue_ingest(&repo, identifier, upload_id, None).await?;
|
||||||
|
|
||||||
files.push(serde_json::json!({
|
files.push(serde_json::json!({
|
||||||
"upload_id": upload_id.to_string(),
|
"upload_id": upload_id.to_string(),
|
||||||
|
@ -470,7 +471,7 @@ async fn do_download_inline<R: FullRepo + 'static, S: Store + 'static>(
|
||||||
repo: web::Data<R>,
|
repo: web::Data<R>,
|
||||||
store: web::Data<S>,
|
store: web::Data<S>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let mut session = ingest::ingest(&repo, &store, stream, None, true).await?;
|
let mut session = ingest::ingest(&repo, &store, stream, None).await?;
|
||||||
|
|
||||||
let alias = session.alias().expect("alias should exist").to_owned();
|
let alias = session.alias().expect("alias should exist").to_owned();
|
||||||
let delete_token = session.delete_token().await?;
|
let delete_token = session.delete_token().await?;
|
||||||
|
@ -503,7 +504,7 @@ async fn do_download_backgrounded<R: FullRepo + 'static, S: Store + 'static>(
|
||||||
.expect("Identifier exists")
|
.expect("Identifier exists")
|
||||||
.to_bytes()?;
|
.to_bytes()?;
|
||||||
|
|
||||||
queue::queue_ingest(&repo, identifier, upload_id, None, true).await?;
|
queue::queue_ingest(&repo, identifier, upload_id, None).await?;
|
||||||
|
|
||||||
backgrounded.disarm();
|
backgrounded.disarm();
|
||||||
|
|
||||||
|
@ -536,7 +537,7 @@ type ProcessQuery = Vec<(String, String)>;
|
||||||
fn prepare_process(
|
fn prepare_process(
|
||||||
query: web::Query<ProcessQuery>,
|
query: web::Query<ProcessQuery>,
|
||||||
ext: &str,
|
ext: &str,
|
||||||
) -> Result<(ImageFormat, Alias, PathBuf, Vec<String>), Error> {
|
) -> Result<(InputProcessableFormat, Alias, PathBuf, Vec<String>), Error> {
|
||||||
let (alias, operations) =
|
let (alias, operations) =
|
||||||
query
|
query
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
@ -562,12 +563,11 @@ fn prepare_process(
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let format = ext
|
let format = ext
|
||||||
.parse::<ImageFormat>()
|
.parse::<InputProcessableFormat>()
|
||||||
.map_err(|_| UploadError::UnsupportedProcessExtension)?;
|
.map_err(|_| UploadError::UnsupportedProcessExtension)?;
|
||||||
|
|
||||||
let ext = format.to_string();
|
let (thumbnail_path, thumbnail_args) =
|
||||||
|
self::processor::build_chain(&operations, &format.to_string())?;
|
||||||
let (thumbnail_path, thumbnail_args) = self::processor::build_chain(&operations, &ext)?;
|
|
||||||
|
|
||||||
Ok((format, alias, thumbnail_path, thumbnail_args))
|
Ok((format, alias, thumbnail_path, thumbnail_args))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ mod tests;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ImageFormat, VideoCodec},
|
config::{ImageFormat, VideoCodec},
|
||||||
|
formats::ProcessableFormat,
|
||||||
process::{Process, ProcessError},
|
process::{Process, ProcessError},
|
||||||
repo::Alias,
|
repo::Alias,
|
||||||
store::Store,
|
store::Store,
|
||||||
|
@ -416,14 +417,26 @@ pub(crate) async fn input_type_bytes(
|
||||||
Ok((details, input_type))
|
Ok((details, input_type))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_image(process_args: Vec<String>, format: ImageFormat) -> Result<Process, ProcessError> {
|
fn process_image(
|
||||||
|
process_args: Vec<String>,
|
||||||
|
format: ProcessableFormat,
|
||||||
|
) -> Result<Process, ProcessError> {
|
||||||
let command = "magick";
|
let command = "magick";
|
||||||
let convert_args = ["convert", "-"];
|
let convert_args = ["convert", "-"];
|
||||||
let last_arg = format!("{}:-", format.as_magick_format());
|
let last_arg = format!("{}:-", format.magick_format());
|
||||||
|
|
||||||
let mut args = Vec::with_capacity(process_args.len() + 3);
|
let len = if format.coalesce() {
|
||||||
|
process_args.len() + 4
|
||||||
|
} else {
|
||||||
|
process_args.len() + 3
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut args = Vec::with_capacity(len);
|
||||||
args.extend_from_slice(&convert_args[..]);
|
args.extend_from_slice(&convert_args[..]);
|
||||||
args.extend(process_args.iter().map(|s| s.as_str()));
|
args.extend(process_args.iter().map(|s| s.as_str()));
|
||||||
|
if format.coalesce() {
|
||||||
|
args.push("-coalesce");
|
||||||
|
}
|
||||||
args.push(&last_arg);
|
args.push(&last_arg);
|
||||||
|
|
||||||
Process::run(command, &args)
|
Process::run(command, &args)
|
||||||
|
@ -433,7 +446,7 @@ pub(crate) fn process_image_store_read<S: Store + 'static>(
|
||||||
store: S,
|
store: S,
|
||||||
identifier: S::Identifier,
|
identifier: S::Identifier,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
format: ImageFormat,
|
format: ProcessableFormat,
|
||||||
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
||||||
Ok(process_image(args, format)
|
Ok(process_image(args, format)
|
||||||
.map_err(MagickError::Process)?
|
.map_err(MagickError::Process)?
|
||||||
|
@ -443,7 +456,7 @@ pub(crate) fn process_image_store_read<S: Store + 'static>(
|
||||||
pub(crate) fn process_image_async_read<A: AsyncRead + Unpin + 'static>(
|
pub(crate) fn process_image_async_read<A: AsyncRead + Unpin + 'static>(
|
||||||
async_read: A,
|
async_read: A,
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
format: ImageFormat,
|
format: ProcessableFormat,
|
||||||
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
||||||
Ok(process_image(args, format)
|
Ok(process_image(args, format)
|
||||||
.map_err(MagickError::Process)?
|
.map_err(MagickError::Process)?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::ImageFormat,
|
|
||||||
error::Error,
|
error::Error,
|
||||||
|
formats::ProcessableFormat,
|
||||||
repo::{
|
repo::{
|
||||||
Alias, AliasRepo, DeleteToken, FullRepo, HashRepo, IdentifierRepo, QueueRepo, UploadId,
|
Alias, AliasRepo, DeleteToken, FullRepo, HashRepo, IdentifierRepo, QueueRepo, UploadId,
|
||||||
},
|
},
|
||||||
|
@ -67,10 +67,9 @@ enum Process {
|
||||||
identifier: Base64Bytes,
|
identifier: Base64Bytes,
|
||||||
upload_id: Serde<UploadId>,
|
upload_id: Serde<UploadId>,
|
||||||
declared_alias: Option<Serde<Alias>>,
|
declared_alias: Option<Serde<Alias>>,
|
||||||
should_validate: bool,
|
|
||||||
},
|
},
|
||||||
Generate {
|
Generate {
|
||||||
target_format: ImageFormat,
|
target_format: ProcessableFormat,
|
||||||
source: Serde<Alias>,
|
source: Serde<Alias>,
|
||||||
process_path: PathBuf,
|
process_path: PathBuf,
|
||||||
process_args: Vec<String>,
|
process_args: Vec<String>,
|
||||||
|
@ -128,13 +127,11 @@ pub(crate) async fn queue_ingest<R: QueueRepo>(
|
||||||
identifier: Vec<u8>,
|
identifier: Vec<u8>,
|
||||||
upload_id: UploadId,
|
upload_id: UploadId,
|
||||||
declared_alias: Option<Alias>,
|
declared_alias: Option<Alias>,
|
||||||
should_validate: bool,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let job = serde_json::to_vec(&Process::Ingest {
|
let job = serde_json::to_vec(&Process::Ingest {
|
||||||
identifier: Base64Bytes(identifier),
|
identifier: Base64Bytes(identifier),
|
||||||
declared_alias: declared_alias.map(Serde::new),
|
declared_alias: declared_alias.map(Serde::new),
|
||||||
upload_id: Serde::new(upload_id),
|
upload_id: Serde::new(upload_id),
|
||||||
should_validate,
|
|
||||||
})?;
|
})?;
|
||||||
repo.push(PROCESS_QUEUE, job.into()).await?;
|
repo.push(PROCESS_QUEUE, job.into()).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -142,7 +139,7 @@ pub(crate) async fn queue_ingest<R: QueueRepo>(
|
||||||
|
|
||||||
pub(crate) async fn queue_generate<R: QueueRepo>(
|
pub(crate) async fn queue_generate<R: QueueRepo>(
|
||||||
repo: &R,
|
repo: &R,
|
||||||
target_format: ImageFormat,
|
target_format: ProcessableFormat,
|
||||||
source: Alias,
|
source: Alias,
|
||||||
process_path: PathBuf,
|
process_path: PathBuf,
|
||||||
process_args: Vec<String>,
|
process_args: Vec<String>,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::ImageFormat,
|
|
||||||
error::Error,
|
error::Error,
|
||||||
|
formats::InputProcessableFormat,
|
||||||
ingest::Session,
|
ingest::Session,
|
||||||
queue::{Base64Bytes, LocalBoxFuture, Process},
|
queue::{Base64Bytes, LocalBoxFuture, Process},
|
||||||
repo::{Alias, DeleteToken, FullRepo, UploadId, UploadResult},
|
repo::{Alias, DeleteToken, FullRepo, UploadId, UploadResult},
|
||||||
|
@ -26,7 +26,6 @@ where
|
||||||
identifier: Base64Bytes(identifier),
|
identifier: Base64Bytes(identifier),
|
||||||
upload_id,
|
upload_id,
|
||||||
declared_alias,
|
declared_alias,
|
||||||
should_validate,
|
|
||||||
} => {
|
} => {
|
||||||
process_ingest(
|
process_ingest(
|
||||||
repo,
|
repo,
|
||||||
|
@ -34,7 +33,6 @@ where
|
||||||
identifier,
|
identifier,
|
||||||
Serde::into_inner(upload_id),
|
Serde::into_inner(upload_id),
|
||||||
declared_alias.map(Serde::into_inner),
|
declared_alias.map(Serde::into_inner),
|
||||||
should_validate,
|
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
@ -71,7 +69,6 @@ async fn process_ingest<R, S>(
|
||||||
unprocessed_identifier: Vec<u8>,
|
unprocessed_identifier: Vec<u8>,
|
||||||
upload_id: UploadId,
|
upload_id: UploadId,
|
||||||
declared_alias: Option<Alias>,
|
declared_alias: Option<Alias>,
|
||||||
should_validate: bool,
|
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
where
|
where
|
||||||
R: FullRepo + 'static,
|
R: FullRepo + 'static,
|
||||||
|
@ -85,8 +82,7 @@ where
|
||||||
.await?
|
.await?
|
||||||
.map_err(Error::from);
|
.map_err(Error::from);
|
||||||
|
|
||||||
let session =
|
let session = crate::ingest::ingest(repo, store, stream, declared_alias).await?;
|
||||||
crate::ingest::ingest(repo, store, stream, declared_alias, should_validate).await?;
|
|
||||||
|
|
||||||
let token = session.delete_token().await?;
|
let token = session.delete_token().await?;
|
||||||
|
|
||||||
|
@ -120,7 +116,7 @@ where
|
||||||
async fn generate<R: FullRepo, S: Store + 'static>(
|
async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
repo: &R,
|
repo: &R,
|
||||||
store: &S,
|
store: &S,
|
||||||
target_format: ImageFormat,
|
target_format: InputProcessableFormat,
|
||||||
source: Alias,
|
source: Alias,
|
||||||
process_path: PathBuf,
|
process_path: PathBuf,
|
||||||
process_args: Vec<String>,
|
process_args: Vec<String>,
|
||||||
|
@ -148,7 +144,7 @@ async fn generate<R: FullRepo, S: Store + 'static>(
|
||||||
source,
|
source,
|
||||||
process_path,
|
process_path,
|
||||||
process_args,
|
process_args,
|
||||||
original_details.to_input_format(),
|
original_details.input_format(),
|
||||||
None,
|
None,
|
||||||
hash,
|
hash,
|
||||||
)
|
)
|
||||||
|
|
111
src/validate.rs
111
src/validate.rs
|
@ -1,9 +1,11 @@
|
||||||
|
mod exiftool;
|
||||||
|
mod ffmpeg;
|
||||||
|
mod magick;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ImageFormat, MediaConfiguration},
|
|
||||||
either::Either,
|
either::Either,
|
||||||
error::{Error, UploadError},
|
error::Error,
|
||||||
ffmpeg::{FileFormat, TranscodeOptions},
|
formats::{AnimationOutput, ImageOutput, InputFile, InternalFormat, PrescribedFormats},
|
||||||
magick::ValidInputType,
|
|
||||||
};
|
};
|
||||||
use actix_web::web::Bytes;
|
use actix_web::web::Bytes;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
|
@ -38,71 +40,56 @@ impl AsyncRead for UnvalidatedBytes {
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub(crate) async fn validate_bytes(
|
pub(crate) async fn validate_bytes(
|
||||||
bytes: Bytes,
|
bytes: Bytes,
|
||||||
media: &MediaConfiguration,
|
prescribed: &PrescribedFormats,
|
||||||
validate: bool,
|
) -> Result<(InternalFormat, impl AsyncRead + Unpin), Error> {
|
||||||
) -> Result<(ValidInputType, impl AsyncRead + Unpin), Error> {
|
let discovery = crate::discover::discover_bytes(bytes.clone()).await?;
|
||||||
let (details, input_type) =
|
|
||||||
if let Some(tup) = crate::ffmpeg::input_type_bytes(bytes.clone()).await? {
|
match &discovery.input {
|
||||||
tup
|
InputFile::Image(input) => {
|
||||||
|
let ImageOutput {
|
||||||
|
format,
|
||||||
|
needs_transcode,
|
||||||
|
} = input.build_output(prescribed.image);
|
||||||
|
|
||||||
|
let read = if needs_transcode {
|
||||||
|
Either::left(Either::left(magick::convert_image(
|
||||||
|
input.format,
|
||||||
|
format,
|
||||||
|
bytes,
|
||||||
|
)?))
|
||||||
} else {
|
} else {
|
||||||
crate::magick::input_type_bytes(bytes.clone()).await?
|
Either::left(Either::right(exiftool::clear_metadata_bytes_read(bytes)?))
|
||||||
};
|
};
|
||||||
|
|
||||||
if !validate {
|
Ok((InternalFormat::Image(format), read))
|
||||||
return Ok((input_type, Either::left(UnvalidatedBytes::new(bytes))));
|
|
||||||
}
|
}
|
||||||
|
InputFile::Animation(input) => {
|
||||||
|
let AnimationOutput {
|
||||||
|
format,
|
||||||
|
needs_transcode,
|
||||||
|
} = input.build_output(prescribed.animation);
|
||||||
|
|
||||||
match (input_type.to_file_format(), media.format) {
|
let read = if needs_transcode {
|
||||||
(FileFormat::Video(video_format), _) => {
|
Either::right(Either::left(magick::convert_animation(
|
||||||
if !(media.enable_silent_video || media.enable_full_video) {
|
input.format,
|
||||||
return Err(UploadError::SilentVideoDisabled.into());
|
format,
|
||||||
}
|
bytes,
|
||||||
let transcode_options = TranscodeOptions::new(media, &details, video_format);
|
)?))
|
||||||
|
} else {
|
||||||
|
Either::right(Either::right(Either::left(
|
||||||
|
exiftool::clear_metadata_bytes_read(bytes)?,
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
|
||||||
if transcode_options.needs_reencode() {
|
Ok((InternalFormat::Animation(format), read))
|
||||||
Ok((
|
|
||||||
transcode_options.output_type(),
|
|
||||||
Either::right(Either::left(Either::left(
|
|
||||||
crate::ffmpeg::transcode_bytes(bytes, transcode_options).await?,
|
|
||||||
))),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok((
|
|
||||||
transcode_options.output_type(),
|
|
||||||
Either::right(Either::right(crate::exiftool::clear_metadata_bytes_read(
|
|
||||||
bytes,
|
|
||||||
)?)),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(FileFormat::Image(image_format), Some(format)) if image_format != format => Ok((
|
|
||||||
ValidInputType::from_format(format),
|
|
||||||
Either::right(Either::left(Either::right(
|
|
||||||
crate::magick::convert_bytes_read(bytes, format)?,
|
|
||||||
))),
|
|
||||||
)),
|
|
||||||
(FileFormat::Image(ImageFormat::Webp), _) => Ok((
|
|
||||||
ValidInputType::Webp,
|
|
||||||
Either::right(Either::left(Either::right(
|
|
||||||
crate::magick::convert_bytes_read(bytes, ImageFormat::Webp)?,
|
|
||||||
))),
|
|
||||||
)),
|
|
||||||
(FileFormat::Image(image_format), _) => {
|
|
||||||
if crate::exiftool::needs_reorienting(bytes.clone()).await? {
|
|
||||||
Ok((
|
|
||||||
ValidInputType::from_format(image_format),
|
|
||||||
Either::right(Either::left(Either::right(
|
|
||||||
crate::magick::convert_bytes_read(bytes, image_format)?,
|
|
||||||
))),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Ok((
|
|
||||||
ValidInputType::from_format(image_format),
|
|
||||||
Either::right(Either::right(crate::exiftool::clear_metadata_bytes_read(
|
|
||||||
bytes,
|
|
||||||
)?)),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
InputFile::Video(input) => {
|
||||||
|
let output = input.build_output(prescribed.video, prescribed.allow_audio);
|
||||||
|
let read = Either::right(Either::right(Either::right(
|
||||||
|
ffmpeg::transcode_bytes(*input, output, bytes).await?,
|
||||||
|
)));
|
||||||
|
|
||||||
|
Ok((InternalFormat::Video(output.internal_format()), read))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
src/validate/exiftool.rs
Normal file
12
src/validate/exiftool.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
use tokio::io::AsyncRead;
|
||||||
|
|
||||||
|
use crate::{exiftool::ExifError, process::Process};
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(input))]
|
||||||
|
pub(crate) fn clear_metadata_bytes_read(input: Bytes) -> Result<impl AsyncRead + Unpin, ExifError> {
|
||||||
|
let process =
|
||||||
|
Process::run("exiftool", &["-all=", "-", "-out", "-"]).map_err(ExifError::Process)?;
|
||||||
|
|
||||||
|
Ok(process.bytes_read(input))
|
||||||
|
}
|
112
src/validate/ffmpeg.rs
Normal file
112
src/validate/ffmpeg.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
use tokio::io::AsyncRead;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ffmpeg::FfMpegError,
|
||||||
|
formats::{OutputVideoFormat, VideoFormat},
|
||||||
|
process::Process,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) async fn transcode_bytes(
|
||||||
|
input_format: VideoFormat,
|
||||||
|
output_format: OutputVideoFormat,
|
||||||
|
bytes: Bytes,
|
||||||
|
) -> Result<impl AsyncRead + Unpin, FfMpegError> {
|
||||||
|
let input_file = crate::tmp_file::tmp_file(None);
|
||||||
|
let input_file_str = input_file.to_str().ok_or(FfMpegError::Path)?;
|
||||||
|
crate::store::file_store::safe_create_parent(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::CreateDir)?;
|
||||||
|
|
||||||
|
let mut tmp_one = crate::file::File::create(&input_file)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::CreateFile)?;
|
||||||
|
tmp_one
|
||||||
|
.write_from_bytes(bytes)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Write)?;
|
||||||
|
tmp_one.close().await.map_err(FfMpegError::CloseFile)?;
|
||||||
|
|
||||||
|
let output_file = crate::tmp_file::tmp_file(None);
|
||||||
|
let output_file_str = output_file.to_str().ok_or(FfMpegError::Path)?;
|
||||||
|
|
||||||
|
transcode_files(input_file_str, input_format, output_file_str, output_format).await?;
|
||||||
|
|
||||||
|
let tmp_two = crate::file::File::open(&output_file)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::OpenFile)?;
|
||||||
|
let stream = tmp_two
|
||||||
|
.read_to_stream(None, None)
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::ReadFile)?;
|
||||||
|
let reader = tokio_util::io::StreamReader::new(stream);
|
||||||
|
let clean_reader = crate::tmp_file::cleanup_tmpfile(reader, output_file);
|
||||||
|
|
||||||
|
Ok(Box::pin(clean_reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn transcode_files(
|
||||||
|
input_path: &str,
|
||||||
|
input_format: VideoFormat,
|
||||||
|
output_path: &str,
|
||||||
|
output_format: OutputVideoFormat,
|
||||||
|
) -> Result<(), FfMpegError> {
|
||||||
|
if let Some(audio_codec) = output_format.ffmpeg_audio_codec() {
|
||||||
|
Process::run(
|
||||||
|
"ffmpeg",
|
||||||
|
&[
|
||||||
|
"-hide_banner",
|
||||||
|
"-v",
|
||||||
|
"warning",
|
||||||
|
"-f",
|
||||||
|
input_format.ffmpeg_format(),
|
||||||
|
"-i",
|
||||||
|
input_path,
|
||||||
|
"-pix_fmt",
|
||||||
|
output_format.pix_fmt(),
|
||||||
|
"-vf",
|
||||||
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||||
|
"-c:a",
|
||||||
|
audio_codec,
|
||||||
|
"-c:v",
|
||||||
|
output_format.ffmpeg_video_codec(),
|
||||||
|
"-f",
|
||||||
|
output_format.ffmpeg_format(),
|
||||||
|
output_path,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(FfMpegError::Process)?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Process)?;
|
||||||
|
} else {
|
||||||
|
Process::run(
|
||||||
|
"ffmpeg",
|
||||||
|
&[
|
||||||
|
"-hide_banner",
|
||||||
|
"-v",
|
||||||
|
"warning",
|
||||||
|
"-f",
|
||||||
|
input_format.ffmpeg_format(),
|
||||||
|
"-i",
|
||||||
|
input_path,
|
||||||
|
"-pix_fmt",
|
||||||
|
output_format.pix_fmt(),
|
||||||
|
"-vf",
|
||||||
|
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||||
|
"-an",
|
||||||
|
"-c:v",
|
||||||
|
output_format.ffmpeg_video_codec(),
|
||||||
|
"-f",
|
||||||
|
output_format.ffmpeg_format(),
|
||||||
|
output_path,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(FfMpegError::Process)?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(FfMpegError::Process)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
39
src/validate/magick.rs
Normal file
39
src/validate/magick.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use actix_web::web::Bytes;
|
||||||
|
use tokio::io::AsyncRead;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
formats::{AnimationFormat, ImageFormat},
|
||||||
|
magick::MagickError,
|
||||||
|
process::Process,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(super) fn convert_image(
|
||||||
|
input: ImageFormat,
|
||||||
|
output: ImageFormat,
|
||||||
|
bytes: Bytes,
|
||||||
|
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
||||||
|
let input_arg = format!("{}:-", input.magick_format());
|
||||||
|
let output_arg = format!("{}:-", output.magick_format());
|
||||||
|
|
||||||
|
let process = Process::run(
|
||||||
|
"magick",
|
||||||
|
&["-strip", "-auto-orient", &input_arg, &output_arg],
|
||||||
|
)
|
||||||
|
.map_err(MagickError::Process)?;
|
||||||
|
|
||||||
|
Ok(process.bytes_read(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn convert_animation(
|
||||||
|
input: AnimationFormat,
|
||||||
|
output: AnimationFormat,
|
||||||
|
bytes: Bytes,
|
||||||
|
) -> Result<impl AsyncRead + Unpin, MagickError> {
|
||||||
|
let input_arg = format!("{}:-", input.magick_format());
|
||||||
|
let output_arg = format!("{}:-", output.magick_format());
|
||||||
|
|
||||||
|
let process = Process::run("magick", &["-strip", &input_arg, "-coalesce", &output_arg])
|
||||||
|
.map_err(MagickError::Process)?;
|
||||||
|
|
||||||
|
Ok(process.bytes_read(bytes))
|
||||||
|
}
|
Loading…
Reference in a new issue