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 pixelswidth
: Width in pixelsprogressive
:true
for progressive optimization,false
for baseline optimizationbits
: 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);
}
}
}