tekkenking / tinypeexi
A fluent Laravel package for integrating with the Lossless Media Service (aipeexi).
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.8
- illuminate/http: ^10.0|^11.0
- illuminate/support: ^10.0|^11.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
README
Upload, transform, and deliver images from your Laravel app in minutes.
TinyPeexi is a Laravel package that gives you a clean, fluent PHP API to interact with a self-hosted aipeexi / Lossless Media Service instance. Upload images, generate optimized variants, apply effects, create e-commerce product shots, and get CDN-ready delivery URLs โ all without leaving your Laravel codebase.
๐ฆ Installation
1. Require via Composer
composer require tekkenking/tinypeexi
2. Publish the Config File
php artisan vendor:publish --provider="Tekkenking\TinyPeexi\TinyPeexiServiceProvider"
This creates config/tinypeexi.php in your Laravel project.
3. Add Your Credentials to .env
TINYPEEXI_API_URL=http://localhost:8080 TINYPEEXI_TENANT_SLUG=dev TINYPEEXI_API_KEY=sk_live_your_api_key_here
That's it. You're ready to use TinyPeexi.
๐ Quick Start
use Tekkenking\TinyPeexi\Facades\TinyPeexi; // Upload an image $asset = TinyPeexi::upload($request->file('photo')); // Generate a 400px wide WebP variant $variant = TinyPeexi::variant($asset->sha) ->resize(400) ->format('webp') ->quality(80) ->generate(); // Get the public URL echo $variant->url(); // => http://localhost:8080/a/dev/abc123.../w400.webp
๐ Full API Reference
Uploading Assets
Upload from a Form Request
$asset = TinyPeexi::upload($request->file('image')); echo $asset->sha; // "a1b2c3d4e5f6..."
Upload from a File Path
$asset = TinyPeexi::upload('/path/to/local/image.png'); echo $asset->sha;
The upload() method returns an AssetDto with the asset's sha (SHA-256 hash), which you'll use as the identifier for all future operations on this image.
Uploading Multiple Files at Once
TinyPeexi supports uploading multiple files in a single HTTP request (the backend defaults to a max of 10 files per request).
$files = $request->file('gallery_images'); // Array of UploadedFile $assets = TinyPeexi::uploadMany($files); foreach ($assets as $asset) { echo $asset->sha . "\n"; }
The uploadMany() method returns an array of AssetDto objects.
๐จ Blade & View Helpers
TinyPeexi includes a global helper function tinypeexi() to natively integrate into your views. Furthermore, the Variant builder automatically converts directly into a URL string when echoed, keeping your Blade templates incredibly clean.
1. The Fluent Way (Cleanest)
You don't need to call ->url(). Just echo the builder directly!
<!-- Automatically outputs the URL string --> <img src="{{ tinypeexi($product->image_sha)->resize(400)->format('webp') }}"> <img src="{{ tinypeexi($product->image_sha)->ecommerceVariant() }}">
2. The Array Shorthand (Shortest)
Pass parameters dynamically if you prefer:
<img src="{{ tinypeexi($product->image_sha, ['w' => 800, 'format' => 'webp']) }}">
3. Using Config Presets
Instantly apply predefined transformations from your tinypeexi.php config:
<img src="{{ tinypeexi($product->image_sha, 'thumbnail') }}">
Bulk Asset Migration
TinyPeexi includes a powerful Artisan command to migrate your existing local files into the Lossless Media Service. It is memory-safe, supports batching, queue-based variant generation, and uses a CSV file (storage/app/tinypeexi_migration_results.csv) for idempotency, meaning if the process dies, you can run the exact same command again and it will pick up right where it left off!
php artisan tinypeexi:migrate \
--path="/var/www/uploads/2023|/var/www/uploads/2024" \
--ext="jpg|png|webp" \
--starts-with="product_|user_" \
--variants="w800,h800|w200" \
--batch=50
--path: Pipe-separated list of directories to scan.--ext: Pipe-separated list of extensions to include.--starts-with: Optional pipe-separated list of filename prefixes to include.--variants: Optional pipe-separated list of variant dimensions to generate in the background (dispatchesGenerateTinyPeexiVariantJob).--batch: Number of originals to upload per HTTP request (default: 50).
Deleting Assets
TinyPeexi::delete($sha);
Returns true on success, throws TinyPeexiException on failure.
Generating Variants
The heart of TinyPeexi is the fluent Variant Builder. You start with TinyPeexi::variant($sha) and chain methods to describe the transformation you want.
Basic Resize
$variant = TinyPeexi::variant($sha) ->resize(800) ->format('jpeg') ->quality(85) ->generate(); echo $variant->url();
Resize with Exact Dimensions
$variant = TinyPeexi::variant($sha) ->resize(800, 600) // width, height ->fit('cover') // crop to fill ->generate();
Available Fit Modes
| Mode | Behavior |
|---|---|
cover |
Crop to fill exact dimensions |
contain |
Fit within dimensions, letterbox if needed |
fill |
Stretch to exact dimensions (may distort) |
inside |
Fit inside, never upscale (default) |
outside |
Fit outside, may upscale to cover |
Retina / HiDPI Support
$variant = TinyPeexi::variant($sha) ->resize(400) ->dpr(2.0) // Outputs 800px wide for retina displays ->generate();
Cropping
Smart Crop
$variant = TinyPeexi::variant($sha) ->resize(400, 400) ->fit('cover') ->crop('face') // AI-powered face detection crop ->generate();
Available Crop Positions
| Position | Description |
|---|---|
center |
Center of image (default) |
top |
Top edge |
bottom |
Bottom edge |
left |
Left edge |
right |
Right edge |
topleft |
Top-left corner |
topright |
Top-right corner |
bottomleft |
Bottom-left corner |
bottomright |
Bottom-right corner |
face |
AI face detection (Pro tier) |
entropy |
Smart crop using entropy analysis |
attention |
Smart crop using saliency/attention |
Manual Crop Rectangle
$variant = TinyPeexi::variant($sha) ->cropRect(100, 50, 600, 400) // x, y, width, height ->generate();
Auto-Trim Whitespace
$variant = TinyPeexi::variant($sha) ->trim(25) // threshold 0-255 ->generate();
Orientation
// Rotate 90 degrees clockwise $variant = TinyPeexi::variant($sha) ->rotate(90) ->generate(); // Flip horizontally (mirror) $variant = TinyPeexi::variant($sha) ->flip('h') ->generate(); // Auto-fix EXIF orientation $variant = TinyPeexi::variant($sha) ->autoOrient() ->generate();
| Rotate Values | Flip Values |
|---|---|
0, 90, 180, 270, -90 |
h (horizontal), v (vertical), both |
Effects
$variant = TinyPeexi::variant($sha) ->resize(800) ->blur(5.0) // Gaussian blur (0.3 - 1000.0) ->sharpen(1.5) // Sharpen (0.5 - 10.0) ->brightness(1.2) // Brightness (0.1 - 10.0) ->contrast(1.1) // Contrast (0.1 - 10.0) ->saturation(1.5) // Saturation (0.0 - 2.0) ->generate();
Grayscale
$variant = TinyPeexi::variant($sha) ->grayscale() ->format('jpeg') ->generate();
E-Commerce Product Images
This is where TinyPeexi really shines. Creating uniform, professional product images is a single method call.
Using the Fluent Builder
$variant = TinyPeexi::variant($sha) ->format('jpeg') ->ecommerce() // Flag as e-commerce operation ->canvas(1024) // 1024ร1024 square canvas ->resize(800) // Product fits within 800px ->pad(40) // 40px breathing room ->background('white') // Clean white background ->quality(90) ->generate(); echo $variant->url(); // => http://localhost:8080/a/dev/abc123.../w800.jpg
Using the Shorthand Helper
If you use the same e-commerce settings repeatedly, use the shorthand:
$variant = TinyPeexi::variant($sha)->ecommerceVariant(); // Uses defaults from config: canvas=1024, pad=40, bg=white, jpeg, q=85
Custom E-Commerce Settings
$variant = TinyPeexi::variant($sha)->ecommerceVariant( canvasSize: 800, padding: 20, background: 'transparent', format: 'png', quality: 100, );
With AI Background Removal
$variant = TinyPeexi::variant($sha) ->removeBg() // Remove background via AI ->canvas(1024) ->pad(40) ->background('white') ->format('jpeg') ->quality(90) ->generate();
Watermarks
Image Watermark
// First, upload your watermark image (do this once) $watermark = TinyPeexi::upload('/path/to/watermark.png'); // Then apply it to any variant $variant = TinyPeexi::variant($sha) ->resize(1200) ->watermark( assetSha: $watermark->sha, position: 'bottomright', opacity: 0.5, scale: 0.15, ) ->generate();
Available Watermark Positions
| Position | Description |
|---|---|
center |
Center of image |
topleft |
Top-left corner |
topright |
Top-right corner |
bottomleft |
Bottom-left corner |
bottomright |
Bottom-right corner (default) |
tile |
Tile across entire image |
Using Presets
Define reusable transformation presets in config/tinypeexi.php:
// In config/tinypeexi.php 'defaults' => [ 'thumbnail' => [ 'width' => 200, 'height' => 200, 'fit' => 'cover', 'crop' => 'center', 'format' => 'webp', 'quality' => 80, ], ]
Then use them in your code:
$variant = TinyPeexi::variant($sha) ->preset('thumbnail') ->generate();
You can define as many presets as you need (e.g., hero_banner, avatar, og_image, etc.).
Batch Variant Generation
Generate multiple sizes at once:
TinyPeexi::variant($sha)->batch( sizes: [400, 800, 1200], format: 'webp', ); // Queues: w400.webp, w800.webp, w1200.webp
Output Formats
| Format | Extension | Notes |
|---|---|---|
jpeg |
.jpg |
Best for photos |
png |
.png |
Lossless, supports transparency |
webp |
.webp |
Modern, smaller files (default) |
avif |
.avif |
Newest, smallest files |
gif |
.gif |
Animated images |
tiff |
.tiff |
Print-quality |
ico |
.ico |
Favicons |
svg |
.svg |
Vector graphics |
โ๏ธ Configuration Reference
After publishing, edit config/tinypeexi.php:
return [ // === REQUIRED === 'api_url' => env('TINYPEEXI_API_URL', 'http://localhost:8080'), 'delivery_url' => env('TINYPEEXI_DELIVERY_URL', env('TINYPEEXI_API_URL', 'http://localhost:8080')), 'tenant_slug' => env('TINYPEEXI_TENANT_SLUG', 'dev'), 'api_key' => env('TINYPEEXI_API_KEY', ''), // === ADVANCED (optional) === 'advanced' => [ 'timeout' => env('TINYPEEXI_TIMEOUT', 10), // seconds 'retries' => env('TINYPEEXI_RETRIES', 3), 'retry_delay' => env('TINYPEEXI_RETRY_DELAY', 500), // ms 'upload' => [ 'max_filesize_bytes' => env('TINYPEEXI_MAX_UPLOAD_SIZE', 26214400), // 25MB 'allowed_mimes' => ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'], 'strip_metadata' => env('TINYPEEXI_STRIP_METADATA', true), 'auto_optimize' => env('TINYPEEXI_AUTO_OPTIMIZE', false), ], ], // === PRESETS (optional) === 'defaults' => [ 'ecommerce' => [ 'canvas' => 1024, 'pad' => 40, 'background' => 'white', 'format' => 'jpeg', 'quality' => 85, ], 'thumbnail' => [ 'width' => 200, 'height' => 200, 'fit' => 'cover', 'crop' => 'center', 'format' => 'webp', 'quality' => 80, ], ], ];
Environment Variables
| Variable | Default | Description |
|---|---|---|
TINYPEEXI_API_URL |
http://localhost:8080 |
Base URL of aipeexi instance |
TINYPEEXI_DELIVERY_URL |
Same as API URL | CDN/delivery URL |
TINYPEEXI_TENANT_SLUG |
dev |
Your tenant slug |
TINYPEEXI_API_KEY |
(empty) | Your API key |
TINYPEEXI_TIMEOUT |
10 |
HTTP timeout (seconds) |
TINYPEEXI_RETRIES |
3 |
Retry count for failed requests |
TINYPEEXI_RETRY_DELAY |
500 |
Delay between retries (ms) |
TINYPEEXI_MAX_UPLOAD_SIZE |
26214400 |
Max upload size (bytes) |
TINYPEEXI_STRIP_METADATA |
true |
Strip EXIF on upload |
TINYPEEXI_AUTO_OPTIMIZE |
false |
Auto-optimize on upload |
๐งโ๐ป Real-World Examples
Profile Avatar Upload
// In a Controller public function updateAvatar(Request $request) { $request->validate(['avatar' => 'required|image|max:5120']); $asset = TinyPeexi::upload($request->file('avatar')); // Generate a small square avatar $variant = TinyPeexi::variant($asset->sha) ->resize(200, 200) ->fit('cover') ->crop('face') ->format('webp') ->quality(80) ->generate(); auth()->user()->update(['avatar_url' => $variant->url()]); return back()->with('success', 'Avatar updated!'); }
Product Image Upload (E-Commerce)
public function storeProduct(Request $request) { $request->validate(['photo' => 'required|image|max:25600']); $asset = TinyPeexi::upload($request->file('photo')); // Professional product shot with white background $main = TinyPeexi::variant($asset->sha)->ecommerceVariant(); // Also create a thumbnail $thumb = TinyPeexi::variant($asset->sha) ->preset('thumbnail') ->generate(); Product::create([ 'name' => $request->name, 'image_sha' => $asset->sha, 'image_url' => $main->url(), 'thumbnail_url' => $thumb->url(), ]); return redirect()->route('products.index'); }
Responsive Image Set
$asset = TinyPeexi::upload($request->file('hero')); // Generate multiple sizes for responsive <picture> element TinyPeexi::variant($asset->sha)->batch( sizes: [400, 800, 1200, 1600], format: 'webp', ); // In your Blade template: // <picture> // <source srcset="/a/dev/{sha}/w400.webp 400w, // /a/dev/{sha}/w800.webp 800w, // /a/dev/{sha}/w1200.webp 1200w, // /a/dev/{sha}/w1600.webp 1600w" // type="image/webp"> // </picture>
Blog Post with Watermarked Images
$watermarkAsset = TinyPeexi::upload('/path/to/blog-watermark.png'); $asset = TinyPeexi::upload($request->file('blog_image')); $variant = TinyPeexi::variant($asset->sha) ->resize(1200) ->format('jpeg') ->quality(85) ->watermark( assetSha: $watermarkAsset->sha, position: 'bottomright', opacity: 0.3, scale: 0.1, ) ->generate();
๐ง Error Handling
All errors throw TinyPeexiException:
use Tekkenking\TinyPeexi\Exceptions\TinyPeexiException; try { $asset = TinyPeexi::upload($request->file('image')); } catch (TinyPeexiException $e) { Log::error('TinyPeexi upload failed', [ 'message' => $e->getMessage(), 'code' => $e->getCode(), ]); return back()->withErrors(['image' => 'Image upload failed. Please try again.']); }
๐งช Testing
When writing tests, you can mock the TinyPeexi facade:
use Tekkenking\TinyPeexi\Facades\TinyPeexi; use Tekkenking\TinyPeexi\DTOs\AssetDto; TinyPeexi::shouldReceive('upload') ->once() ->andReturn(new AssetDto('fake_sha_256_hash'));
๐ Method Cheat Sheet
| Method | Description |
|---|---|
TinyPeexi::upload($file) |
Upload an image, returns AssetDto |
TinyPeexi::uploadMany(array $files) |
Upload multiple images, returns AssetDto[] |
TinyPeexi::delete($sha) |
Delete an asset |
TinyPeexi::variant($sha) |
Start building a variant |
->resize($w, $h?) |
Set target dimensions |
->fit($mode) |
Set fit mode |
->dpr($ratio) |
Set device pixel ratio |
->format($fmt) |
Set output format |
->quality($q) |
Set compression quality |
->progressive() |
Enable progressive JPEG |
->stripMetadata() |
Strip EXIF metadata |
->crop($position) |
Set crop anchor |
->cropRect($x,$y,$w,$h) |
Manual crop rectangle |
->trim($threshold?) |
Auto-trim whitespace |
->rotate($degrees) |
Rotate image |
->flip($direction) |
Flip image |
->autoOrient() |
Auto-fix EXIF orientation |
->blur($sigma) |
Apply Gaussian blur |
->sharpen($amount) |
Apply sharpening |
->brightness($value) |
Adjust brightness |
->contrast($value) |
Adjust contrast |
->saturation($value) |
Adjust saturation |
->grayscale() |
Convert to grayscale |
->canvas($size) |
Set square canvas size |
->pad($px) |
Set canvas padding |
->background($color) |
Set background color |
->removeBg() |
AI background removal |
->ecommerce() |
Flag as e-commerce operation |
->ecommerceVariant(...) |
Shorthand for e-commerce variant |
->watermark(...) |
Add image watermark |
->preset($name) |
Apply a config preset |
->batch($sizes, $fmt?) |
Generate multiple sizes |
->generate() |
Execute and return VariantDto |
->toArray() |
Debug: get params as array |
๐ License
MIT License. See LICENSE for details.
๐ค Contributing
Pull requests are welcome! Please open an issue first to discuss what you'd like to change.
Built with โค๏ธ for Laravel developers by tekkenking.