220 lines
6.7 KiB
PHP
220 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Intervention\Image\Drivers\Gd\Analyzers;
|
|
|
|
use Intervention\Image\Analyzers\ResolutionAnalyzer as GenericResolutionAnalyzer;
|
|
use Intervention\Image\Exceptions\AnalyzerException;
|
|
use Intervention\Image\Exceptions\StreamException;
|
|
use Intervention\Image\Exceptions\InvalidArgumentException;
|
|
use Intervention\Image\Exceptions\MissingDependencyException;
|
|
use Intervention\Image\Interfaces\ImageInterface;
|
|
use Intervention\Image\Interfaces\OriginInterface;
|
|
use Intervention\Image\Interfaces\SpecializedInterface;
|
|
use Intervention\Image\Resolution;
|
|
use Intervention\Image\Traits\CanBuildStream;
|
|
use Throwable;
|
|
|
|
class ResolutionAnalyzer extends GenericResolutionAnalyzer implements SpecializedInterface
|
|
{
|
|
use CanBuildStream;
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*
|
|
* @see AnalyzerInterface::analyze()
|
|
*
|
|
* @throws InvalidArgumentException
|
|
* @throws AnalyzerException
|
|
*/
|
|
public function analyze(ImageInterface $image): mixed
|
|
{
|
|
$result = imageresolution($image->core()->native());
|
|
|
|
if (!is_array($result)) {
|
|
throw new AnalyzerException('Failed to read image resolution');
|
|
}
|
|
|
|
// GD returns 96x96 as resolution by default even if the image has no resolution.
|
|
// This is problematic because it is impossible to tell whether the image
|
|
// really has this resolution or whether it just corresponds to the default value.
|
|
//
|
|
// If GD's default resolution is returned here and the resolution is still unchanged
|
|
// we will make an attempt to find the resolution from origin.
|
|
if ($this->isGdDefaultResolution($result) && $image->core()->meta()->get('resolutionChanged') !== true) {
|
|
try {
|
|
$alternativeResoltion = $this->readResolutionFromOrigin($image->origin());
|
|
} catch (Throwable) {
|
|
$alternativeResoltion = [96, 96];
|
|
}
|
|
|
|
$result = $alternativeResoltion !== $result ? $alternativeResoltion : $result;
|
|
}
|
|
|
|
return new Resolution(...$result);
|
|
}
|
|
|
|
/**
|
|
* @param array<int|float> $resolution
|
|
*/
|
|
private function isGdDefaultResolution(array $resolution): bool
|
|
{
|
|
return intval($resolution[0] ?? 0) === 96 && intval($resolution[1] ?? 0) === 96;
|
|
}
|
|
|
|
/**
|
|
* @throws AnalyzerException
|
|
* @throws InvalidArgumentException
|
|
* @throws StreamException
|
|
* @return array<float>
|
|
*/
|
|
private function readResolutionFromOrigin(OriginInterface $origin): array
|
|
{
|
|
$handle = self::buildStreamOrFail(file_get_contents($origin->filePath()));
|
|
|
|
try {
|
|
try {
|
|
return $this->resolutionFromJfifHeader($handle);
|
|
} catch (Throwable) {
|
|
# code ...
|
|
}
|
|
|
|
try {
|
|
return $this->resolutionFromExifHeader($handle);
|
|
} catch (Throwable) {
|
|
# code ...
|
|
}
|
|
|
|
try {
|
|
return $this->resolutionFromPngPhys($handle);
|
|
} catch (Throwable) {
|
|
# code ...
|
|
}
|
|
|
|
throw new AnalyzerException('Unable to read resolution from path');
|
|
} finally {
|
|
fclose($handle);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param resource $handle
|
|
* @throws AnalyzerException
|
|
* @return array<float>
|
|
*/
|
|
private function resolutionFromJfifHeader($handle): array
|
|
{
|
|
// read first 20 bytes
|
|
rewind($handle);
|
|
$header = fread($handle, 20);
|
|
|
|
// find the JFIF segment
|
|
$offset = strpos($header, 'JFIF');
|
|
if ($offset === false) {
|
|
throw new AnalyzerException('Unable to read JFIF header');
|
|
}
|
|
|
|
// read bytes at known offsets relative to JFIF
|
|
$units = ord($header[$offset + 7]);
|
|
$x = unpack('n', substr($header, $offset + 8, 2))[1];
|
|
$y = unpack('n', substr($header, $offset + 10, 2))[1];
|
|
|
|
if ($units === 2) { // unit is dots per cm → convert to DPI
|
|
return [round($x * 2.54), round($y * 2.54)];
|
|
}
|
|
|
|
return [$x, $y]; // unit is DPI or no unit
|
|
}
|
|
|
|
/**
|
|
* @param resource $handle
|
|
* @throws MissingDependencyException
|
|
* @throws AnalyzerException
|
|
* @return array<float>
|
|
*/
|
|
private function resolutionFromExifHeader($handle): array
|
|
{
|
|
if (!function_exists('exif_read_data')) {
|
|
throw new MissingDependencyException('Unable to read exif data');
|
|
}
|
|
|
|
rewind($handle);
|
|
$data = @exif_read_data($handle, null, true);
|
|
|
|
if ($data === false) {
|
|
throw new AnalyzerException('Unable to read exif data');
|
|
}
|
|
|
|
if (isset($data['XResolution']) && isset($data['YResolution'])) {
|
|
$resolution = [$data['XResolution'], $data['YResolution']];
|
|
}
|
|
|
|
if (isset($data['IFD0']) && isset($data['IFD0']['XResolution']) && isset($data['IFD0']['YResolution'])) {
|
|
$resolution = [$data['IFD0']['XResolution'], $data['IFD0']['YResolution']];
|
|
}
|
|
|
|
if (!isset($resolution)) {
|
|
throw new AnalyzerException('Unable to read exif data');
|
|
}
|
|
|
|
return array_map(function (mixed $value): int|float {
|
|
if (strpos($value, '/') === false) {
|
|
return $value;
|
|
}
|
|
|
|
$values = array_map(fn(string $value): int => intval($value), explode('/', $value));
|
|
|
|
if ($values[1] === 0) {
|
|
throw new AnalyzerException('Unable to read exif data, division by zero');
|
|
}
|
|
|
|
return $values[0] / $values[1];
|
|
}, $resolution);
|
|
}
|
|
|
|
/**
|
|
* @param resource $handle
|
|
* @throws AnalyzerException
|
|
* @return array<float>
|
|
*/
|
|
private function resolutionFromPngPhys($handle): array
|
|
{
|
|
rewind($handle);
|
|
$signature = fread($handle, 8);
|
|
|
|
// no PNG content
|
|
if ($signature !== "\x89PNG\x0D\x0A\x1A\x0A") {
|
|
throw new AnalyzerException('Input must be PNG format');
|
|
}
|
|
|
|
$marker = '';
|
|
|
|
while (!feof($handle)) {
|
|
$marker = strlen($marker) < 4 ? $marker . fread($handle, 1) : substr($marker, 1) . fread($handle, 1);
|
|
|
|
// find pHYs chunk
|
|
if ($marker === 'pHYs') {
|
|
// find length
|
|
fseek($handle, -8, SEEK_CUR);
|
|
$length = fread($handle, 4);
|
|
$length = unpack('N', $length)[1];
|
|
fseek($handle, 4, SEEK_CUR);
|
|
|
|
// read data
|
|
$data = fread($handle, $length);
|
|
|
|
$x = unpack('N', substr($data, 0, 4))[1];
|
|
$y = unpack('N', substr($data, 4, 4))[1];
|
|
|
|
return [
|
|
round($x * .0254),
|
|
round($y * .0254),
|
|
];
|
|
}
|
|
}
|
|
|
|
return [0, 0];
|
|
}
|
|
}
|