diff --git a/Cargo.lock b/Cargo.lock index 46d8aac..5511941 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,7 +958,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "picturium" -version = "0.1.1" +version = "0.1.2" dependencies = [ "actix-cors", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index f121e94..1f94466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "picturium" -version = "0.1.1" +version = "0.1.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index cd2fc0f..887efd6 100755 --- a/README.md +++ b/README.md @@ -67,11 +67,20 @@ Supports all file formats in pass-through mode, but some of them get special tre - old cached files are periodically purged from disk +## Serving files + +All files are served from the working directory. The working directory in docker images is located at `/app`.\ +For example file located at `/app/data/image.jpeg` will be available at `https://.../data/image.jpeg`. + + ## Token authorization -- picturium supports token authorization of requests to protect against bots or other unwanted traffic -- if environment variable `KEY` is not set, token authorization will be disabled, otherwise each request needs to be signed with SHA256 HMAC token -- token is generated from file path + all URL parameters except `token` parameter, sorted alphabetically (check out `RawUrlParameters::verify_token` in [src/parameters/mod.rs](https://github.com/lamka02sk/picturium/blob/master/src/parameters/mod.rs) for more) +- by default, picturium **requires** token authorization of all requests to protect against unwanted traffic +- you can disable token authorization by completely removing `KEY` environment variable from `.env` file +- tokens are SHA256 HMAC authentication codes +- token is generated from file path + all URL parameters except the `token` parameter, sorted alphabetically (check out `RawUrlParameters::verify_token` in [src/parameters/mod.rs](https://github.com/lamka02sk/picturium/blob/master/src/parameters/mod.rs) for more) + +- [How to generate token with PHP](examples/generate_token.php) ## URL GET parameters @@ -135,3 +144,31 @@ The original image will be processed, rotated left by 90 degrees, resized to be ```url https://example.com/folder/test.jpg?token=fsd5f4sd5f4&w=160&q=50&dpr=2&rot=left ``` + +## Limitations + +picturium uses a few libraries that enforce limits on the size of images that can be processed. +We tried to discover and tailor these limits to ensure stability and good (not only) developer experience. + +### PNG +Maximum output image resolution: `16384 x 16384 px` (reason: quantization) + +### WebP +Maximum output image resolution: `16383 x 16383 px` (reason: WebP format limitation)\ +Maximum total output image resolution: `170 megapixels` (reason: `cwebp` internal limitations) + +### AVIF +Maximum output image resolution: `16384 x 16384 px` (reason: `libvips` internal limitation) + +### SVG +Images included in SVG files (`xlink:href`), cannot exceed memory limit of 512 MB +(https://gitlab.gnome.org/GNOME/librsvg/-/issues/1093) due to default configuration of `image` crate +which cannot be increased through both `librsvg` and `libvips`. + +According to test files found in `image` crate, the memory needed to process the image (with reserve) can be calculated like this: + +``` +{image_width} * {image_height} * 5 / 1024 / 1024 +``` + +We recommend including images with maximum resolution of `105 megapixels` (or for example `10000 x 10500 px`). diff --git a/examples/generate_token.php b/examples/generate_token.php new file mode 100755 index 0000000..fb3e495 --- /dev/null +++ b/examples/generate_token.php @@ -0,0 +1,7 @@ +, output_format: OutputFormat // crop::run(&image, &url_parameters, &output_format).await?; // } + let output_format = validate_output_format(&image, url_parameters, &output_format)?; + if url_parameters.width.is_some() || url_parameters.height.is_some() { image = resize::run(image, url_parameters).await?; } diff --git a/src/pipeline/resize.rs b/src/pipeline/resize.rs index b0aa915..ec973a9 100755 --- a/src/pipeline/resize.rs +++ b/src/pipeline/resize.rs @@ -71,7 +71,6 @@ fn get_pipeline_dimensions(image: &VipsImage, url_parameters: &UrlParameters<'_> } let (original_width, original_height) = get_original_dimensions(image); - let ratio = original_width as f64 / original_height as f64; if width.is_none() { diff --git a/src/services/formats.rs b/src/services/formats.rs index bde24f1..81afbe6 100755 --- a/src/services/formats.rs +++ b/src/services/formats.rs @@ -2,10 +2,23 @@ use std::env; use std::fmt::Display; use std::path::Path; use actix_web::http::header::HeaderValue; +use libvips::VipsImage; +use log::{error, warn}; use crate::parameters::format::Format; use crate::parameters::UrlParameters; +use crate::pipeline::{PipelineError, PipelineResult}; -#[derive(Debug, PartialEq)] +const WEBP_MAX_WIDTH: i32 = 16383; // px +const WEBP_MAX_HEIGHT: i32 = 16383; // px +const WEBP_MAX_RESOLUTION: f64 = 170.0; // MPix + +const AVIF_MAX_WIDTH: i32 = 16384; // px +const AVIF_MAX_HEIGHT: i32 = 16384; // px + +const PNG_MAX_WIDTH: i32 = 16384; // px +const PNG_MAX_HEIGHT: i32 = 16384; // px + +#[derive(Debug, Clone, PartialEq)] pub enum OutputFormat { Avif, Webp, @@ -105,4 +118,62 @@ pub fn is_generated(path: &Path) -> bool { pub fn supports_transparency(path: &Path) -> bool { let extension = get_extension(path).unwrap_or_else(|_| String::new()); !matches!(extension.as_str(), "jpg" | "jpeg") +} + +pub fn validate_output_format(image: &VipsImage, url_parameters: &UrlParameters<'_>, output_format: &OutputFormat) -> PipelineResult { + match output_format { + OutputFormat::Webp => { + let (width, height) = (image.get_width(), image.get_height()); + let downsize = width > WEBP_MAX_WIDTH || height > WEBP_MAX_HEIGHT || (width * height) as f64 > WEBP_MAX_RESOLUTION; + + if !downsize { + return Ok(output_format.clone()); + } + + if url_parameters.format != Format::Auto { + error!("WEBP output image is too large (max. {WEBP_MAX_WIDTH}x{WEBP_MAX_HEIGHT} or {WEBP_MAX_RESOLUTION} MPix)"); + return Err(PipelineError("Failed to save image: too large".to_string())); + } + + warn!("Very large image, falling back to JPEG/PNG format"); + + Ok(match image.image_hasalpha() && width <= PNG_MAX_WIDTH && height <= PNG_MAX_HEIGHT { + true => OutputFormat::Png, + false => OutputFormat::Jpg, + }) + }, + OutputFormat::Avif => { + let (width, height) = (image.get_width(), image.get_height()); + let downsize = width > AVIF_MAX_WIDTH || height > AVIF_MAX_HEIGHT; + + if !downsize { + return Ok(output_format.clone()); + } + + if url_parameters.format != Format::Auto { + error!("AVIF output image is too large (max. {AVIF_MAX_WIDTH}x{AVIF_MAX_HEIGHT})"); + return Err(PipelineError("Failed to save image: too large".to_string())); + } + + warn!("Very large image, falling back to JPEG format"); + Ok(OutputFormat::Jpg) + }, + OutputFormat::Png => { + let (width, height) = (image.get_width(), image.get_height()); + let downsize = width > PNG_MAX_WIDTH || height > PNG_MAX_HEIGHT; + + if !downsize { + return Ok(output_format.clone()); + } + + if url_parameters.format != Format::Auto { + error!("PNG output image is too large (max. {PNG_MAX_WIDTH}x{PNG_MAX_HEIGHT})"); + return Err(PipelineError("Failed to save image: too large".to_string())); + } + + warn!("Very large image, falling back to JPEG format"); + Ok(OutputFormat::Jpg) + }, + _ => Ok(output_format.clone()) + } } \ No newline at end of file