Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
name: Rust Tests

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
push:
branches: ["main"]
pull_request:
branches: ["main"]

permissions:
contents: read
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
CARGO_TERM_COLOR: always

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.92.0

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@1.92.0
- name: Install dependencies
run: sudo apt install -y libdav1d7 libdav1d-dev nasm

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2

- name: Run tests
run: cargo test --all-features --verbose
- name: Run tests
run: cargo test
20 changes: 20 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
webp = "0.3.1"

[dev-dependencies]
tempfile = "3"

[profile.release.package."*"]
opt-level = 3
codegen-units = 1
Expand Down
26 changes: 25 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::operations::resize::ResizeAlgorithm;

#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct EncodingConfig {
// JPEG encoding parameters
pub jpeg_quality: u8,
Expand Down Expand Up @@ -108,3 +108,27 @@ impl EncodingConfig {
}
}
}

impl Default for EncodingConfig {
fn default() -> Self {
Self {
jpeg_quality: 75,
png_compression_level: 6,
avif_quality: 75,
avif_speed: 7,
webp_quality: 75,
webp_effort: 4,
resize_algorithm: ResizeAlgorithm::Auto,
root_path: "/tmp/test-images".to_string(),
strip_path: None,
fallback_image_url: None,
fallback_image_max_size: 5 * 1024 * 1024,
enable_cache: false,
cache_memory_size: 100 * 1024 * 1024,
enable_disk_cache: false,
cache_disk_size: 512 * 1024 * 1024,
cache_disk_path: "./cache".to_string(),
cache_memory_max_item_size: 1024 * 1024,
}
}
}
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub mod api;
pub mod cache;
pub mod config;
pub mod logs;
pub mod metrics;
pub mod operations;
pub mod utils;
15 changes: 4 additions & 11 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,17 @@ use std::{sync::Arc, time::Duration};

use actix_web::{App, HttpServer, middleware, web};

use crate::{
use image_proxy::{
api::image::process_image_request, api::metrics::metrics_handler, config::EncodingConfig,
};

mod api;
mod cache;
mod config;
mod logs;
mod metrics;
mod operations;
mod utils;

#[actix_web::main]
async fn main() -> anyhow::Result<()> {
crate::logs::setup_tracing();
image_proxy::logs::setup_tracing();
let config = Arc::new(EncodingConfig::from_env());
let (prometheus_registry, pipeline_duration, request_count) = crate::metrics::setup_metrics();
let hybrid_cache = crate::cache::setup_cache(&config, &prometheus_registry).await?;
let (prometheus_registry, pipeline_duration, request_count) = image_proxy::metrics::setup_metrics();
let hybrid_cache = image_proxy::cache::setup_cache(&config, &prometheus_registry).await?;

HttpServer::new(move || {
let http_client = awc::ClientBuilder::new()
Expand Down
121 changes: 121 additions & 0 deletions src/operations/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,124 @@ pub fn convert_image_format(

Ok(buffer)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::config::EncodingConfig;

fn test_config() -> EncodingConfig {
EncodingConfig::default()
}

fn make_rgb_image(w: u32, h: u32) -> DynamicImage {
DynamicImage::ImageRgba8(image::RgbaImage::from_fn(w, h, |x, y| {
image::Rgba([(x % 256) as u8, (y % 256) as u8, 128, 255])
}))
}

#[test]
fn convert_to_jpeg() {
let img = make_rgb_image(64, 64);
let config = test_config();
let bytes = convert_image_format(img, Some("jpeg"), &config).unwrap();
// JPEG files start with FFD8
assert!(bytes.len() > 2);
assert_eq!(bytes[0], 0xFF);
assert_eq!(bytes[1], 0xD8);
}

#[test]
fn convert_to_jpg_alias() {
let img = make_rgb_image(64, 64);
let config = test_config();
let bytes = convert_image_format(img, Some("jpg"), &config).unwrap();
assert_eq!(bytes[0], 0xFF);
assert_eq!(bytes[1], 0xD8);
}

#[test]
fn convert_to_png() {
let img = make_rgb_image(64, 64);
let config = test_config();
let bytes = convert_image_format(img, Some("png"), &config).unwrap();
// PNG magic bytes: 89 50 4E 47
assert!(bytes.len() > 4);
assert_eq!(&bytes[0..4], &[0x89, 0x50, 0x4E, 0x47]);
}

#[test]
fn convert_to_webp() {
let img = make_rgb_image(64, 64);
let config = test_config();
let bytes = convert_image_format(img, Some("webp"), &config).unwrap();
// WebP starts with RIFF....WEBP
assert!(bytes.len() > 12);
assert_eq!(&bytes[0..4], b"RIFF");
assert_eq!(&bytes[8..12], b"WEBP");
}

#[test]
fn convert_to_avif() {
let img = make_rgb_image(64, 64);
let config = test_config();
let bytes = convert_image_format(img, Some("avif"), &config).unwrap();
// AVIF files contain the ftyp box with "avif" brand
assert!(bytes.len() > 12);
let content = String::from_utf8_lossy(&bytes);
assert!(content.contains("ftyp") || content.contains("avif"));
}

#[test]
fn convert_none_format_returns_raw_bytes() {
let img = make_rgb_image(4, 4);
let config = test_config();
let bytes = convert_image_format(img.clone(), None, &config).unwrap();
assert_eq!(bytes, img.as_bytes());
}

#[test]
fn convert_unsupported_format_errors() {
let img = make_rgb_image(4, 4);
let config = test_config();
let result = convert_image_format(img, Some("bmp"), &config);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Unsupported format")
);
}

#[test]
fn convert_respects_jpeg_quality() {
let img = make_rgb_image(64, 64);

let mut low_q = test_config();
low_q.jpeg_quality = 10;
let bytes_low = convert_image_format(img.clone(), Some("jpeg"), &low_q).unwrap();

let mut high_q = test_config();
high_q.jpeg_quality = 100;
let bytes_high = convert_image_format(img, Some("jpeg"), &high_q).unwrap();

// Higher quality should produce larger output
assert!(bytes_high.len() > bytes_low.len());
}

#[test]
fn convert_respects_webp_quality() {
let img = make_rgb_image(64, 64);

let mut low_q = test_config();
low_q.webp_quality = 1;
let bytes_low = convert_image_format(img.clone(), Some("webp"), &low_q).unwrap();

let mut high_q = test_config();
high_q.webp_quality = 100;
let bytes_high = convert_image_format(img, Some("webp"), &high_q).unwrap();

assert!(bytes_high.len() > bytes_low.len());
}
}
Loading