Files
eCert-MBIP/vendor/intervention/image/src/Drivers/Gd/Analyzers/ResolutionAnalyzer.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];
}
}