Outputting and Saving Images
After bringing images into memory using included-image-upload() (for new uploads) or included-image-load() (for existing images) you need to do something with them. Trongate gives you two options:
- Save them to disk for permanent storage
- Output them directly to the browser for immediate display
Understanding when to save versus when to output - and how to control compression, format, and permissions - separates amateur image handling from professional implementations.
Save vs Output:
- included-image-save() - Writes image to disk as a file. Use for permanent storage, user uploads, generated thumbnails, or any image that needs to persist.
- included-image-output() - Streams image directly to browser without disk storage. Use for dynamic images, watermarks, on-the-fly transformations, or temporary displays.
Saving Images to Disk
The included-image-save() method writes the currently loaded image to a file with optional compression and permission settings.
Basic Saving
After using included-image-load() to bring an image into memory and performing any manipulations, use included-image-save() to write it to disk:
// Load, manipulate, and save
$this->image->load('modules/gallery/photos/original.jpg');
$this->image->resize_to_width(800);
$this->image->save('modules/gallery/photos/resized.jpg');
Method Signature
public function save(
?string $filename = null, // Path where image will be saved
int $compression = 100, // Quality: 0-100 (JPEG/WEBP only)
?int $permissions = null // File permissions (e.g., 0644)
): void
Overwriting vs Creating New Files
// Overwrite the original (destructive)
$this->image->load('modules/products/images/photo.jpg');
$this->image->resize_to_width(1200);
$this->image->save('modules/products/images/photo.jpg'); // Same path - overwrites
// Create a new file (non-destructive)
$this->image->load('modules/products/images/photo.jpg');
$this->image->resize_to_width(800);
$this->image->save('modules/products/images/photo_800w.jpg'); // New path - preserves original
Destructive Operations: When saving to the same path you loaded from, you overwrite the original file permanently. For production systems, consider keeping originals and saving manipulated versions with different names.
Controlling Image Quality
The compression parameter (0-100) controls the trade-off between file size and visual quality for JPEG and WEBP formats.
Quality Guidelines
// Maximum quality (default)
$this->image->save('modules/blog/images/hero.jpg', 100);
// Result: Pristine quality, ~500KB
// Recommended for web
$this->image->save('modules/blog/images/hero.jpg', 85);
// Result: Excellent quality, ~200KB (60% smaller!)
// Acceptable for thumbnails
$this->image->save('modules/blog/images/thumb.jpg', 75);
// Result: Good quality, ~100KB
// Low quality (avoid for main content)
$this->image->save('modules/blog/images/preview.jpg', 60);
// Result: Visible artifacts, ~50KB
Quality by Use Case
| Image Type |
Recommended Quality |
Reasoning |
| Hero images, banners |
90-95 |
High visibility, worth the file size |
| Product photos |
85-90 |
Balance of quality and performance |
| Content images |
80-85 |
Good quality, faster page loads |
| Thumbnails |
75-80 |
Small display size hides artifacts |
| Background images |
70-75 |
Often blurred or overlaid |
Format-Specific Compression Behavior:
- JPEG and WEBP respond to quality settings:
The quality parameter (0-100) determines the trade-off between file size and visual fidelity.
• Lower values = smaller files with more compression artifacts
• Higher values = better quality with larger file sizes
• 85-90 = recommended sweet spot for web images
- PNG and GIF ignore quality settings:
These formats use lossless compression that preserves all image data.
• The quality parameter has no effect
• PNG preserves exact pixel values and alpha transparency
• GIF preserves binary transparency with maximum 256 colors
Practical tip: Use JPEG/WEBP for photographs where file size matters, and PNG/GIF for logos, graphics, and images requiring transparency.
Setting File Permissions
Control who can read, write, or execute saved images using the permissions parameter:
// Readable by everyone, writable by owner (recommended)
$this->image->save('modules/gallery/images/public.jpg', 85, 0644);
// Readable and writable by owner only (private files)
$this->image->save('modules/documents/private/scan.jpg', 90, 0600);
// Default permissions (system default, typically 0644)
$this->image->save('modules/products/images/widget.jpg', 85);
Permission Recommendations:
- 0644 - Public images (web galleries, user avatars, product photos)
- 0640 - Semi-private (group-readable only)
- 0600 - Private images (documents, scanned files, sensitive photos)
Working with Different Formats
The Image module automatically detects and preserves the original format, but you can change formats by changing the file extension:
Format Conversion
// Load PNG, save as JPEG
$this->image->load('modules/branding/logos/logo.png');
$this->image->resize_to_width(400);
$this->image->save('modules/branding/logos/logo.jpg', 90);
// Note: Transparent areas become white
// Load JPEG, save as PNG
$this->image->load('modules/gallery/photos/photo.jpg');
$this->image->resize_to_width(800);
$this->image->save('modules/gallery/photos/photo.png');
// Note: Lossless but larger file size
// Save as WEBP for modern browsers
$this->image->load('modules/products/images/widget.jpg');
$this->image->save('modules/products/images/widget.webp', 85);
// Note: Better compression than JPEG
Format Selection Guide
| Format |
Best For |
Compression |
Transparency |
| JPEG |
Photos, complex images |
Lossy, adjustable |
No |
| PNG |
Logos, graphics, screenshots |
Lossless |
Yes (alpha) |
| GIF |
Simple graphics, animations |
Lossless, limited colors |
Yes (binary) |
| WEBP |
Modern web, photos + graphics |
Lossy or lossless |
Yes (alpha) |
Outputting Images to Browser
The included-image-output() method streams an image directly to the browser without saving it to disk. Perfect for dynamic image generation or temporary displays.
Direct Browser Output
public function serve_thumbnail(): void {
$product_id = segment(3, 'int');
// Get product record
$product = $this->db->get_where($product_id, 'products');
if ($product === false) {
redirect('products/not_found');
}
// Check if original image exists
if (!$this->file->exists($product->original_image_path)) {
redirect('products/image_missing');
}
// Load and manipulate
$this->image->load($product->original_image_path);
$this->image->resize_and_crop(300, 300);
// Set proper headers
header('Content-Type: ' . $this->image->get_header());
// Stream directly to browser
$this->image->output();
$this->image->destroy();
}
// In your view:
// <img src="<?= BASE_URL ?>products/serve_thumbnail/123">
Always Set Content-Type: When using output() to stream images to browsers, you must set the Content-Type header using get_header(). Without this header, browsers may fail to display the image or attempt to download it as a file.
Capturing Image Data as String
// Get image binary data
$this->image->load('modules/gallery/photos/photo.jpg');
$this->image->resize_to_width(400);
$image_data = $this->image->output(true); // Pass true to capture
// Embed in email as attachment
$attachment = base64_encode($image_data);
// Store in database (not recommended for large images)
$data['thumbnail_binary'] = $image_data;
$this->model->insert($data, 'cached_thumbnails');
// Generate data URI for inline HTML
$base64 = base64_encode($image_data);
$mime = $this->image->get_header();
$data_uri = "data:{$mime};base64,{$base64}";
// Use in HTML: <img src="<?= $data_uri ?>">
Getting the Correct MIME Type
When outputting images to browsers, you must set the correct Content-Type header. Use included-image-get_header() to get the appropriate MIME type:
$this->image->load('modules/products/images/widget.jpg');
// Get MIME type for the loaded image
$mime_type = $this->image->get_header();
// Returns: 'image/jpeg'
// Use in header
header('Content-Type: ' . $mime_type);
$this->image->output();
Supported MIME Types
image/jpeg - JPEG images
image/png - PNG images
image/gif - GIF images
image/webp - WEBP images
Real-World Example: Dynamic Avatar System
Generate multiple avatar sizes from a single upload without storing every size:
<?php
class Users extends Trongate {
/**
* Serve user avatar at requested size
*/
public function avatar(): void {
$user_id = segment(3, 'int');
$size = segment(4, 'int') ?? 150; // Default 150px
// Validate size (prevent abuse)
$allowed_sizes = [50, 100, 150, 300];
if (!in_array($size, $allowed_sizes)) {
$size = 150;
}
// Get user's avatar path
$user = $this->db->get_where($user_id, 'users');
if ($user === false || empty($user->avatar_path)) {
// Serve default avatar
$avatar_path = 'public/assets/default_avatar.png';
} else {
$avatar_path = $user->avatar_path;
}
// Verify file exists
if (!$this->file->exists($avatar_path)) {
$avatar_path = 'public/assets/default_avatar.png';
}
// Load and resize on-the-fly
$this->image->load($avatar_path);
$this->image->resize_and_crop($size, $size);
// Cache for 1 hour
header('Content-Type: ' . $this->image->get_header());
header('Cache-Control: public, max-age=3600');
$this->image->output();
$this->image->destroy();
}
}
?>
<!-- Usage in views: -->
<!-- 50x50 icon -->
<img src="<?= BASE_URL ?>users/avatar/42/50" alt="User avatar">
<!-- 150x150 profile -->
<img src="<?= BASE_URL ?>users/avatar/42/150" alt="User avatar">
<!-- 300x300 full size -->
<img src="<?= BASE_URL ?>users/avatar/42/300" alt="User avatar">
Dynamic vs Pre-Generated: This avatar system generates sizes on-demand rather than pre-generating all sizes during upload. The browser cache (1 hour) ensures subsequent requests are fast without repeatedly processing the image.
Save vs Output: Decision Guide
Use save() when:
- Images need to persist between requests
- You're processing user uploads
- Generating thumbnails or variants for later use
- Creating static assets for deployment
- Performance matters (serve static files via web server)
- Building image galleries or catalogs
Use output() when:
- Generating images dynamically per request
- Adding watermarks or overlays on-the-fly
- Creating temporary previews or proofs
- Serving user-specific customized images
- Avoiding disk storage for security or space reasons
- Testing or debugging image operations
Performance Considerations
Caching Headers for Dynamic Images
public function dynamic_thumbnail(): void {
$product_id = segment(3, 'int');
// Get product
$product = $this->db->get_where($product_id, 'products');
if ($product === false) {
redirect('products/not_found');
}
// Calculate etag based on file and size
$etag = md5($product->image_path . '_300x300_' . filemtime($product->image_path));
// Check if client has cached version
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) &&
$_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
http_response_code(304); // Not Modified
die();
}
// Verify file exists
if (!$this->file->exists($product->image_path)) {
redirect('products/image_missing');
}
// Generate and serve
$this->image->load($product->image_path);
$this->image->resize_and_crop(300, 300);
header('Content-Type: ' . $this->image->get_header());
header('Cache-Control: public, max-age=86400'); // 24 hours
header('ETag: ' . $etag);
$this->image->output();
$this->image->destroy();
}
ETags and Browser Caching: Using ETags (entity tags) with cache headers prevents unnecessary image regeneration. When the browser sends the ETag back, return a 304 Not Modified response instead of regenerating the image. This dramatically reduces server load for frequently accessed dynamic images.
When to Pre-Generate vs On-Demand
| Scenario |
Strategy |
Reasoning |
| User uploads (avatars, photos) |
Pre-generate common sizes |
Predictable sizes, frequent access |
| Product images |
Pre-generate |
High traffic, consistent sizing |
| Admin previews |
On-demand |
Infrequent access, varied sizes |
| Watermarked images |
On-demand |
Prevents unauthorized distribution |
| User-specific customizations |
On-demand |
Unique per user, storage waste |
| Blog post images |
Pre-generate |
Consistent layouts, public access |
Complete Example: Professional Upload Handler
public function process_product_image(): void {
$product_id = segment(3, 'int');
// Validate upload
$this->validation->set_rules('userfile', 'Product Image', 'required|allowed_types[jpg,png,webp]|max_size[5000]');
if ($this->validation->run() === true) {
// Configure upload
$config = [
'destination' => 'products/originals',
'upload_to_module' => true,
'target_module' => 'shop',
'make_rand_name' => true
];
// Ensure directories exist
$dirs = ['products/originals', 'products/large', 'products/medium', 'products/thumbnails'];
foreach ($dirs as $dir) {
$path = 'modules/shop/' . $dir;
if (!$this->file->exists($path)) {
$this->file->create_directory($path, 0755);
}
}
// Upload original
$file_info = $this->image->upload($config);
// Generate sizes
$sizes = [
'large' => ['width' => 1200, 'quality' => 90],
'medium' => ['width' => 600, 'quality' => 85],
'thumbnail' => ['width' => 300, 'quality' => 80]
];
$generated_paths = [];
foreach ($sizes as $size_name => $settings) {
// Load original fresh for each size
$this->image->load($file_info['file_path']);
// Resize
$this->image->resize_to_width($settings['width']);
// Save with appropriate quality
$output_path = 'modules/shop/products/' . $size_name . '/' . $file_info['file_name'];
$this->image->save($output_path, $settings['quality'], 0644);
$this->image->destroy();
$generated_paths[$size_name] = $output_path;
}
// Update product record
$update_data = [
'image_original' => $file_info['file_path'],
'image_large' => $generated_paths['large'],
'image_medium' => $generated_paths['medium'],
'image_thumbnail' => $generated_paths['thumbnail'],
'image_updated_at' => date('Y-m-d H:i:s')
];
$this->model->update($product_id, $update_data, 'products');
set_flashdata('Product image uploaded and processed successfully');
redirect('shop/products/edit/' . $product_id);
} else {
$this->image_upload_form($product_id);
}
}
Best Practices
Output and Saving Guidelines:
- ✅ Use quality 85-90 for production web images
- ✅ Set appropriate file permissions (0644 for public, 0600 for private)
- ✅ Always set Content-Type header when using output()
- ✅ Add caching headers (Cache-Control, ETag) for dynamically served images
- ✅ Pre-generate common sizes during upload
- ✅ Use output() for watermarks and user-specific content
- ✅ Store originals and generate variants as needed
- ✅ Validate size parameters when serving on-demand
- ✅ Call destroy() after output() to free memory
- ✅ Check file existence before load() operations
- ✅ Consider WEBP for modern browsers (better compression)
- ❌ Never output images without setting Content-Type header
- ❌ Avoid storing binary image data in databases (use filesystem)
- ❌ Don't generate unlimited sizes (validate against allowed list)