forked from leftypol/leftypol
153 lines
4.3 KiB
PHP
153 lines
4.3 KiB
PHP
<?php
|
|
namespace Vichan\Data\Driver\Metadata;
|
|
|
|
|
|
class PngExifReader implements ExifReader {
|
|
// Chunks larger than this will be ignored.
|
|
private const MAX_CHUNK_SIZE = 1048576; // 1 MB
|
|
private const ORIENTATION_TAG_ID = 0x0112;
|
|
// Exif data type identifier.
|
|
private const TYPE_SHORT = 0x3;
|
|
|
|
// Errors, mostly for debugging.
|
|
private const ERR_BAD_EXIF = -1;
|
|
private const ERR_BAD_ENDIAN = -2;
|
|
private const ERR_BAD_MARK = -2;
|
|
private const ERR_BAD_OFFSET_TO_IFD = -3;
|
|
private const ERR_BAD_OFFSET_TO_DIR_ENTRY = -4;
|
|
private const ERR_BAD_ORIENTATION_NOT_FOUND = -5;
|
|
private const ERR_NOT_A_PNG = -6;
|
|
private const ERR_EOF = -7;
|
|
private const ERR_COULD_NOT_OPEN = -8;
|
|
|
|
|
|
private static function tryReadExifChunk(string $chunk_blob): int {
|
|
// Ensure the chunk starts with "Exif" (EXIF header).
|
|
$a = \substr($chunk_blob, 0, 6);
|
|
if ($a !== "Exif\0\0") {
|
|
return self::ERR_BAD_EXIF;
|
|
}
|
|
|
|
// Remove the "Exif\0\0" header.
|
|
// This is to simplify offset calculations later on since offsets are relative to the TIFF header.
|
|
$tiff_data = \substr($chunk_blob, 6);
|
|
|
|
// Determine byte order (II for little-endian, MM for big-endian).
|
|
$byte_order = \substr($tiff_data, 0, 2);
|
|
if ($byte_order === 'II') {
|
|
$little_endian = true;
|
|
} elseif ($byte_order === 'MM') {
|
|
$little_endian = false;
|
|
} else {
|
|
return self::ERR_BAD_ENDIAN;
|
|
}
|
|
|
|
// Verify tag mark.
|
|
$tag_mark = \substr($tiff_data, 2, 4);
|
|
if ($little_endian) {
|
|
if ($tag_mark !== "*\0") {
|
|
return self::ERR_BAD_MARK;
|
|
}
|
|
} else {
|
|
if ($tag_mark !== "\0*") {
|
|
return self::ERR_BAD_MARK;
|
|
}
|
|
}
|
|
|
|
// Unpack format string.
|
|
$unpack_fmt = $little_endian ? 'v' : 'n';
|
|
|
|
// Offset to the first IFD.
|
|
$current_offset = \unpack($unpack_fmt, \substr($tiff_data, 4, 2))[1];
|
|
if ($current_offset > \strlen($chunk_blob)) {
|
|
return self::ERR_BAD_OFFSET_TO_IFD;
|
|
}
|
|
|
|
while ($current_offset > 0) {
|
|
// Number of directory entries.
|
|
$num_entries = \unpack($unpack_fmt, \substr($tiff_data, $current_offset, 2))[1];
|
|
$current_offset += 2;
|
|
|
|
if ($current_offset + $num_entries * 12 > \strlen($chunk_blob)) {
|
|
return self::ERR_BAD_OFFSET_TO_DIR_ENTRY;
|
|
}
|
|
|
|
for ($i = 0; $i < $num_entries; $i++) {
|
|
$entry_offset = $current_offset + ($i * 12);
|
|
|
|
// Read tag ID.
|
|
$tag_id = \unpack($unpack_fmt, \substr($tiff_data, $entry_offset, 2))[1];
|
|
|
|
if ($tag_id === self::ORIENTATION_TAG_ID) {
|
|
$field_type = \unpack($unpack_fmt, \substr($tiff_data, $entry_offset + 2, 2))[1];
|
|
$value_count = \unpack($unpack_fmt, \substr($tiff_data, $entry_offset + 4, 4))[1];
|
|
|
|
if ($field_type === self::TYPE_SHORT && $value_count !== 1) {
|
|
// Read value. It is stored directly if it's <4 bytes.
|
|
$value_offset = $entry_offset + 8;
|
|
$orientation = \unpack($unpack_fmt, \substr($tiff_data, $value_offset, 2))[1];
|
|
|
|
return $orientation; // Return the Orientation value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Offset to the next IFD (last 4 bytes of the directory)
|
|
$current_offset = \unpack($unpack_fmt, \substr($tiff_data, $current_offset + ($num_entries * 12), 4))[1];
|
|
}
|
|
|
|
return self::ERR_BAD_ORIENTATION_NOT_FOUND;
|
|
}
|
|
|
|
// Mostly adapted from https://stackoverflow.com/a/2190438
|
|
private static function tryReadPngChunks(mixed $fd): int {
|
|
// Read the magic bytes and verify.
|
|
$header = \fread($fd, 8);
|
|
|
|
if ($header != "\x89PNG\x0d\x0a\x1a\x0a") {
|
|
return self::ERR_NOT_A_PNG;
|
|
}
|
|
|
|
// Loop through the PNG's chunks. Byte 0-3 is length, Byte 4-7 is type.
|
|
$chunkHeader = \fread($fd, 8);
|
|
|
|
while ($chunkHeader) {
|
|
// Extract length and type from binary data.
|
|
$chunk = @\unpack('Nsize/a4type', $chunkHeader);
|
|
|
|
if ($chunk['type'] === 'tEXt') {
|
|
if ($chunk['size'] > 0 && $chunk['size'] < self::MAX_CHUNK_SIZE) {
|
|
$size = $chunk['size'];
|
|
$chunk_blob = \fread($fd, $size);
|
|
$ret = self::tryReadExifChunk($chunk_blob);
|
|
if ($ret < 1) {
|
|
return $ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Skip to next chunk (over body and CRC)
|
|
\fseek($fd, $chunk['size'] + 4, SEEK_CUR);
|
|
|
|
// Read next chunk header
|
|
$chunkHeader = \fread($fd, 8);
|
|
}
|
|
|
|
return self::ERR_EOF;
|
|
}
|
|
|
|
public function getOrientation(string $file): ?int {
|
|
// Open the file.
|
|
$fd = \fopen($file, 'r');
|
|
if ($fd === false) {
|
|
return self::ERR_COULD_NOT_OPEN;
|
|
}
|
|
return self::tryReadPngChunks($fd);
|
|
\fclose($fd);
|
|
if ($ret < 1 || $ret > 9) {
|
|
return null;
|
|
} else {
|
|
return $ret;
|
|
}
|
|
}
|
|
}
|