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 $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 */ 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 */ 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 */ 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 */ 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]; } }