Last updated: May 29, 2025

<span aria-hidden="true" id="fix-rotated-image-uploads-in-php-with-jpegtran"></span>

# Fix rotated image uploads in PHP with Jpegtran

![Kevin van Zonneveld](/assets/images/teammates/avatar-kvz-4.jpg?dpl=dpl_5eQDcFwLGbzg3PfkSEW2a2j2x5pY)

**Kevin van Zonneveld**

Co-founder · Amsterdam, The Netherlands · Show bio

[](https://x.com/kvz)[](https://github.com/kvz)

Mobile devices often embed orientation data in photos, causing images to appear rotated when uploaded to web applications. This common issue can frustrate users and developers alike. Luckily,`jpegtran`—a tiny command-line program shipped with libjpeg—can rotate JPEGs without re-encoding, so you fix orientation while keeping every pixel intact.

<span aria-hidden="true" id="understand-exif-orientation-data"></span>

## Understand exif orientation data

Photos taken with modern phones include EXIF metadata that records, among many other things, how the camera was held. Viewers that honor this field will render the picture upright, while software that ignores it will show the raw pixels—which is why freshly uploaded images sometimes look sideways or upside-down.

The orientation tag can hold these relevant values:

* **1** – Normal
* **3** – 180-degree rotation
* **6** – 90-degree clockwise rotation
* **8** – 90-degree counter-clockwise rotation

<span aria-hidden="true" id="rotate-images-in-php-with-gd-or-imagemagick"></span>

## Rotate images in PHP with gd or ImageMagick

The classic PHP fix is to load the JPEG into GD or ImageMagick, rotate it, and write it back:

```php
<?php
$image   = imagecreatefromjpeg('photo.jpg');
$rotated = imagerotate($image, 90, 0);
imagejpeg($rotated, 'photo-fixed.jpg');

```

It works, but the file is decompressed and recompressed, which can soften fine details and burn CPU cycles on busy servers.

<span aria-hidden="true" id="use-jpegtran-for-lossless-rotation"></span>

## Use Jpegtran for lossless rotation

`jpegtran` sidesteps re-encoding. It rearranges the compressed MCU (Minimum Coded Unit) blocks so the pixels stay in their original form—bit-for-bit—after rotation. It also supports progressive JPEGs, keeps Huffman tables untouched, and, with the `-copy all` flag, preserves every EXIF entry.

<span aria-hidden="true" id="install-jpegtran-in-your-php-environment"></span>

## Install Jpegtran in your PHP environment

```bash
# Debian/Ubuntu
sudo apt-get install libjpeg-progs

# Rhel/centos
sudo yum install libjpeg-turbo-utils

# macOS (homebrew)
brew install jpeg

```

Verify the binary is available:

```bash
jpegtran -version

```

<span aria-hidden="true" id="check-system-requirements"></span>

## Check system requirements

Before you dive in, make sure both the EXIF extension and `jpegtran` are present:

```php
<?php
function checkRequirements(): void
{
    if (!extension_loaded('exif')) {
        throw new RuntimeException('The EXIF extension is not enabled.');
    }

    $jpegtranPath = trim(shell_exec('command -v jpegtran')); // @phpstan-ignore-line
    if ($jpegtranPath === '') {
        throw new RuntimeException('jpegtran is not installed or not in PATH.');
    }
}

```

<span aria-hidden="true" id="read-exif-data-with-php"></span>

## Read exif data with PHP

```php
<?php
function getOrientation(string $file): int
{
    if (!is_readable($file)) {
        throw new RuntimeException("File {$file} is not readable.");
    }
    $exif = @exif_read_data($file);
    if ($exif === false) {
        // No EXIF block or unreadable — assume upright
        // You might want to log a warning here if EXIF data was expected
        return 1;
    }

    return (int) ($exif['Orientation'] ?? 1);
}

```

<span aria-hidden="true" id="call-jpegtran-from-php-safely"></span>

## Call Jpegtran from PHP safely

```php
<?php
function rotateJpeg(string $src, string $dst = null): string
{
    if (!is_readable($src)) {
        throw new RuntimeException("Source file {$src} is not readable.");
    }

    $dst ??= 'rotated_' . basename($src);

    $orientation = getOrientation($src);

    // Map EXIF orientation to jpegtran switch
    $map = [3 => 180, 6 => 90, 8 => 270];
    if (!isset($map[$orientation])) {
        // If orientation is 1 (normal) or any other unhandled value,
        // copy the file as is.
        if (!copy($src, $dst)) {
            throw new RuntimeException("Failed to copy {$src} to {$dst}.");
        }
        return $dst;
    }

    $degrees = $map[$orientation];

    $cmd = sprintf(
        'jpegtran -rotate %d -copy all %s > %s',
        $degrees,
        escapeshellarg($src),
        escapeshellarg($dst)
    );

    // Clear stat cache to ensure filesize check is accurate
    clearstatcache();
    shell_exec($cmd);

    if (!is_file($dst) || filesize($dst) === 0) {
        // You might want to check shell_exec's return value for errors too
        throw new RuntimeException("jpegtran failed to produce output for {$src}. Command: {$cmd}");
    }

    return $dst;
}

```

<span aria-hidden="true" id="batch-processing-multiple-images"></span>

## Batch processing multiple images

If you have multiple images to process, you can loop through them and apply the rotation logic. Here's a basic example:

```php
<?php
function batchRotateImages(array $sourceFiles, string $destinationDir): array
{
    if (!is_dir($destinationDir) || !is_writable($destinationDir)) {
        throw new RuntimeException("Destination directory {$destinationDir} is not writable or does not exist.");
    }

    $processedFiles = [];
    foreach ($sourceFiles as $srcFile) {
        try {
            $baseName = basename($srcFile);
            $dstFile = $destinationDir . DIRECTORY_SEPARATOR . 'rotated_' . $baseName;
            $processedFiles[$srcFile] = rotateJpeg($srcFile, $dstFile);
        } catch (RuntimeException $e) {
            // Log error for this specific file and continue with others
            error_log("Failed to process {$srcFile}: " . $e->getMessage());
            $processedFiles[$srcFile] = false; // Indicate failure
        }
    }
    return $processedFiles;
}

// Example usage:
// $filesToProcess = ['image1.jpg', 'path/to/image2.jpg'];
// $outputDirectory = 'processed_images';
// if (!is_dir($outputDirectory)) {
//     mkdir($outputDirectory, 0755, true);
// }
// $results = batchRotateImages($filesToProcess, $outputDirectory);
// print_r($results);

```

Make sure the destination directory exists and is writable by your PHP script.

<span aria-hidden="true" id="complete-working-example-with-error-handling"></span>

## Complete working example with error handling

```php
<?php

// Ensure these functions are defined or included from where they are declared above.
// function checkRequirements(): void { ... }
// function getOrientation(string $file): int { ... }
// function rotateJpeg(string $src, string $dst = null): string { ... }
// function batchRotateImages(array $sourceFiles, string $destinationDir): array { ... }

try {
    checkRequirements(); // Checks for EXIF extension and jpegtran

    $sourceFile = 'photo.jpg'; // Ensure this file exists for testing
    if (!file_exists($sourceFile)) {
         // Create a dummy file for testing if it doesn't exist
         // In a real scenario, ensure photo.jpg is a valid JPEG with EXIF data.
         if (!touch($sourceFile)) {
            error_log("Warning: Could not create dummy file {$sourceFile}. Ensure the directory is writable.");
         } else {
            error_log("Warning: {$sourceFile} does not exist. A dummy file was touched for the example to run. Rotation may not occur as expected without valid EXIF data.");
         }
    }

    $fixed = rotateJpeg($sourceFile, 'rotated_photo.jpg');
    echo "Saved correctly oriented file to $fixed\n";

    // Example for batch processing (optional, uncomment to test)
    /*
    $filesToProcess = [$sourceFile]; // Add more files as needed
    $outputDirectory = 'processed_batch';
    if (!is_dir($outputDirectory)) {
        if (!mkdir($outputDirectory, 0755, true)) {
            throw new RuntimeException("Could not create directory: {$outputDirectory}");
        }
    }
    echo "\nStarting batch processing...\n";
    $results = batchRotateImages($filesToProcess, $outputDirectory);
    print_r($results);
    echo "Batch processing finished. Check the '{$outputDirectory}' directory.\n";
    */

} catch (Throwable $e) {
    error_log("Error: " . $e->getMessage());
    // It's generally better to let PHP handle the response code
    // or set it based on the context (e.g., web request vs. CLI script)
    // http_response_code(500);
    echo "An error occurred. Check the error log for details.\n";
}


```

The three functions (`checkRequirements`, `getOrientation`, and `rotateJpeg`) together are fewer than 70 lines yet cover installation checks, EXIF parsing, secure command execution, and output validation.

<span aria-hidden="true" id="security-considerations-with-shell_exec"></span>

## Security considerations with shell\_exec

1. **Escape every argument** with `escapeshellarg()`—never concatenate raw user input. This is crucial to prevent command injection vulnerabilities.
2. **Validate dependencies** (`command -v jpegtran`) before calling them. Ensure `jpegtran` is installed and accessible in the system's PATH.
3. **Check command success** by verifying that the resulting file exists and has a non-zero size. Also, consider checking the return value of `shell_exec` (though it can be tricky as it returns the full output string, and errors from `jpegtran` might go to stderr).
4. **Limit write locations** to directories outside the public web root when possible, and ensure proper file permissions for read and write operations.
5. **Sanitize file paths**: Ensure that input file paths and output destinations are validated and sanitized to prevent directory traversal attacks or writing to unintended locations.

<span aria-hidden="true" id="handle-edge-cases-and-error-scenarios"></span>

## Handle edge cases and error scenarios

* **Missing EXIF data** – The `getOrientation` function defaults to orientation 1 (normal), and in such cases, `rotateJpeg` will copy the file as-is.
* **Unreadable source file** – The `getOrientation` and `rotateJpeg` functions now include checks using `is_readable()` and throw exceptions if a file cannot be read.
* **`jpegtran` command failure** – `rotateJpeg` checks if the output file was created and is not empty. The error message now includes the command for easier debugging.
* **Progressive JPEGs** – `jpegtran` rotates them perfectly; no extra flag required.
* **Large images** – Rotation is memory-efficient because data stays compressed, but make sure you have disk space for the temporary output file. Consider monitoring disk space if processing very large batches.
* **Unsupported orientation values** – The current code copies the image as-is if the EXIF orientation value is not 3, 6, or 8. You could modify it to throw an exception for other unsupported values if strict handling is required.
* **Permission errors** – Ensure the PHP script has read permissions for source files and write permissions for the destination directory. The `is_readable()` checks help, and file operation failures (like `copy()` or `jpegtran` output redirection) will typically result in exceptions or failed output checks.
* **File cleanup**: If temporary files are created or if original files need to be removed after successful processing, implement a cleanup mechanism. The current examples create new files (e.g.,`rotated_photo.jpg`), so explicit cleanup of originals might be needed depending on your workflow.

<span aria-hidden="true" id="why-jpegtran-excels-for-jpeg-rotation"></span>

## Why Jpegtran excels for JPEG rotation

* **Lossless** – The compressed data is never decoded, so pixels remain untouched.
* **Metadata-friendly** – `-copy all` keeps EXIF, XMP, and ICC profiles.
* **Often faster** – Because no decompression happens, it can finish quicker than re-encoding, but benchmark your own workload to be sure.
* **Stable** – Part of libjpeg for decades and available on virtually every server platform.

<span aria-hidden="true" id="wrap-up"></span>

## Wrap-up

With a handful of lines you can read the EXIF orientation, call `jpegtran`, and hand your users an upright photo—without losing a single bit of quality. Drop the functions above into your upload handler or queue worker, and sideways selfies will be a thing of the past.

At Transloadit, we use advanced image optimization techniques in our [🤖 /image/optimize](/docs/robots/image-optimize.md) robot, which supports JPEG, PNG, GIF, WebP and SVG formats with configurable optimization priorities.

\#php#jpegtran#image-processing#exif-orientation#lossless-jpeg#shell-exec-php#batch-image-processing#image-processing-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed

Cancel anytime
