Tinify

English | δΈζ
A high-performance Rust library for image compression and optimization, built on the TinyPNG API. Provides async support, intelligent retry mechanisms, rate limiting, and cloud storage integration.
β¨ Features
- πΌοΈ Smart Compression: Lossless quality PNG/JPEG/WebP/AVIF image compression
- π Image Resizing: Multiple resize methods (scale/fit/cover/thumb)
- π Format Conversion: Convert between popular image formats
- π Metadata Preservation: Optionally preserve copyright, creation time, location data
- βοΈ Cloud Storage: Direct upload to AWS S3, Google Cloud Storage
- π High-Performance Async: Built on tokio for concurrent processing
- π‘οΈ Type Safety: Full Rust type system and comprehensive error handling
- β‘ Smart Retry: Built-in exponential backoff retry logic and rate limiting
- π¦ Zero Config: Works out of the box with minimal setup
π¦ Installation
Add to your Cargo.toml:
[dependencies]
tinify = "0.1.0"
tokio = { version = "1.0", features = ["full"] }
π Quick Start
Basic Usage
use tinify::Tinify;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_file("input.png").await?;
source.to_file("output.png").await?;
println!("Image compression completed!");
Ok(())
}
Advanced Configuration
use tinify::Tinify;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::builder()
.api_key("your-api-key")
.app_identifier("MyApp/1.0")
.timeout(Duration::from_secs(30))
.max_retry_attempts(3)
.requests_per_minute(100)
.build()?;
let source = client.source_from_file("input.png").await?;
source.to_file("output.png").await?;
Ok(())
}
π Detailed Examples
Image Resizing
use tinify::{Tinify, ResizeOptions, ResizeMethod};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_file("input.png").await?;
let resize_options = ResizeOptions {
method: ResizeMethod::Fit,
width: Some(300),
height: Some(200),
};
let mut result = source.resize(resize_options).await?;
result.to_file("resized.png").await?;
if let Some(width) = result.image_width() {
println!("Resized width: {} pixels", width);
}
Ok(())
}
Format Conversion
use tinify::{Tinify, ConvertOptions, ImageFormat};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_file("input.png").await?;
let convert_options = ConvertOptions {
format: ImageFormat::WebP,
background: Some("#FFFFFF".to_string()),
};
let mut result = source.convert(convert_options).await?;
result.to_file("output.webp").await?;
Ok(())
}
Metadata Preservation
use tinify::{Tinify, PreserveOptions, PreserveMetadata};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_file("input.jpg").await?;
let preserve_options = PreserveOptions {
preserve: vec![
PreserveMetadata::Copyright,
PreserveMetadata::Creation,
],
};
let mut result = source.preserve(preserve_options).await?;
result.to_file("preserved.jpg").await?;
Ok(())
}
AWS S3 Cloud Storage
use tinify::{Tinify, StoreOptions, S3Options};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_file("input.png").await?;
let s3_options = S3Options {
service: "s3".to_string(),
aws_access_key_id: "your-access-key".to_string(),
aws_secret_access_key: "your-secret-key".to_string(),
region: "us-east-1".to_string(),
path: "my-bucket/images/compressed.png".to_string(),
headers: None,
acl: Some("public-read".to_string()),
};
let result = source.store(StoreOptions::S3(s3_options)).await?;
if let Some(count) = result.compression_count() {
println!("API usage count: {}", count);
}
Ok(())
}
Google Cloud Storage
use tinify::{Tinify, StoreOptions, GCSOptions};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_file("input.png").await?;
let gcs_options = GCSOptions {
service: "gcs".to_string(),
gcp_access_token: "your-access-token".to_string(),
path: "my-bucket/images/compressed.png".to_string(),
headers: Some(json!({
"Cache-Control": "public, max-age=31536000",
"X-Goog-Meta-Source": "tinify-rs"
})),
};
let result = source.store(StoreOptions::GCS(gcs_options)).await?;
Ok(())
}
URL-based Processing
use tinify::Tinify;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_url("https://example.com/image.jpg").await?;
source.to_file("compressed.jpg").await?;
Ok(())
}
Buffer-based Processing
use tinify::Tinify;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let image_data = std::fs::read("input.png")?;
let source = client.source_from_buffer(image_data).await?;
let compressed_data = source.to_buffer().await?;
std::fs::write("output.png", compressed_data)?;
Ok(())
}
π§ API Reference
Resize Methods
| Method |
Description |
Use Case |
Scale |
Proportional scaling |
Precise width or height control |
Fit |
Fit within dimensions (preserve aspect ratio) |
Create largest image within bounds |
Cover |
Cover dimensions (may crop) |
Fill exact dimensions, preserve ratio |
Thumb |
Smart thumbnail |
Auto-detect important regions |
Supported Image Formats
| Format |
Input Support |
Output Support |
Description |
| PNG |
β
|
β
|
Lossless compression, transparency support |
| JPEG |
β
|
β
|
Lossy compression, ideal for photos |
| WebP |
β
|
β
|
Modern format, smaller file sizes |
| AVIF |
β |
β
|
Next-gen format, best compression |
Cloud Storage Support
| Service |
Support Status |
Notes |
| AWS S3 |
β
|
Full support with custom headers and ACL |
| Google Cloud Storage |
β
|
Full support with metadata |
| S3-Compatible Services |
β
|
MinIO, DigitalOcean Spaces, Backblaze B2, etc. |
β οΈ Error Handling
The library provides comprehensive error types:
use tinify::{Tinify, TinifyError};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("api-key".to_string())?;
match client.source_from_file("input.png").await {
Ok(source) => {
println!("Processing successful");
}
Err(TinifyError::FileNotFound { path }) => {
println!("File not found: {}", path);
}
Err(TinifyError::UnsupportedFormat { format }) => {
println!("Unsupported format: {}", format);
}
Err(TinifyError::FileTooLarge { size, max_size }) => {
println!("File too large: {} bytes (max: {} bytes)", size, max_size);
}
Err(TinifyError::QuotaExceeded) => {
println!("API quota exhausted");
}
Err(TinifyError::AccountError { status, message }) => {
println!("Account error [{}]: {}", status, message);
}
Err(e) => {
println!("Other error: {}", e);
}
}
Ok(())
}
π Performance Optimization
Async Concurrent Processing
use tinify::Tinify;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let mut join_set = JoinSet::new();
let files = vec!["image1.png", "image2.jpg", "image3.webp"];
for (i, file) in files.iter().enumerate() {
let client = client.clone();
let file = file.to_string();
join_set.spawn(async move {
let source = client.source_from_file(&file).await?;
let output = format!("compressed_{}.png", i);
source.to_file(&output).await?;
Ok::<String, tinify::TinifyError>(output)
});
}
while let Some(result) = join_set.join_next().await {
match result {
Ok(Ok(filename)) => println!("β
Compressed: {}", filename),
Ok(Err(e)) => println!("β Compression failed: {}", e),
Err(e) => println!("β Task error: {}", e),
}
}
Ok(())
}
Batch Processing
use tinify::{Tinify, ResizeOptions, ResizeMethod};
async fn batch_process_images(
client: &Tinify,
input_files: Vec<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
for file in input_files {
let source = client.source_from_file(file).await?;
let resize_options = ResizeOptions {
method: ResizeMethod::Fit,
width: Some(800),
height: Some(600),
};
let mut result = source.resize(resize_options).await?;
let output = format!("processed_{}", file);
result.to_file(&output).await?;
println!("β
Processed: {} -> {}", file, output);
}
Ok(())
}
π Cloud Storage Integration
AWS S3 Examples
use tinify::{Tinify, StoreOptions, S3Options};
use serde_json::json;
let s3_options = S3Options {
service: "s3".to_string(),
aws_access_key_id: "your-access-key".to_string(),
aws_secret_access_key: "your-secret-key".to_string(),
region: "us-east-1".to_string(),
path: "my-bucket/images/compressed.png".to_string(),
headers: None,
acl: Some("public-read".to_string()),
};
let s3_options_with_headers = S3Options {
service: "s3".to_string(),
aws_access_key_id: "your-access-key".to_string(),
aws_secret_access_key: "your-secret-key".to_string(),
region: "us-east-1".to_string(),
path: "my-bucket/images/compressed.png".to_string(),
headers: Some(json!({
"Cache-Control": "public, max-age=31536000",
"Content-Disposition": "inline; filename=\"optimized.png\""
})),
acl: Some("public-read".to_string()),
};
let source = client.source_from_file("input.png").await?;
let result = source.store(StoreOptions::S3(s3_options)).await?;
S3-Compatible Storage
Supports various S3-compatible storage services:
- MinIO: Self-hosted object storage
- DigitalOcean Spaces: Simple cloud storage
- Backblaze B2: Affordable cloud storage
- Wasabi: High-performance cloud storage
let minio_options = S3Options {
service: "s3".to_string(),
aws_access_key_id: "minioadmin".to_string(),
aws_secret_access_key: "minioadmin".to_string(),
region: "us-east-1".to_string(),
path: "test-bucket/compressed.png".to_string(),
headers: None,
acl: None,
};
π― Complete Feature Showcase
Check out examples in the examples/ directory:
01_compressing_images.rs - Basic image compression
02_resizing_images.rs - Image resizing operations
03_converting_images.rs - Format conversion
04_preserving_metadata.rs - Metadata preservation
05_saving_to_s3.rs - AWS S3 storage
06_saving_to_gcs.rs - Google Cloud Storage
07_error_handling.rs - Error handling patterns
08_compression_count.rs - Compression counter tracking
09_s3_compatible_storage.rs - S3-compatible services
10_comprehensive_demo.rs - Complete feature demonstration
Run examples:
cargo run --example 01_compressing_images
export TINIFY_API_KEY="your-api-key"
export AWS_ACCESS_KEY_ID="your-aws-key"
export AWS_SECRET_ACCESS_KEY="your-aws-secret"
cargo run --example 05_saving_to_s3
cargo run --example 07_error_handling
π API Quota Management
use tinify::Tinify;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Tinify::new("your-api-key".to_string())?;
let source = client.source_from_file("input.png").await?;
let result = source.to_buffer().await?;
if let Some(count) = result.compression_count() {
println!("Current API usage: {}", count);
if count > 450 {
println!("β οΈ Approaching free quota limit (500/month)");
}
}
Ok(())
}
βοΈ Environment Setup
Environment Variables
export TINIFY_API_KEY="your-tinify-api-key"
export AWS_ACCESS_KEY_ID="your-aws-access-key"
export AWS_SECRET_ACCESS_KEY="your-aws-secret-key"
export GCP_ACCESS_TOKEN="your-gcp-access-token"
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
Getting API Key
- Visit TinyPNG Developer Page
- Register account and verify email
- Get free API key (500 compressions/month)
- Upgrade to paid plan for higher quotas
π§ͺ Testing
cargo test
cargo test --doc
cargo run --example 01_compressing_images
cargo run --example test_real_image
./test_cloud_storage.sh
π System Requirements
- Rust: 1.70.0 or higher
- Operating System: Windows, macOS, Linux
- Network: Stable internet connection for TinyPNG API access
- Memory: Minimum 100MB available memory for image processing
π¨ Limitations and Considerations
API Limitations
- Free Quota: 500 compressions/month
- File Size: Maximum 5MB per file
- Supported Formats: PNG, JPEG, WebP (input), PNG, JPEG, WebP, AVIF (output)
- Concurrency: Recommended max 10 concurrent requests
Best Practices
- API Key Security: Never hardcode API keys, use environment variables
- Error Handling: Always properly handle network and API errors
- Quota Monitoring: Regularly check API usage to avoid limits
- File Validation: Validate file format and size before upload
- Concurrency Control: Manage concurrent request count appropriately
match client.source_from_file("input.png").await {
Ok(source) => {
}
Err(TinifyError::QuotaExceeded) => {
eprintln!("API quota exhausted, wait for next month or upgrade plan");
}
Err(TinifyError::FileTooLarge { size, max_size }) => {
eprintln!("File too large: {} bytes (max: {})", size, max_size);
}
Err(e) => {
eprintln!("Compression failed: {}", e);
}
}
π€ Contributing
We welcome contributions of all kinds!
Development Setup
git clone https://github.com/raynoryim/tinify.git
cd tinify-rs
cargo test
cargo clippy
cargo fmt
cargo check --examples
Submitting PRs
- Fork the repository
- Create feature branch:
git checkout -b feature/amazing-feature
- Commit changes:
git commit -m 'feat: add amazing feature'
- Push branch:
git push origin feature/amazing-feature
- Create Pull Request
Reporting Issues
Please report bugs or request features in GitHub Issues.
π License
This project is licensed under the MIT License. See the LICENSE file for details.
π Related Links
π Acknowledgments
- TinyPNG for providing excellent image compression API
- Rust community for amazing libraries and tools
- All contributors and users for their support
β If this project helps you, please give us a star!