Skip to content

Commit

Permalink
- Added output limits to better handle very large images
Browse files Browse the repository at this point in the history
- Improved README and token generation example
  • Loading branch information
lamka02sk committed Nov 18, 2024
1 parent cb1d432 commit 6c9dcdb
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
43 changes: 40 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`).
7 changes: 7 additions & 0 deletions examples/generate_token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

hash_hmac(
/* algorithm */ 'sha256',
/* file URL with alphabetically sorted parameters */ 'data/photo.jpg?dpr=2&q=10&w=100',
/* picturium KEY */ 'bP47vDOPMqIUTDEeO1TTw1HOg1Y4HUrZcDTZdELhxM11ApHgwZKakB61zbFXRG'
);
3 changes: 0 additions & 3 deletions examples/hmac.php

This file was deleted.

4 changes: 3 additions & 1 deletion src/pipeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use log::debug;

use crate::cache;
use crate::parameters::{Rotate, UrlParameters};
use crate::services::formats::{is_svg, OutputFormat, supports_transparency};
use crate::services::formats::{is_svg, OutputFormat, supports_transparency, validate_output_format};

mod thumbnail;
mod rotate;
Expand Down Expand Up @@ -39,6 +39,8 @@ pub async fn run(url_parameters: &UrlParameters<'_>, 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?;
}
Expand Down
1 change: 0 additions & 1 deletion src/pipeline/resize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
73 changes: 72 additions & 1 deletion src/services/formats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<OutputFormat> {
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())
}
}

0 comments on commit 6c9dcdb

Please sign in to comment.