mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-11-20 11:21:14 +00:00
276 lines
7.1 KiB
Rust
276 lines
7.1 KiB
Rust
mod exiftool;
|
|
mod ffmpeg;
|
|
mod magick;
|
|
|
|
use crate::{
|
|
discover::Discovery,
|
|
error::Error,
|
|
error_code::ErrorCode,
|
|
formats::{
|
|
AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InputVideoFormat,
|
|
InternalFormat, Validations,
|
|
},
|
|
process::ProcessRead,
|
|
tmp_file::TmpDir,
|
|
};
|
|
use actix_web::web::Bytes;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub(crate) enum ValidationError {
|
|
#[error("Too wide")]
|
|
Width,
|
|
|
|
#[error("Too tall")]
|
|
Height,
|
|
|
|
#[error("Too many pixels")]
|
|
Area,
|
|
|
|
#[error("Too many frames")]
|
|
Frames,
|
|
|
|
#[error("Uploaded file is empty")]
|
|
Empty,
|
|
|
|
#[error("Filesize too large")]
|
|
Filesize,
|
|
|
|
#[error("Video is disabled")]
|
|
VideoDisabled,
|
|
}
|
|
|
|
impl ValidationError {
|
|
pub(crate) const fn error_code(&self) -> ErrorCode {
|
|
match self {
|
|
Self::Width => ErrorCode::VALIDATE_WIDTH,
|
|
Self::Height => ErrorCode::VALIDATE_HEIGHT,
|
|
Self::Area => ErrorCode::VALIDATE_AREA,
|
|
Self::Frames => ErrorCode::VALIDATE_FRAMES,
|
|
Self::Empty => ErrorCode::VALIDATE_FILE_EMPTY,
|
|
Self::Filesize => ErrorCode::VALIDATE_FILE_SIZE,
|
|
Self::VideoDisabled => ErrorCode::VIDEO_DISABLED,
|
|
}
|
|
}
|
|
}
|
|
|
|
const MEGABYTES: usize = 1024 * 1024;
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
pub(crate) async fn validate_bytes(
|
|
tmp_dir: &TmpDir,
|
|
bytes: Bytes,
|
|
validations: Validations<'_>,
|
|
timeout: u64,
|
|
) -> Result<(InternalFormat, ProcessRead), Error> {
|
|
if bytes.is_empty() {
|
|
return Err(ValidationError::Empty.into());
|
|
}
|
|
|
|
let Discovery {
|
|
input,
|
|
width,
|
|
height,
|
|
frames,
|
|
} = crate::discover::discover_bytes(tmp_dir, timeout, bytes.clone()).await?;
|
|
|
|
match &input {
|
|
InputFile::Image(input) => {
|
|
let (format, process_read) = process_image(
|
|
tmp_dir,
|
|
bytes,
|
|
*input,
|
|
width,
|
|
height,
|
|
validations.image,
|
|
timeout,
|
|
)
|
|
.await?;
|
|
|
|
Ok((format, process_read))
|
|
}
|
|
InputFile::Animation(input) => {
|
|
let (format, process_read) = process_animation(
|
|
tmp_dir,
|
|
bytes,
|
|
*input,
|
|
width,
|
|
height,
|
|
frames.unwrap_or(1),
|
|
validations.animation,
|
|
timeout,
|
|
)
|
|
.await?;
|
|
|
|
Ok((format, process_read))
|
|
}
|
|
InputFile::Video(input) => {
|
|
let (format, process_read) = process_video(
|
|
tmp_dir,
|
|
bytes,
|
|
*input,
|
|
width,
|
|
height,
|
|
frames.unwrap_or(1),
|
|
validations.video,
|
|
timeout,
|
|
)
|
|
.await?;
|
|
|
|
Ok((format, process_read))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tracing::instrument(skip(tmp_dir, bytes, validations))]
|
|
async fn process_image(
|
|
tmp_dir: &TmpDir,
|
|
bytes: Bytes,
|
|
input: ImageInput,
|
|
width: u16,
|
|
height: u16,
|
|
validations: &crate::config::Image,
|
|
timeout: u64,
|
|
) -> Result<(InternalFormat, ProcessRead), Error> {
|
|
if width > validations.max_width {
|
|
return Err(ValidationError::Width.into());
|
|
}
|
|
if height > validations.max_height {
|
|
return Err(ValidationError::Height.into());
|
|
}
|
|
if u32::from(width) * u32::from(height) > validations.max_area {
|
|
return Err(ValidationError::Area.into());
|
|
}
|
|
if bytes.len() > validations.max_file_size * MEGABYTES {
|
|
return Err(ValidationError::Filesize.into());
|
|
}
|
|
|
|
let ImageOutput {
|
|
format,
|
|
needs_transcode,
|
|
} = input.build_output(validations.format);
|
|
|
|
let process_read = if needs_transcode {
|
|
let quality = validations.quality_for(format);
|
|
|
|
magick::convert_image(tmp_dir, input.format, format, quality, timeout, bytes).await?
|
|
} else {
|
|
exiftool::clear_metadata_bytes_read(bytes, timeout)?
|
|
};
|
|
|
|
Ok((InternalFormat::Image(format), process_read))
|
|
}
|
|
|
|
fn validate_animation(
|
|
size: usize,
|
|
width: u16,
|
|
height: u16,
|
|
frames: u32,
|
|
validations: &crate::config::Animation,
|
|
) -> Result<(), ValidationError> {
|
|
if width > validations.max_width {
|
|
return Err(ValidationError::Width);
|
|
}
|
|
if height > validations.max_height {
|
|
return Err(ValidationError::Height);
|
|
}
|
|
if u32::from(width) * u32::from(height) > validations.max_area {
|
|
return Err(ValidationError::Area);
|
|
}
|
|
if frames > validations.max_frame_count {
|
|
return Err(ValidationError::Frames);
|
|
}
|
|
if size > validations.max_file_size * MEGABYTES {
|
|
return Err(ValidationError::Filesize);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[tracing::instrument(skip(tmp_dir, bytes, validations))]
|
|
async fn process_animation(
|
|
tmp_dir: &TmpDir,
|
|
bytes: Bytes,
|
|
input: AnimationFormat,
|
|
width: u16,
|
|
height: u16,
|
|
frames: u32,
|
|
validations: &crate::config::Animation,
|
|
timeout: u64,
|
|
) -> Result<(InternalFormat, ProcessRead), Error> {
|
|
validate_animation(bytes.len(), width, height, frames, validations)?;
|
|
|
|
let AnimationOutput {
|
|
format,
|
|
needs_transcode,
|
|
} = input.build_output(validations.format);
|
|
|
|
let process_read = if needs_transcode {
|
|
let quality = validations.quality_for(format);
|
|
|
|
magick::convert_animation(tmp_dir, input, format, quality, timeout, bytes).await?
|
|
} else {
|
|
exiftool::clear_metadata_bytes_read(bytes, timeout)?
|
|
};
|
|
|
|
Ok((InternalFormat::Animation(format), process_read))
|
|
}
|
|
|
|
fn validate_video(
|
|
size: usize,
|
|
width: u16,
|
|
height: u16,
|
|
frames: u32,
|
|
validations: &crate::config::Video,
|
|
) -> Result<(), ValidationError> {
|
|
if !validations.enable {
|
|
return Err(ValidationError::VideoDisabled);
|
|
}
|
|
if width > validations.max_width {
|
|
return Err(ValidationError::Width);
|
|
}
|
|
if height > validations.max_height {
|
|
return Err(ValidationError::Height);
|
|
}
|
|
if u32::from(width) * u32::from(height) > validations.max_area {
|
|
return Err(ValidationError::Area);
|
|
}
|
|
if frames > validations.max_frame_count {
|
|
return Err(ValidationError::Frames);
|
|
}
|
|
if size > validations.max_file_size * MEGABYTES {
|
|
return Err(ValidationError::Filesize);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[tracing::instrument(skip(tmp_dir, bytes, validations))]
|
|
async fn process_video(
|
|
tmp_dir: &TmpDir,
|
|
bytes: Bytes,
|
|
input: InputVideoFormat,
|
|
width: u16,
|
|
height: u16,
|
|
frames: u32,
|
|
validations: &crate::config::Video,
|
|
timeout: u64,
|
|
) -> Result<(InternalFormat, ProcessRead), Error> {
|
|
validate_video(bytes.len(), width, height, frames, validations)?;
|
|
|
|
let output = input.build_output(
|
|
validations.video_codec,
|
|
validations.audio_codec,
|
|
validations.allow_audio,
|
|
);
|
|
|
|
let crf = validations.crf_for(width, height);
|
|
|
|
let process_read = ffmpeg::transcode_bytes(tmp_dir, input, output, crf, timeout, bytes).await?;
|
|
|
|
Ok((
|
|
InternalFormat::Video(output.format.internal_format()),
|
|
process_read,
|
|
))
|
|
}
|