Fast JPEG Info in PHP

By

PHP Logo

PHP function to efficiently read a JPEG file for size and optimization info.

This is an alternative to PHP’s getimagesize() function (http://php.net/manual/en/function.getimagesize.php) which reportedly reads the entire JPG file before returning info. I needed something much faster and the ability to report the JPG’s optimization type, “baseline” or “progressive”, which getimagesize() does not return.

On the getimagesize() PHP manual page, the top-voted script that attempts a similar goal was not as elegant and efficient, and possibly not as accurate in parsing all types of JPG files. So here is my contribution.

It wouldn’t take much work to return the application-specific (APPn) records by adding conditions to the switch statement, but as I didn’t need them, that code is omitted.

It seems to be much faster than PHP’s built-in function, especially when processing large images.

Usage

jpginfo(string $imagePath): array|false

The jpginfo() function will determine the size of a JPG image file and return the dimensions along with the optimization type.

Parameters

  • filename: This parameter specifies the file you wish to retrieve information about. It can reference a local file or (configuration permitting) a remote file using one of the supported streams.

Return Values

Returns false on error (e.g., unable to read file data, invalid JPG data markers, etc.), or an associative array with the following elements:

  • height: Height in pixels
  • width: Width in pixels
  • progressive: true for progressive optimization, false for baseline optimization
  • bits: The bit depth of the image (usually 8)

Notes

Not all streams support seeking. For those that do not support seeking, forward seeking from the current position is accomplished by reading and discarding data; other forms of seeking will fail. Therefore, jpginfo works most optimally on local filesystem files.

If you’re looking for a function to return a JPG’s compression quality, this is not it. That value is not defined by the JPG format specification. It might be parsed from application-specific data records, like Exif, if creation software adds it. Attempts to estimate compression quality by analyzing quantization tables might be of interest (see http://www.hackerfactor.com/src/jpegquality.c).

jpginfo.php

<?php

/**
 * jpginfo.php
 *
 * Efficiently parses JPEG stream by reading minimal data in order
 * to return size and optimization info quickly.
 *
 * https://github.com/morganwdavis/jpginfo
 * Copyright (C) 2014 Morgan Davis; Licensed MIT
 *
 * See:
 *
 * Wikipedia - JPEG:    http://en.wikipedia.org/wiki/JPEG
 * JPEG File structure: http://www.xbdev.net/image_formats/jpeg/tut_jpg/jpeg_file_layout.php
 * Estimate Quality:    http://www.hackerfactor.com/src/jpegquality.c
 *
 * @param  string $imagePath Path to JPEG image
 * @return array|false       False on error, or
 *                           Associative array with keys: bits, height, width, progressive
 */

declare(strict_types=1);

// JPEG marker constants
const JPEG_SOI = "\xFF\xD8"; // Start Of Image
const JPEG_EOI = 0xD9;       // End Of Image (marker byte only)
const JPEG_SOF0 = 0xC0;      // Start Of Frame (Baseline DCT)
const JPEG_SOF2 = 0xC2;      // Start Of Frame (Progressive DCT)

// String containing marker bytes (excluding FF) that signal segment start
// Used for quick validation after reading 0xFF marker prefix.
const JPEG_MARKER_SEGMENTS =
    "\xC0" . // SOF0
    "\xC2" . // SOF2
    "\xC4" . // DHT
    "\xDB" . // DQT
    "\xDD" . // DRI
    "\xDA" . // SOS
    "\xD0\xD1\xD2\xD3\xD4\xD5\xD6\xD7" . // RSTn
    "\xE0\xE1\xE2\xE3\xE4\xE5\xE6\xE7\xE8\xE9\xEA\xEB\xEC\xED\xEE\xEF" . // APPn
    "\xFE" . // COM
    "\xD9";  // EOI (though usually we stop before EOI)

/**
 * Parses a JPEG file to extract width, height, bits, and progressive flag.
 *
 * @param string $imagePath
 * @return array{width: int, height: int, bits: int, progressive: bool}|false
 */
function jpginfo(string $imagePath): array|false
{
    $fileHandle = @fopen($imagePath, 'rb');
    if ($fileHandle === false) {
        return false;
    }

    try {
        $soiMarker = fread($fileHandle, 2);
        if ($soiMarker === false || $soiMarker !== JPEG_SOI) {
            return false;
        }

        while (!feof($fileHandle)) {
            $markerBytes = fread($fileHandle, 2);
            if ($markerBytes === false || strlen($markerBytes) !== 2) {
                break;
            }

            if ($markerBytes[0] !== "\xFF") {
                break;
            }

            $markerType = $markerBytes[1];
            $markerTypeCode = ord($markerType);

            if ($markerTypeCode === JPEG_EOI) {
                break;
            }

            if (strpos(JPEG_MARKER_SEGMENTS, $markerType) === false) {
                break;
            }

            $lengthBytes = fread($fileHandle, 2);
            if ($lengthBytes === false || strlen($lengthBytes) !== 2) {
                break;
            }

            $segmentLength = unpack('n', $lengthBytes)[1];
            if ($segmentLength < 2) {
                break;
            }
            $dataLength = $segmentLength - 2;

            switch ($markerTypeCode) {
                case JPEG_SOF0:
                case JPEG_SOF2:
                    if ($dataLength < 5) {
                        break 2;
                    }

                    $sofData = fread($fileHandle, 5);
                    if ($sofData === false || strlen($sofData) !== 5) {
                        break 2;
                    }

                    $info = unpack('Cbits/nheight/nwidth', $sofData);
                    if ($info === false) {
                        break 2;
                    }

                    $info['progressive'] = ($markerTypeCode === JPEG_SOF2);
                    return $info;

                default:
                    if ($dataLength > 0) {
                        if (fseek($fileHandle, $dataLength, SEEK_CUR) === -1) {
                            break 2;
                        }
                    }
                    break;
            }
        }

        return false;
    } finally {
        if (is_resource($fileHandle)) {
            fclose($fileHandle);
        }
    }
}

See Project on GitHub

Efficiently Read JPEG Info in PHP

Explore
Reply
Notifications: 
How is my info protected?
  • Your email address is kept private and is never shown.
  • You are not signing up for anything.
  • You only get email for reply activity if notifications are enabled.
  • You can edit or delete your reply (using same device and browser).
  • Your info is not shared or used in any other way.
Message
How is my info used and protected?
  • Your email address and message are kept private.
  • You are not signing up for anything.
  • Your info is not shared or used in any other way.