From 47ebde88655e9a109ce3d37c67b6b1ef741c8b65 Mon Sep 17 00:00:00 2001 From: Chris Leppanen Date: Wed, 26 Jan 2022 11:15:14 -0500 Subject: [PATCH 1/5] Use a memory stream instead of a data stream --- composer.json | 3 ++- src/PNGMetadata.php | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 8eb930b..837b555 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ ], "require": { "php": "^7.4 || ^8.0 <8.1", - "ext-exif": "*" + "ext-exif": "*", + "ext-gd": "*" }, "require-dev": { "phpstan/phpstan": "^0.12.74", diff --git a/src/PNGMetadata.php b/src/PNGMetadata.php index 70e617b..7abf473 100644 --- a/src/PNGMetadata.php +++ b/src/PNGMetadata.php @@ -43,8 +43,12 @@ class PNGMetadata extends ArrayObject */ private array $metadata = []; - /** Exif data (jpg) in base64. */ - private string $exif_data = ''; + /** + * Exif data (jpg) in in a memory stream.. + * + * @var resource + */ + private $exif_data; /** * The list of data chunks. @@ -213,7 +217,10 @@ public static function getType(string $path) public function getThumbnail() { if ($this->exif_data) { - return imagecreatefromstring(exif_thumbnail($this->exif_data)); + $image = imagecreatefromstring(exif_thumbnail($this->exif_data)); + rewind($this->exif_data); + + return $image; } return false; @@ -462,11 +469,16 @@ private function extractRGB(): void private function extractExif(): void { if (isset($this->chunks['eXIf'])) { - $this->exif_data = 'data://image/jpeg;base64,' . base64_encode($this->chunks['eXIf']); + $this->exif_data = fopen('php://memory', 'r+b'); + fwrite($this->exif_data, $this->chunks['eXIf']); + rewind($this->exif_data); + $this->metadata['exif'] = array_replace( $this->metadata['exif'] ?? [], exif_read_data($this->exif_data), ); + + rewind($this->exif_data); } } From 33849a8aa7369aa562d57384f640afc121886042 Mon Sep 17 00:00:00 2001 From: joserick Date: Wed, 14 Dec 2022 10:07:32 -0600 Subject: [PATCH 2/5] feat: Updated actions --- .github/workflows/coding-style.yml | 8 ++++---- .github/workflows/main.yml | 6 +++--- README.md | 1 + composer.json | 2 +- src/PNGMetadata.php | 25 ++++++++++++++----------- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 6102cd5..9c05709 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -7,10 +7,10 @@ jobs: name: Nette Code Checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress @@ -20,10 +20,10 @@ jobs: name: Nette Coding Standard runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3.1.0 - uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 coverage: none - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress --ignore-platform-reqs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea29a72..ac4daff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,9 +10,9 @@ jobs: - uses: actions/checkout@master - name: Install PHP - uses: shivammathur/setup-php@master + uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.1 - name: Install composer deps run: | @@ -23,7 +23,7 @@ jobs: composer install --no-interaction --prefer-dist - name: The PHP Security Checker - uses: symfonycorp/security-checker-action@v2 + uses: symfonycorp/security-checker-action@v5 - name: Check PHPStan rules run: composer phpstan diff --git a/README.md b/README.md index d805adf..0ea956a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ It is important to mention that PNGMetadata does **not** use the software ExifTo - PHP >= 7.4 - XML PHP Extension - JSON PHP Extension +- GD PHP Extension - EXIF PHP Extension ## Installation diff --git a/composer.json b/composer.json index 837b555..81f6ec7 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php": "^7.4 || ^8.0 <8.1", + "php": "^7.4 || ^8.0", "ext-exif": "*", "ext-gd": "*" }, diff --git a/src/PNGMetadata.php b/src/PNGMetadata.php index 7abf473..8f571d9 100644 --- a/src/PNGMetadata.php +++ b/src/PNGMetadata.php @@ -4,16 +4,11 @@ namespace PNGMetadata; - use ArrayObject; /** * PNG Metadata. * - * @author José Erick Carreón Gómez - * @copyright (c) 2019 José Erick Carreón Gómez - * @license http://www.gnu.org/licenses/gpl-3.0.html GNU Public Licence (GPLv3) - * * This file is part of photo-metadata. * * PNGMetadata is free software: you can redistribute it and/or modify @@ -46,7 +41,7 @@ class PNGMetadata extends ArrayObject /** * Exif data (jpg) in in a memory stream.. * - * @var resource + * @var resource|null */ private $exif_data; @@ -212,17 +207,25 @@ public static function getType(string $path) * * @see PNGMetadata::$exif_data Efix data formatted. * - * @return resource|false + * @return \GdImage|false */ public function getThumbnail() { + // Check if the Exif data is not null. if ($this->exif_data) { - $image = imagecreatefromstring(exif_thumbnail($this->exif_data)); - rewind($this->exif_data); + // Check if the Exif data contains a thumbnail + if ($thumb = exif_thumbnail($this->exif_data)) { + // Create an image resource from the thumbnail data + $image = imagecreatefromstring($thumb); - return $image; + // Rewind the Exif data to the beginning + rewind($this->exif_data); + + return $image; + } } + // No thumbnail found or error occurred return false; } @@ -475,7 +478,7 @@ private function extractExif(): void $this->metadata['exif'] = array_replace( $this->metadata['exif'] ?? [], - exif_read_data($this->exif_data), + exif_read_data($this->exif_data, null, true), ); rewind($this->exif_data); From baf7e7103a736f78c938b6049394b54378b9ceb8 Mon Sep 17 00:00:00 2001 From: joserick Date: Wed, 14 Dec 2022 11:14:20 -0600 Subject: [PATCH 3/5] fix: Integrity check --- .github/workflows/main.yml | 2 +- src/PNGMetadata.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac4daff..9582d73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress # Install app deps - composer install --no-interaction --prefer-dist + composer install --no-interaction --prefer-dist --ignore-platform-req=ext-gd - name: The PHP Security Checker uses: symfonycorp/security-checker-action@v5 diff --git a/src/PNGMetadata.php b/src/PNGMetadata.php index 8f571d9..24a23cc 100644 --- a/src/PNGMetadata.php +++ b/src/PNGMetadata.php @@ -211,7 +211,7 @@ public static function getType(string $path) */ public function getThumbnail() { - // Check if the Exif data is not null. + // Check if the Exif data is not null. if ($this->exif_data) { // Check if the Exif data contains a thumbnail if ($thumb = exif_thumbnail($this->exif_data)) { @@ -516,7 +516,7 @@ private function extractTExif(): void private function extractXMP(): void { if (isset($this->chunks['iTXt']) && strncmp($this->chunks['iTXt'], 'XML:com.adobe.xmp', 17) === 0) { - $dom = new \DomDocument('1.0', 'UTF-8'); + $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->preserveWhiteSpace = false; $dom->formatOutput = false; $dom->substituteEntities = false; From 717b0665730c11e9d3e13d333fd187df591e1ed2 Mon Sep 17 00:00:00 2001 From: joserick Date: Mon, 13 Mar 2023 00:18:08 -0600 Subject: [PATCH 4/5] task: Added list steps as comments to each method via github copilot --- src/PNGMetadata.php | 80 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/PNGMetadata.php b/src/PNGMetadata.php index 24a23cc..ed2f236 100644 --- a/src/PNGMetadata.php +++ b/src/PNGMetadata.php @@ -262,6 +262,7 @@ private function extractNodesXML($node) $child = $node->childNodes->item($i); if ($child !== null) { $childValues = $this->extractNodesXML($child); + // if the node has a tag name, it is not a text node if (isset($child->tagName)) { [$prefixTagName, $suffixTagName] = explode(':', $child->tagName); if (is_array($childValues)) { @@ -273,6 +274,8 @@ private function extractNodesXML($node) } else { $output = $this->arrayMerge($output, $childValues); } + // if the node has a prefix and the prefix is in the list of prefixes, it is a XMP tag + // if the node has a suffix and the suffix is in the list of suffixes, it is a XMP tag } elseif ( \in_array($prefixTagName, $this->prefSuffXMP, true) || \in_array($prefixTagName, $this->tagsXMP, true) @@ -282,9 +285,11 @@ private function extractNodesXML($node) } else { $output[$suffixTagName][] = $childValues; } + // if the node has a prefix and the prefix is not in the list of prefixes, it is not a XMP tag } else { $output[$prefixTagName][$suffixTagName][] = $childValues; } + // if the node does not have a tag name, it is a text node } elseif ($childValues || $childValues === '0') { $output = is_array($childValues) ? implode(', ', $childValues) @@ -317,22 +322,32 @@ private function extractNodesXML($node) */ private function printVertical(string $lastKey = '', ?array $array = null): array { + // Create a new array to store the result. $columns = []; + + // If the key is not empty, add a colon to the end of the key. if ($lastKey !== '') { $lastKey .= ':'; } + + // Traverse the array. foreach ($array ?: $this->metadata as $key => $value) { + // If the value is a array, call the function recursively. if (is_array($value)) { + // If the value is an indexed array, merge the key and value together. if (isset($value[0])) { $columns[] = [$lastKey . $key => implode(',', $value)]; } else { + // If the value is a associative array, call the function recursively. $columns[] = $this->printVertical($lastKey . $key, $value); } } else { + // If the value is not an array, merge the key and value together. $columns[] = [$lastKey . $key => $value]; } } + // Return the merged result. return array_merge(...$columns); } @@ -346,18 +361,26 @@ private function printVertical(string $lastKey = '', ?array $array = null): arra */ private function extractChunks(): void { + // Open the image file in binary mode. $content = fopen($this->path, 'rb'); + + // Verify that the file is a PNG file. if (fread($content, 8) !== "\x89PNG\x0d\x0a\x1a\x0a") { throw new \InvalidArgumentException('Invalid PNG file signature, path "' . $this->path . '" given.', 104); } + // The PNG image is composed of a series of chunks. Each chunk is composed of + // a 4-byte length, a 4-byte type, a variable number of data bytes, and a + // 4-byte CRC. $chunkHeader = fread($content, 8); while ($chunkHeader) { $chunk = unpack('Nsize/a4type', $chunkHeader); if ($chunk['type'] === 'IEND') { break; } + // Read the chunk data. if ($chunk['type'] === 'tEXt') { + // For the tEXt chunk, the data is a keyword and a text string. $this->chunks[$chunk['type']][] = explode("\0", fread($content, $chunk['size'])); fseek($content, 4, SEEK_CUR); } else { @@ -367,10 +390,13 @@ private function extractChunks(): void || $chunk['type'] === 'iTXt' || $chunk['type'] === 'bKGD' ) { + // For the eXIf, sRGB, iTXt, and bKGD chunks, the data is a byte + // sequence. $lastOffset = ftell($content); $this->chunks[$chunk['type']] = fread($content, $chunk['size']); fseek($content, $lastOffset, SEEK_SET); } elseif ($chunk['type'] === 'IHDR') { + // For the IHDR chunk, the data is a series of fields. $lastOffset = ftell($content); for ($i = 0; $i < 6; $i++) { $this->chunks[$chunk['type']][] = fread($content, ($i > 1 ? 1 : 4)); @@ -394,6 +420,7 @@ private function extractChunks(): void private function extractIHDR(): void { if (isset($this->chunks['IHDR'])) { + // Create a array that will be used to associate the chunk data with the corresponding metadata name. $ihdr = [ 'ImageWidth', 'ImageHeight', @@ -418,14 +445,20 @@ private function extractIHDR(): void ], ]; + // Loop through each chunk data. foreach ($this->chunks['IHDR'] as $key => $value) { + // Check if the current chunk data is not the first two bytes of the chunk. if ($key > 1) { + // Check if the current chunk data is the third byte of the chunk. if ($key === 2) { + // Add the metadata name (in the $ihdr array) corresponding to the chunk data and the value of the chunk data. $this->metadata['IHDR'][$ihdr[$key]] = ord($value); } else { + // Add the metadata name (in the $ihdr array) corresponding to the chunk data and the value of the chunk data. $this->metadata['IHDR'][$ihdr[$key][8]] = $ihdr[$key][ord($value)]; } } else { + // Add the metadata name (in the $ihdr array) corresponding to the chunk data and the value of the chunk data. $this->metadata['IHDR'][$ihdr[$key]] = unpack('Ni', $value)['i']; } } @@ -436,12 +469,19 @@ private function extractIHDR(): void /** * Extract KGD type from bKGD chunk as a array. * + * The format of bKGD chunk is: + * - For indexed-color images: 1 byte. + * - For grayscale images: 2 bytes. + * - For truecolor images: 6 bytes. + * * @see PNGMetadata::$metadata For the property whose metadata are storage. * @see PNGMetadata::$chunks For the property whose chunks data are storage. */ private function extractBKGD(): void { if (isset($this->chunks['bKGD'])) { + // If the length of bKGD chunk is less than 2, it is an indexed-color images. + // Otherwise it is a grayscale or truecolor images. $this->metadata['bKGD'] = implode(' ', unpack(strlen($this->chunks['bKGD']) < 2 ? 'C' : 'n*', $this->chunks['bKGD'])); } } @@ -456,8 +496,11 @@ private function extractBKGD(): void private function extractRGB(): void { if (isset($this->chunks['sRGB'])) { + // $rgb is an array with 4 possible values $rgb = ['Perceptual', 'Relative Colorimetric', 'Saturation', 'Absolute Colorimetric']; + // Unpack the sRGB chunk data as a byte $unpacked = unpack('C', $this->chunks['sRGB']); + // Set the sRGB value to the value of $rgb that matches the byte $this->metadata['sRGB'] = $rgb[end($unpacked)] ?? 'Unknown'; } } @@ -471,16 +514,22 @@ private function extractRGB(): void */ private function extractExif(): void { + // Check if eXIf chunk exists. if (isset($this->chunks['eXIf'])) { + // Open eXIf chunk data as a memory stream. $this->exif_data = fopen('php://memory', 'r+b'); + // Write eXIf chunk data to the memory stream. fwrite($this->exif_data, $this->chunks['eXIf']); + // Set the pointer of memory stream to the beginning of the stream. rewind($this->exif_data); + // Read Exif data from memory stream. $this->metadata['exif'] = array_replace( $this->metadata['exif'] ?? [], exif_read_data($this->exif_data, null, true), ); + // Set the pointer of memory stream to the beginning of the stream. rewind($this->exif_data); } } @@ -494,12 +543,21 @@ private function extractExif(): void */ private function extractTExif(): void { + // Check if the tEXt chunk is present in the PNG file. if (isset($this->chunks['tEXt']) && is_array($this->chunks['tEXt'])) { + + // Loop over all tEXt chunks. foreach ($this->chunks['tEXt'] as $exif) { + + // Split the key into parts. [$group, $tag, $tag2] = array_pad(explode(':', $exif[0]), 3, null); + + // Convert the thumbnail key to uppercase. if ($tag === 'thumbnail') { $tag = strtoupper($tag); } + + // Store the value in the metadata array. $this->metadata[$group][$tag] = ($tag2 ? [$tag2 => $exif[1]] : $exif[1]); } } @@ -516,18 +574,27 @@ private function extractTExif(): void private function extractXMP(): void { if (isset($this->chunks['iTXt']) && strncmp($this->chunks['iTXt'], 'XML:com.adobe.xmp', 17) === 0) { + // create a new DOMDocument instance $dom = new \DOMDocument('1.0', 'UTF-8'); + // set the preserveWhiteSpace property to false to remove white space $dom->preserveWhiteSpace = false; + // set the formatOutput property to false to remove extra line breaks $dom->formatOutput = false; + // set the substituteEntities property to false to prevent entity substitution $dom->substituteEntities = false; + // load the XML data from the string $dom->loadXML(ltrim(substr($this->chunks['iTXt'], 17), "\x00")); + // set the encoding to UTF-8 $dom->encoding = 'UTF-8'; + // check if the root node of the XML document is of type x:xmpmeta if ($dom->documentElement->nodeName !== 'x:xmpmeta') { error_log('ExtractRoot node must be of type x:xmpmeta.'); return; } + // extract the metadata from the XML document if (!empty($result = $this->flatten($this->extractNodesXML($dom->documentElement)))) { + // store the metadata in the PNGMetadata::$metadata property $this->metadata['xmp'] = $result; } } @@ -542,14 +609,20 @@ private function extractXMP(): void */ private function flatten(array $array): array { + // Loop through each key in the array foreach ($array as $key => $value) { + // If the value is an array if (is_array($value)) { + // If the array has only one key, and that key is 0 if (isset($value[0]) && \count($value) === 1) { + // Set the value of this key to be the value of key 0 $array[$key] = $value[0]; + // If the new value is an array, call flatten again to flatten it if (is_array($array[$key])) { $array[$key] = $this->flatten($array[$key]); } } else { + // If the array doesn't have only one key, call flatten again to flatten it $array[$key] = $this->flatten($value); } } @@ -568,17 +641,24 @@ private function flatten(array $array): array */ private function arrayMerge(array $baseArray, array $array): array { + // Loop through the array to be merged. foreach ($array as $key => $value) { + // If the value is an object, get the value of the object. if (is_object($value)) { $value = $value->value; } + + // If the key already exists in the base array, merge the values. if (isset($baseArray[$key])) { + // If the value is an array, merge the arrays. if (is_array($value)) { $baseArray[$key] = $this->arrayMerge($baseArray[$key], $array[$key]); } elseif ($baseArray[$key] !== $value) { + // If the value is not an array, concatenate the values. $baseArray[$key] .= ',' . $value; } } else { + // If the key does not exist, create the key and set the value. $baseArray[$key] = $value; } } From a1f749cc98a81a40302f45f2e4429ebdf8be25a7 Mon Sep 17 00:00:00 2001 From: Jose Erick Carreon Date: Fri, 16 Feb 2024 21:16:44 -0600 Subject: [PATCH 5/5] chore: Added buymeacoffee --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ea956a..9b22043 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ PNGMetadata is a library which is able to extract the metadata of an image in pn It is important to mention that PNGMetadata does **not** use the software ExifTool so it is completely native. ![PNGMetadata.gif](https://joserick.com/docs/pngmetadata/PNGMetadata.gif) - +## Enjoying this package? [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/joserick) ## Requirements - PHP >= 7.4