diff --git a/README.md b/README.md index c49d3b0..6da0eec 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Options: --media-filters Which media filters should be enabled on the `process` endpoint --media-format - Enforce uploaded media is transcoded to the provided format [possible values: jpeg, webp, png] + Enforce uploaded media is transcoded to the provided format [possible values: avif, jpeg, jxl, png, webp] -h, --help Print help information (use `--help` for more detail) ``` @@ -383,7 +383,7 @@ pict-rs offers the following endpoints: aspect ratio. For example, a 1600x900 image cropped with a 1x1 aspect ratio will become 900x900. A 1600x1100 image cropped with a 16x9 aspect ratio will become 1600x900. - Supported `ext` file extensions include `png`, `jpg`, and `webp` + Supported `ext` file extensions include `avif`, `jpg`, `jxl`, `png`, and `webp` An example of usage could be ``` diff --git a/pict-rs.toml b/pict-rs.toml index c257683..c5688bd 100644 --- a/pict-rs.toml +++ b/pict-rs.toml @@ -188,10 +188,10 @@ filters = ['blur', 'crop', 'identity', 'resize', 'thumbnail'] # environment variable: PICTRS__MEDIA__FORMAT # default: empty # -# available options: png, jpeg, webp -# When set, all uploaded still images will be converted to this file type. If you care about file -# size, setting this to 'webp' is probably the best option. By default, images are stored in their -# original file type. +# available options: avif, png, jpeg, jxl, webp +# When set, all uploaded still images will be converted to this file type. For balancing quality vs +# file size vs browser support, 'avif', 'jxl', and 'webp' should be considered. By default, images +# are stored in their original file type. format = "webp" ## Optional: whether to validate images uploaded through the `import` endpoint diff --git a/src/config/primitives.rs b/src/config/primitives.rs index f2179fe..dbc9bf5 100644 --- a/src/config/primitives.rs +++ b/src/config/primitives.rs @@ -40,9 +40,11 @@ pub(crate) enum LogFormat { )] #[serde(rename_all = "snake_case")] pub(crate) enum ImageFormat { + Avif, Jpeg, - Webp, + Jxl, Png, + Webp, } #[derive( @@ -158,7 +160,9 @@ impl ImageFormat { pub(crate) fn as_magick_format(self) -> &'static str { match self { + Self::Avif => "AVIF", Self::Jpeg => "JPEG", + Self::Jxl => "JXL", Self::Png => "PNG", Self::Webp => "WEBP", } @@ -166,7 +170,9 @@ impl ImageFormat { pub(crate) fn as_ext(self) -> &'static str { match self { + Self::Avif => ".avif", Self::Jpeg => ".jpeg", + Self::Jxl => ".jxl", Self::Png => ".png", Self::Webp => ".webp", } @@ -243,7 +249,9 @@ impl FromStr for ImageFormat { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { + "avif" => Ok(Self::Avif), "jpeg" | "jpg" => Ok(Self::Jpeg), + "jxl" => Ok(Self::Jxl), "png" => Ok(Self::Png), "webp" => Ok(Self::Webp), other => Err(format!("Invalid variant: {other}")), diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index 5e3bf0e..71ffa56 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -222,7 +222,9 @@ impl ValidInputType { Self::Gif => FileFormat::Video(VideoFormat::Gif), Self::Mp4 => FileFormat::Video(VideoFormat::Mp4), Self::Webm => FileFormat::Video(VideoFormat::Webm), + Self::Avif => FileFormat::Image(ImageFormat::Avif), Self::Jpeg => FileFormat::Image(ImageFormat::Jpeg), + Self::Jxl => FileFormat::Image(ImageFormat::Jxl), Self::Png => FileFormat::Image(ImageFormat::Png), Self::Webp => FileFormat::Image(ImageFormat::Webp), } @@ -472,7 +474,7 @@ fn parse_details(output: std::borrow::Cow<'_, str>) -> Result, E for (k, v) in FORMAT_MAPPINGS { if formats.contains(k) { - return Ok(Some(parse_details_inner(width, height, frames, *v)?)); + return parse_details_inner(width, height, frames, *v); } } @@ -484,17 +486,22 @@ fn parse_details_inner( height: &str, frames: &str, format: VideoFormat, -) -> Result { +) -> Result, Error> { let width = width.parse().map_err(|_| UploadError::UnsupportedFormat)?; let height = height.parse().map_err(|_| UploadError::UnsupportedFormat)?; let frames = frames.parse().map_err(|_| UploadError::UnsupportedFormat)?; - Ok(Details { + // Probably a still image. ffmpeg thinks AVIF is an mp4 + if frames == 1 { + return Ok(None); + } + + Ok(Some(Details { mime_type: format.to_mime(), width, height, frames: Some(frames), - }) + })) } async fn pixel_format(input_file: &str) -> Result { diff --git a/src/magick.rs b/src/magick.rs index 8e75dbe..71012e4 100644 --- a/src/magick.rs +++ b/src/magick.rs @@ -22,6 +22,14 @@ pub(crate) fn details_hint(alias: &Alias) -> Option { } } +fn image_avif() -> mime::Mime { + "image/avif".parse().unwrap() +} + +fn image_jxl() -> mime::Mime { + "image/jxl".parse().unwrap() +} + fn image_webp() -> mime::Mime { "image/webp".parse().unwrap() } @@ -39,8 +47,10 @@ pub(crate) enum ValidInputType { Mp4, Webm, Gif, - Png, + Avif, Jpeg, + Jxl, + Png, Webp, } @@ -50,8 +60,10 @@ impl ValidInputType { Self::Mp4 => "MP4", Self::Webm => "WEBM", Self::Gif => "GIF", - Self::Png => "PNG", + Self::Avif => "AVIF", Self::Jpeg => "JPEG", + Self::Jxl => "JXL", + Self::Png => "PNG", Self::Webp => "WEBP", } } @@ -61,8 +73,10 @@ impl ValidInputType { Self::Mp4 => ".mp4", Self::Webm => ".webm", Self::Gif => ".gif", - Self::Png => ".png", + Self::Avif => ".avif", Self::Jpeg => ".jpeg", + Self::Jxl => ".jxl", + Self::Png => ".png", Self::Webp => ".webp", } } @@ -89,7 +103,9 @@ impl ValidInputType { pub(crate) const fn from_format(format: ImageFormat) -> Self { match format { + ImageFormat::Avif => ValidInputType::Avif, ImageFormat::Jpeg => ValidInputType::Jpeg, + ImageFormat::Jxl => ValidInputType::Jxl, ImageFormat::Png => ValidInputType::Png, ImageFormat::Webp => ValidInputType::Webp, } @@ -97,7 +113,9 @@ impl ValidInputType { pub(crate) const fn to_format(self) -> Option { match self { + Self::Avif => Some(ImageFormat::Avif), Self::Jpeg => Some(ImageFormat::Jpeg), + Self::Jxl => Some(ImageFormat::Jxl), Self::Png => Some(ImageFormat::Png), Self::Webp => Some(ImageFormat::Webp), _ => None, @@ -273,8 +291,10 @@ fn parse_details(s: std::borrow::Cow<'_, str>) -> Result { "MP4" => video_mp4(), "WEBM" => video_webm(), "GIF" => mime::IMAGE_GIF, - "PNG" => mime::IMAGE_PNG, + "AVIF" => image_avif(), "JPEG" => mime::IMAGE_JPEG, + "JXL" => image_jxl(), + "PNG" => mime::IMAGE_PNG, "WEBP" => image_webp(), _ => return Err(UploadError::UnsupportedFormat.into()), }; @@ -343,8 +363,10 @@ impl Details { (mime::VIDEO, mime::MP4 | mime::MPEG) => ValidInputType::Mp4, (mime::VIDEO, subtype) if subtype.as_str() == "webm" => ValidInputType::Webm, (mime::IMAGE, mime::GIF) => ValidInputType::Gif, - (mime::IMAGE, mime::PNG) => ValidInputType::Png, + (mime::IMAGE, subtype) if subtype.as_str() == "avif" => ValidInputType::Avif, (mime::IMAGE, mime::JPEG) => ValidInputType::Jpeg, + (mime::IMAGE, subtype) if subtype.as_str() == "jxl" => ValidInputType::Jxl, + (mime::IMAGE, mime::PNG) => ValidInputType::Png, (mime::IMAGE, subtype) if subtype.as_str() == "webp" => ValidInputType::Webp, _ => return Err(UploadError::UnsupportedFormat.into()), };