From a8928bdb963e6550c91be4df38ae0bdb7fc6b389 Mon Sep 17 00:00:00 2001 From: Claude Diderich Date: Wed, 24 Aug 2022 10:51:22 +0200 Subject: [PATCH] Update code using new php 8.0 features --- CHANGELOG.md | 5 +- SECURITY.md | 3 +- composer.json | 5 +- src/Metadata.php | 552 ++++++++++++++++------------------- src/Metadata/Exception.php | 35 +-- src/Metadata/Exif.php | 220 ++++++-------- src/Metadata/Iptc.php | 170 +++++------ src/Metadata/Jpeg.php | 69 ++--- src/Metadata/Xmp.php | 101 +++---- src/Metadata/XmpDocument.php | 186 +++++------- test/example.php | 18 +- 11 files changed, 622 insertions(+), 742 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede8213..3d65e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ # CHANGELOG.md -Current version: `v1.1.1` +Current version: `v1.2.0` Notable changes to **Metadata** - A PHP class for reading and writing *Photo Metadata* from JPEG files in a transparent way: +## v1.2.0 - 2022-08-03 +Updated code to use new features of php 8.0 and make it more consistent with coding recommendations. + ## v1.1.1 - 2022-07-29 Corrected bug decoding XMP data. Added display of image data (i.e., EXIF data) to the test example. diff --git a/SECURITY.md b/SECURITY.md index 13e7bbb..d3cf2af 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,8 @@ See `README.md` for more details. | Version | Supported | | ------- | ------------------ | -| 1.1.x | :white_check_mark: | +| 1.2.0 | :white_check_mark: | +| 1.1.x | :x: | # Reporting a Vulnerability diff --git a/composer.json b/composer.json index 54d70a8..222465d 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,10 @@ }, "require": { "php": ">=8.0", + "ext-date": "*", "ext-dom": "*", - "ext-xml": "*" + "ext-gd": "*", + "ext-gettext": "*", + "ext-mbstring": "*", } } diff --git a/src/Metadata.php b/src/Metadata.php index 089cc30..5797a29 100644 --- a/src/Metadata.php +++ b/src/Metadata.php @@ -3,7 +3,7 @@ * Metadata.php - Image file metadata handing * * @project Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -21,110 +21,110 @@ class Metadata { - const VERSION = '1.1.1'; + public const VERSION = '1.2.0'; /** Fielt types */ - const TYPE_INVALID = 0; - const TYPE_STR = 1; - const TYPE_INT = 2; - const TYPE_FLOAT = 3; - const TYPE_ARY = 4; + public const TYPE_INVALID = 0; + public const TYPE_STR = 1; + public const TYPE_INT = 2; + public const TYPE_FLOAT = 3; + public const TYPE_ARY = 4; /** File specific fields: read only */ - const FILE_NAME = 001; /** String: Filename */ - const FILE_EXT = 002; /** String: File extension */ - const FILE_SIZE = 003; /** Int: File size */ - const FILE_DATE = 004; /** Int: Last modification date */ + public const FILE_NAME = 001; /** String: Filename */ + public const FILE_EXT = 002; /** String: File extension */ + public const FILE_SIZE = 003; /** Int: File size */ + public const FILE_DATE = 004; /** Int: Last modification date */ /** IPTC/XMP fields: read/write */ - const FIELD_ID_WRITE_FIRST = 100; /** First field identifier that can be modified */ - const FIELD_ID_WRITE_LAST = 132; /** Last field identifier that can be modified */ + public const FIELD_ID_WRITE_FIRST = 100; /** First field identifier that can be modified */ + public const FIELD_ID_WRITE_LAST = 132; /** Last field identifier that can be modified */ - const AUTHOR = 101; /** String: Creator (name of photographer) */ - const PHOTOGRAPHER = 101; /** - String: Creator (name of photographer) */ - const AUTHOR_TITLE = 102; /** String: Creator's job title */ - const PHOTOGRAPHER_TITLE = 102; /** - String: Creator's job title */ - const CAPTION = 103; /** String: Description/Caption */ - const CAPTION_WRITER = 104; /** String: Description writer */ - const CATEGORY = 105; /** String: Category - Max 3 characters */ - const CITY = 106; /** String: City */ - const COPYRIGHT = 107; /** String: Copyright notice */ - const COUNTRY = 108; /** String: Country name */ - const COUNTRY_CODE = 109; /** String: ISO country code*/ - const CREDIT = 110; /** String: Credit Line */ - const EDIT_STATUS = 111; /** String: Edit Status - Max 64 characters */ - const EVENT = 112; /** String: Event identifier */ - const GENRE = 113; /** Array: Genre */ - const HEADLINE = 114; /** String: Headline */ - const INSTRUCTIONS = 115; /** String: Instructions */ - const KEYWORDS = 116; /** Array: Keywords */ - const LOCATION = 117; /** String: Location */ - const OBJECT = 118; /** String: Object name (Title)*/ - const ORG_CODE = 119; /** Array: Code of Organization in image */ - const NAT = 119; /** - Array: Nationalities */ - const ORG_NAME = 120; /** Array: Name of Organization in image */ - const ORG = 120; /** - Array: Organizations/Teams in image */ - const PERSON = 121; /** Array: Person shown in image */ - const PEOPLE = 121; /** - Array: Person shown in image */ - const PERSONALITY = 121; /** - Array: Person shown in image (Getty terminology) */ - const PRIORITY = 122; /** Int: Urgency - 1 numeric character */ - const RATING = 123; /** Int: Numeric image rating, -1 (rejected), 0..5 */ - const SCENES = 124; /** Array: Scene codes*/ - const SOURCE = 125; /** String: Source */ - const STATE = 126; /** String: Providence/State */ - const SUBJECT_CODE = 127; /** Array: Subject code */ - const SUPP_CATEGORY_A = 128; /** String: Supplemental Category 1 */ - const SUPP_CATEGORY_B = 129; /** String: Supplemental Category 2 */ - const SUPP_CATEGORY_C = 130; /** String: Supplemental Category 3 */ - const TRANSFER_REF = 131; /** String: Original Transmission Reference - Max 32 characters */ - const USAGE_TERMS = 132; /** String: Rights Usage Terms */ + public const AUTHOR = 101; /** String: Creator (name of photographer) */ + public const PHOTOGRAPHER = 101; /** - String: Creator (name of photographer) */ + public const AUTHOR_TITLE = 102; /** String: Creator's job title */ + public const PHOTOGRAPHER_TITLE = 102; /** - String: Creator's job title */ + public const CAPTION = 103; /** String: Description/Caption */ + public const CAPTION_WRITER = 104; /** String: Description writer */ + public const CATEGORY = 105; /** String: Category - Max 3 characters */ + public const CITY = 106; /** String: City */ + public const COPYRIGHT = 107; /** String: Copyright notice */ + public const COUNTRY = 108; /** String: Country name */ + public const COUNTRY_CODE = 109; /** String: ISO country code*/ + public const CREDIT = 110; /** String: Credit Line */ + public const EDIT_STATUS = 111; /** String: Edit Status - Max 64 characters */ + public const EVENT = 112; /** String: Event identifier */ + public const GENRE = 113; /** Array: Genre */ + public const HEADLINE = 114; /** String: Headline */ + public const INSTRUCTIONS = 115; /** String: Instructions */ + public const KEYWORDS = 116; /** Array: Keywords */ + public const LOCATION = 117; /** String: Location */ + public const OBJECT = 118; /** String: Object name (Title)*/ + public const ORG_CODE = 119; /** Array: Code of Organization in image */ + public const NAT = 119; /** - Array: Nationalities */ + public const ORG_NAME = 120; /** Array: Name of Organization in image */ + public const ORG = 120; /** - Array: Organizations/Teams in image */ + public const PERSON = 121; /** Array: Person shown in image */ + public const PEOPLE = 121; /** - Array: Person shown in image */ + public const PERSONALITY = 121; /** - Array: Person shown in image (Getty terminology) */ + public const PRIORITY = 122; /** Int: Urgency - 1 numeric character */ + public const RATING = 123; /** Int: Numeric image rating, -1 (rejected), 0..5 */ + public const SCENES = 124; /** Array: Scene codes*/ + public const SOURCE = 125; /** String: Source */ + public const STATE = 126; /** String: Providence/State */ + public const SUBJECT_CODE = 127; /** Array: Subject code */ + public const SUPP_CATEGORY_A = 128; /** String: Supplemental Category 1 */ + public const SUPP_CATEGORY_B = 129; /** String: Supplemental Category 2 */ + public const SUPP_CATEGORY_C = 130; /** String: Supplemental Category 3 */ + public const TRANSFER_REF = 131; /** String: Original Transmission Reference - Max 32 characters */ + public const USAGE_TERMS = 132; /** String: Rights Usage Terms */ /** IPTC/XMP fiels: read only */ - const CREATED_DATETIME = 201; /** Int: Timestamp when photo was created */ + public const CREATED_DATETIME = 201; /** Int: Timestamp when photo was created */ /** Image data fields: read only **/ - const IMG_APERTURE = 301; /** Float: Aperture */ - const IMG_APERTURE_FMT = 302; /** String: Aperture (f/X) */ - const IMG_CAMERA_MAKE = 303; /** String: Camera brand */ - const IMG_CAMERA_MODEL = 304; /** String: Camera model */ - const IMG_CAMERA_SERIAL = 305; /** String: Camera serial number */ - const IMG_COLOR_SPACE_FMT = 306; /** String: Color space */ - const IMG_EXPOSURE = 307; /** Array: Exposure */ - const IMG_EXPOSURE_FMT = 308; /** String: Exposure (1/X second(s))*/ - const IMG_EXPOSURE_MODE_FMT = 309; /** String: Exposure mode */ - const IMG_EXPOSURE_PGM_FMT = 310; /** String: Exposure setting */ - const IMG_FLASH = 311; /** Int: Flash used */ - const IMG_FLASH_FMT = 312; /** String: Flash used (Flash | No flash)*/ - const IMG_FOCAL_LENGTH = 313; /** Int: Focal length */ - const IMG_FOCAL_LENGTH_FMT = 314; /** String: Focal length (X mm) */ - const IMG_HEIGHT = 315; /** Int: Image height */ - const IMG_ISO = 316; /** Int: ISO */ - const IMG_LENS_MAKE = 317; /** String: Lens brand */ - const IMG_LENS_MODEL = 318; /** String: Lens name */ - const IMG_LENS_SERIAL = 319; /** String: Lens serial number */ - const IMG_METERING_MODE_FMT = 320; /** String: Merering model */ - const IMG_ORIENTATION = 321; /** Int: Orientation */ - const IMG_RESOLUTION = 322; /** Int: Image resolution, in resolution unit */ - const IMG_RESOLUTION_FMT = 323; /** String: Image resolution, in resolution unit */ - const IMG_RESOLUTION_UNIT = 324; /** Int: Resolution unit (1, 3=cm / 2=inch) */ - const IMG_SIZE_FMT = 325; /** String: Formatted image size ( W x H px - X x Y cm (x MB) */ - const IMG_SOFTWARE = 326; /** String: Software used */ - const IMG_TYPE = 327; /** Int: Image type (see imagetypes() for constants) */ - const IMG_TYPE_FMT = 328; /** String: Image type (jpeg) */ - const IMG_WIDTH = 329; /** Int: Image width */ + public const IMG_APERTURE = 301; /** Float: Aperture */ + public const IMG_APERTURE_FMT = 302; /** String: Aperture (f/X) */ + public const IMG_CAMERA_MAKE = 303; /** String: Camera brand */ + public const IMG_CAMERA_MODEL = 304; /** String: Camera model */ + public const IMG_CAMERA_SERIAL = 305; /** String: Camera serial number */ + public const IMG_COLOR_SPACE_FMT = 306; /** String: Color space */ + public const IMG_EXPOSURE = 307; /** Array: Exposure */ + public const IMG_EXPOSURE_FMT = 308; /** String: Exposure (1/X second(s))*/ + public const IMG_EXPOSURE_MODE_FMT = 309; /** String: Exposure mode */ + public const IMG_EXPOSURE_PGM_FMT = 310; /** String: Exposure setting */ + public const IMG_FLASH = 311; /** Int: Flash used */ + public const IMG_FLASH_FMT = 312; /** String: Flash used (Flash | No flash)*/ + public const IMG_FOCAL_LENGTH = 313; /** Int: Focal length */ + public const IMG_FOCAL_LENGTH_FMT = 314; /** String: Focal length (X mm) */ + public const IMG_HEIGHT = 315; /** Int: Image height */ + public const IMG_ISO = 316; /** Int: ISO */ + public const IMG_LENS_MAKE = 317; /** String: Lens brand */ + public const IMG_LENS_MODEL = 318; /** String: Lens name */ + public const IMG_LENS_SERIAL = 319; /** String: Lens serial number */ + public const IMG_METERING_MODE_FMT = 320; /** String: Merering model */ + public const IMG_ORIENTATION = 321; /** Int: Orientation */ + public const IMG_RESOLUTION = 322; /** Int: Image resolution, in resolution unit */ + public const IMG_RESOLUTION_FMT = 323; /** String: Image resolution, in resolution unit */ + public const IMG_RESOLUTION_UNIT = 324; /** Int: Resolution unit (1, 3=cm / 2=inch) */ + public const IMG_SIZE_FMT = 325; /** String: Formatted image size ( W x H px - X x Y cm (x MB) */ + public const IMG_SOFTWARE = 326; /** String: Software used */ + public const IMG_TYPE = 327; /** Int: Image type (see imagetypes() for public constants) */ + public const IMG_TYPE_FMT = 328; /** String: Image type (jpeg) */ + public const IMG_WIDTH = 329; /** Int: Image width */ /** Orientation encoding: IMG_ORIENTATION */ - const IMG_ORI_VERTICAL = 1; - const IMG_ORI_HORIZONTAL = 2; - const IMG_ORI_SQUARE = 3; - const IMG_ORI_UNKNOWN = -1; + public const IMG_ORI_VERTICAL = 1; + public const IMG_ORI_HORIZONTAL = 2; + public const IMG_ORI_SQUARE = 3; + public const IMG_ORI_UNKNOWN = -1; /** Languages (non exchaustive) */ - const LANG_ALL = 'x-all'; /** Proxy constand including all languages */ - const LANG_DEFAULT = 'x-default'; /** Default language: English */ - const LANG_EN = 'en-us'; /** Language: English */ - const LANG_DE = 'de-de'; /** Language: German */ - const LANG_FR = 'fr-fr'; /** Language: French */ + public const LANG_ALL = 'x-all'; /** Proxy public constand including all languages */ + public const LANG_DEFAULT = 'x-default'; /** Default language: English */ + public const LANG_EN = 'en-us'; /** Language: English */ + public const LANG_DE = 'de-de'; /** Language: German */ + public const LANG_FR = 'fr-fr'; /** Language: French */ /** Private variables */ protected bool $data_read; /** Has data been loaded/read */ @@ -137,8 +137,7 @@ class Metadata { */ public function __construct() { - $this->data_read = false; $this->read_only = true; - $this->data = array(); + $this->data_read = false; $this->read_only = true; $this->data = []; $this->jpeg = new Metadata\Jpeg(); } @@ -160,9 +159,7 @@ public function __desctruct() public function read(string $filename, bool $extend = false, bool $read_only = false): void { // Re-initialize data - $this->data_read = false; - $this->data = array(); - $this->read_only = $read_only; + $this->data_read = false; $this->read_only = $read_only; $this->data = []; // Get file specific fields if(!file_exists($filename)) throw new Exception(_('File not found'), Exception::FILE_NOT_FOUND, $filename); @@ -303,9 +300,7 @@ private function setRW(int $field_id, string|int|float|array|false $field_value, if(is_array($field_value)) { // Replace all values $this->drop($field_id, false, $lang); - foreach($field_value as $field_subvalue) { - $this->data[$field_id][] = $field_subvalue; - } + foreach($field_value as $field_subvalue) $this->data[$field_id][] = $field_subvalue; } else { if(isset($this->data[$field_id])) { @@ -331,7 +326,7 @@ private function setRW(int $field_id, string|int|float|array|false $field_value, */ public function dropAll(string|false $lang = false): void { - if($lang === false) { $this->data = array(); return; } + if($lang === false) { $this->data = []; return; } foreach($this->data as $field_id => $field_value) { if(self::isLang($field_id) && isset($this->data[$field_id][$lang])) unset($this->data[$field_id][$lang]); if(!self::isLang($field_id) && isset($this->data[$field_id])) unset($this->data[$field_id]); @@ -438,9 +433,8 @@ private function setLang(int $field_id, string|int|float|array|false $field_valu // Add/update field value if(is_array($field_value)) { foreach($field_value as $lang_value => $data_value) { - if($lang === self::LANG_ALL || $lang_value === $lang) { + if($lang === self::LANG_ALL || $lang_value === $lang) $this->data[$field_id][$lang_value] = $data_value; - } } } else { @@ -517,12 +511,7 @@ private function dropLang(int $field_id, string|int|float|false $field_value, st */ public static function isLang(int $field_id): bool { - switch($field_id) { - case self::CAPTION: // XMP name - dc:description (Lang Alt) - return true; - default: - return false; - } + return match($field_id) { self::CAPTION => true, default => false }; } /** @@ -533,84 +522,84 @@ public static function isLang(int $field_id): bool */ public static function fieldType(int $field_id): int { - switch($field_id) { - case self::GENRE: - case self::KEYWORDS: - case self::ORG_CODE: - case self::ORG_NAME: - case self::PERSON: - case self::SCENES: - case self::SUBJECT_CODE: - case self::IMG_EXPOSURE: - return self::TYPE_ARY; - - case self::FILE_DATE: - case self::FILE_SIZE: - case self::CREATED_DATETIME: - case self::PRIORITY: - case self::RATING: - case self::IMG_FLASH: - case self::IMG_FOCAL_LENGTH: - case self::IMG_HEIGHT: - case self::IMG_ISO: - case self::IMG_ORIENTATION; - case self::IMG_RESOLUTION: - case self::IMG_RESOLUTION_UNIT: - case self::IMG_TYPE: - case self::IMG_WIDTH: - return self::TYPE_INT; - - case self::IMG_APERTURE: - return self::TYPE_FLOAT; + return match($field_id) { + self::GENRE, + self::KEYWORDS, + self::ORG_CODE, + self::ORG_NAME, + self::PERSON, + self::SCENES, + self::SUBJECT_CODE, + self::IMG_EXPOSURE + => self::TYPE_ARY, + + self::FILE_DATE, + self::FILE_SIZE, + self::CREATED_DATETIME, + self::PRIORITY, + self::RATING, + self::IMG_FLASH, + self::IMG_FOCAL_LENGTH, + self::IMG_HEIGHT, + self::IMG_ISO, + self::IMG_ORIENTATION, + self::IMG_RESOLUTION, + self::IMG_RESOLUTION_UNIT, + self::IMG_TYPE, + self::IMG_WIDTH + => self::TYPE_INT, + + self::IMG_APERTURE + => self::TYPE_FLOAT, - case self::FILE_NAME: - case self::FILE_EXT: - case self::AUTHOR: - case self::AUTHOR_TITLE: - case self::CAPTION: - case self::CAPTION_WRITER: - case self::CATEGORY: - case self::CITY: - case self::COPYRIGHT: - case self::COUNTRY: - case self::COUNTRY_CODE: - case self::CREDIT: - case self::EDIT_STATUS: - case self::EVENT: - case self::HEADLINE: - case self::INSTRUCTIONS: - case self::LOCATION: - case self::OBJECT: - case self::SOURCE: - case self::STATE: - case self::SUPP_CATEGORY_A: - case self::SUPP_CATEGORY_B: - case self::SUPP_CATEGORY_C: - case self::TRANSFER_REF: - case self::USAGE_TERMS: - case self::IMG_APERTURE_FMT: - case self::IMG_CAMERA_MAKE: - case self::IMG_CAMERA_MODEL: - case self::IMG_CAMERA_SERIAL; - case self::IMG_COLOR_SPACE_FMT: - case self::IMG_EXPOSURE_FMT: - case self::IMG_EXPOSURE_MODE_FMT: - case self::IMG_EXPOSURE_PGM_FMT: - case self::IMG_FLASH_FMT: - case self::IMG_FOCAL_LENGTH_FMT: - case self::IMG_LENS_MAKE: - case self::IMG_LENS_MODEL: - case self::IMG_LENS_SERIAL: - case self::IMG_METERING_MODE_FMT: - case self::IMG_RESOLUTION_FMT: - case self::IMG_SIZE_FMT: - case self::IMG_SOFTWARE: - case self::IMG_TYPE_FMT: - return self::TYPE_STR; + self::FILE_NAME, + self::FILE_EXT, + self::AUTHOR, + self::AUTHOR_TITLE, + self::CAPTION, + self::CAPTION_WRITER, + self::CATEGORY, + self::CITY, + self::COPYRIGHT, + self::COUNTRY, + self::COUNTRY_CODE, + self::CREDIT, + self::EDIT_STATUS, + self::EVENT, + self::HEADLINE, + self::INSTRUCTIONS, + self::LOCATION, + self::OBJECT, + self::SOURCE, + self::STATE, + self::SUPP_CATEGORY_A, + self::SUPP_CATEGORY_B, + self::SUPP_CATEGORY_C, + self::TRANSFER_REF, + self::USAGE_TERMS, + self::IMG_APERTURE_FMT, + self::IMG_CAMERA_MAKE, + self::IMG_CAMERA_MODEL, + self::IMG_CAMERA_SERIAL, + self::IMG_COLOR_SPACE_FMT, + self::IMG_EXPOSURE_FMT, + self::IMG_EXPOSURE_MODE_FMT, + self::IMG_EXPOSURE_PGM_FMT, + self::IMG_FLASH_FMT, + self::IMG_FOCAL_LENGTH_FMT, + self::IMG_LENS_MAKE, + self::IMG_LENS_MODEL, + self::IMG_LENS_SERIAL, + self::IMG_METERING_MODE_FMT, + self::IMG_RESOLUTION_FMT, + self::IMG_SIZE_FMT, + self::IMG_SOFTWARE, + self::IMG_TYPE_FMT + => self::TYPE_STR, - default: - return self::TYPE_INVALID; - } + default + => self::TYPE_INVALID + }; } /** @@ -622,18 +611,13 @@ public static function fieldType(int $field_id): int */ public static function isValidFieldType(int $field_id, string|int|float|array|false $field_value): bool { - switch(self::fieldType($field_id)) { - case self::TYPE_STR: - return gettype($field_value) === 'string'; - case self::TYPE_INT: - return gettype($field_value) === 'integer'; - case self::TYPE_FLOAT: - return gettype($field_value) === 'double' || gettype($field_value) === 'integer'; - case self::TYPE_ARY: - return gettype($field_value) === 'array'; - default: - return false; - } + return match(self::fieldType($field_id)) { + self::TYPE_STR => gettype($field_value) === 'string', + self::TYPE_INT => gettype($field_value) === 'integer', + self::TYPE_FLOAT => gettype($field_value) === 'double' || gettype($field_value) === 'integer', + self::TYPE_ARY => gettype($field_value) === 'array', + default => false + }; } /** @@ -646,7 +630,7 @@ public static function isValidFieldType(int $field_id, string|int|float|array|fa public function stringToArray(string|false $input): array|false { if($input === false || empty($input)) return false; - $output_ary = array(); + $output_ary = []; $substr_ary = explode(',', $input); foreach($substr_ary as $substr) { $substr = trim($substr); @@ -667,7 +651,7 @@ public function arrayToString(array|false $input_ary): string|false $output = ''; foreach($input_ary as $field_value) { if(!empty($output)) $output .= ', '; - $output .= $field_value; + $output .= trim($field_value); } return $output; } @@ -687,18 +671,21 @@ private function extendFields(): void { $kwd_ary = $this->get(self::KEYWORDS); foreach($kwd_ary as $kwd) { - if(strpos($kwd, ':') !== false) { - list($field_name, $field_value) = explode(':', $kwd, 2); + if(str_contains($kwd, ':')) { + [$field_name, $field_value] = explode(':', $kwd, 2); switch(trim(strtolower($field_name))) { case 'event': + case 'evt': $this->drop(self::KEYWORDS, $kwd); $this->set(self::EVENT, $field_value); break; case 'scence': + case 'sce': $this->drop(self::KEYWORDS, $kwd); $this->set(self::SCENES, $field_value); break; case 'genre': + case 'gnr': $this->drop(self::KEYWORDS, $kwd); $this->set(self::GENRE, $field_value); break; @@ -707,18 +694,16 @@ private function extendFields(): void $this->drop(self::KEYWORDS, $kwd); $this->set(self::PERSON, $field_value); break; - case 'org': case 'organization': + case 'org': $this->drop(self::KEYWORDS, $kwd); $this->set(self::ORG, $field_value); break; - case 'nat': case 'nationality': + case 'nat': $this->drop(self::KEYWORDS, $kwd); $this->set(self::NAT, $field_value); break; - default: - break; } } } @@ -804,7 +789,7 @@ private function importIptc(): void */ private function exportIptc(): void { - $iptc_data = array(); + $iptc_data = []; if($this->isset(self::AUTHOR)) $iptc_data[Iptc::AUTHOR][0] = $this->get(self::AUTHOR); if($this->isset(self::AUTHOR_TITLE)) $iptc_data[Iptc::AUTHOR_TITLE][0] = $this->get(self::AUTHOR_TITLE); if($this->isset(self::CAPTION, self::LANG_DEFAULT)) @@ -967,17 +952,18 @@ protected function importXmp(): void if(!$this->isset(self::IMG_COLOR_SPACE_FMT) && $xmp_data->isXmpText(Xmp::COLOR_SPACE)) $this->setRW(self::IMG_COLOR_SPACE_FMT, $xmp_data->getXmpText(Xmp::COLOR_SPACE)); if($xmp_data->isXmpText(Xmp::COLOR_MODE) && $this->isset(self::IMG_COLOR_SPACE_FMT)) { - switch($xmp_data->getXmpText(Xmp::COLOR_MODE)) { - case 0: $color_mode = 'Bitmap'; break; - case 1: $color_mode = 'Grayscale'; break; - case 2: $color_mode = 'Indexed'; break; - case 3: $color_mode = 'RGB'; break; - case 4: $color_mode = 'CMYK'; break; - case 7: $color_mode = 'Multichannel'; break; - case 8: $color_mode = 'Duotone'; break; - case 9: $color_mode = 'Lab'; break; - } - if(isset($color_mode)) + $color_mode = match((int)$xmp_data->getXmpText(Xmp::COLOR_MODE)) { + 0 => _('Bitmap'), + 1 => _('Grayscale'), + 2 => _('Indexed'), + 3 => _('RGB'), + 4 => _('CMYK'), + 7 => _('Multichannel'), + 8 => _('Duotone'), + 9 => _('Lab'), + default => _('Unknown') + }; + if($color_mode !== _('Unknown')) $this->setRW(self::IMG_COLOR_SPACE_FMT, $this->get(self::IMG_COLOR_SPACE_FMT).' / '.$color_mode); } } @@ -1021,7 +1007,7 @@ protected function exportXmp(): void if($xmp_data->isXmpText(Xmp::PS_STATE)) $xmp_data->setXmpText(Xmp::PS_STATE, $this->get(self::STATE)); $xmp_data->setXmpBag(Xmp::SUBJECT_CODE, $this->get(self::SUBJECT_CODE)); - $supp_cat = array(); + $supp_cat = []; if($this->isset(self::SUPP_CATEGORY_A)) $supp_cat[0] = $this->get(self::SUPP_CATEGORY_A); if(!$this->isset(self::SUPP_CATEGORY_A) && $this->isset(self::SUPP_CATEGORY_B)) $supp_cat[0] = ''; if($this->isset(self::SUPP_CATEGORY_B)) $supp_cat[1] = $this->get(self::SUPP_CATEGORY_B); @@ -1059,8 +1045,8 @@ protected function exportXmp(): void */ protected static function calcFrac(string $data): float|false { - if(strpos($data, '/') === 0) return false; - list($nom, $denom) = explode('/', $data, 2); + if(!str_contains($data, '/')) return false; + [$nom, $denom] = explode('/', $data, 2); return (float)((int)$nom / (int)$denom); } @@ -1073,8 +1059,8 @@ protected static function calcFrac(string $data): float|false */ protected static function nrmFrac(string $frac): string { - if(strpos($frac, '/') === false) return $frac; - list($num, $denom) = explode('/', $frac); + if(!str_contains($frac, '/')) return $frac; + [$num, $denom] = explode('/', $frac); $num = (int)trim($num); $denom = (int)trim($denom); $new_denom = $denom / $num; return '1/'.round($new_denom); @@ -1089,10 +1075,10 @@ protected static function nrmFrac(string $frac): string */ protected static function fracToArray(string $frac): array { - if(strpos($frac, '/') === false) + if(!str_contains($frac, '/')) throw new Exception(_('Invalid fraction specified'), Exception::DATA_FORMAT_ERROR, $frac); - list($num, $denom) = explode('/', $frac); - return array($num, $denom); + [$num, $denom] = explode('/', $frac); + return [$num, $denom]; } /** @@ -1122,19 +1108,15 @@ protected function importExif(): void $this->setRW(self::IMG_CAMERA_SERIAL, $exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_CAMERA_SERIAL_NUMBER)]); if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_COLOR_SPACE)]) && !$this->isset(self::IMG_COLOR_SPACE_FMT)) { - switch((int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_COLOR_SPACE)]) { - case 0x1: - $this->setRW(self::IMG_COLOR_SPACE_FMT, _('sRGB')); break; - case 0x2: - $this->setRW(self::IMG_COLOR_SPACE_FMT, _('Adobe RGB')); break; - case 0xfffd: - $this->setRW(self::IMG_COLOR_SPACE_FMT, _('Wide Gamut RGB')); break; - case 0xfffe: - $this->setRW(self::IMG_COLOR_SPACE_FMT, _('ICC Profile')); break; - case 0xffff: - $this->setRW(self::IMG_COLOR_SPACE_FMT, _('Uncalibrated')); break; - - } + $this->setRW(self::IMG_COLOR_SPACE_FMT, + match((int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_COLOR_SPACE)]) { + 0x1 => _('sRGB'), + 0x2 => _('Adobe RGB'), + 0xfffd => _('Wide Gamut RGB'), + 0xfffe => _('ICC Profile'), + 0xffff => _('Uncalibrated'), + default => _('Unknown') + }); } if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_TIME)])) { $exposure = $exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_TIME)]; @@ -1144,39 +1126,30 @@ protected function importExif(): void } if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_MODE)]) && !$this->isset(self::IMG_EXPOSURE_MODE_FMT)) { - switch($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_MODE)]) { - case 0: - $this->setRW(self::IMG_EXPOSURE_MODE_FMT, _('Auto')); break; - case 1: - $this->setRW(self::IMG_EXPOSURE_MODE_FMT, _('Manual')); break; - case 2: - $this->setRW(self::IMG_EXPOSURE_MODE_FMT, _('Auto bracket')); break; - } + $this->setRW(self::IMG_EXPOSURE_MODE_FMT, + match((int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_MODE)]) { + 0 => _('Auto'), + 1 => _('Manual'), + 2 => _('Auto bracket'), + default => _('Unknown') + }); } if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_PROGRAM)]) && !$this->isset(self::IMG_EXPOSURE_PGM_FMT)) { - switch($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_PROGRAM)]) { - case 0: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Not defined')); break; - case 1: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Manual')); break; - case 2: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Program')); break; - case 3: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Aperture-priority')); break; - case 4: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Shutter speed priority')); break; - case 5: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Creative (slow speed)')); break; - case 6: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Action (high speed)')); break; - case 7: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Portrait')); break; - case 8: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Landscape')); break; - case 9: - $this->setRW(self::IMG_EXPOSURE_PGM_FMT, _('Bulb')); break; - } + $this->setRW(self::IMG_EXPOSURE_PGM_FMT, + match((int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXPOSURE_PROGRAM)]) { + 0 => _('Not defined'), + 1 => _('Manual'), + 2 => _('Program'), + 3 => _('Aperture-priority'), + 4 => _('Shutter speed priority'), + 5 => _('Creative (slow speed)'), + 6 => _('Action (high speed)'), + 7 => _('Portrait'), + 8 => _('Landscape'), + 9 => _('Bulb'), + default => _('Unknown') + }); } if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_FLASH)])) { $flash =$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_FLASH)]; @@ -1203,8 +1176,7 @@ protected function importExif(): void $this->setRW(self::IMG_ISO, (int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_ISO_SPEED)]); if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_PHOTO_SENSITIVITY)]) && !$this->isset(self::IMG_ISO)) { // Note: SensitivtyType should be 2-7 or 0 - $photo_sensitivity = (int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_PHOTO_SENSITIVITY)]; - $this->setRW(self::IMG_ISO, $photo_sensitivity); + $this->setRW(self::IMG_ISO, (int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_PHOTO_SENSITIVITY)]); } if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_LENS_MAKE)]) && !$this->isset(self::IMG_LENS_MAKE)) @@ -1217,24 +1189,16 @@ protected function importExif(): void $this->setRW(self::IMG_LENS_SERIAL, $exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_LENS_SERIAL_NUMBER)]); if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_METERING_MODE)]) && !$this->isset(self::IMG_METERING_MODE_FMT)) { - switch($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_METERING_MODE)]) { - case 1: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Average')); break; - case 2: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Center-weighted average')); break; - case 3: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Spot')); break; - case 4: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Multi-spot')); break; - case 5: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Multi-segment')); break; - case 6: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Partial')); break; - case 255: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Other')); break; - default: - $this->setRW(self::IMG_METERING_MODE_FMT, _('Unknown')); break; - } + $this->setRW(self::IMG_METERING_MODE_FMT, + match($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_METERING_MODE)]) { + 1 => _('Average'), + 2 => _('Center-weighted average'), + 3 => _('Spot'), + 4 => _('Multi-spot'), + 5 => _('Multi-segment'), + 6 => _('Partial'), + 255 => _('Other'), + default => _('Unknown')}); } if(!$this->isset(self::IMG_RESOLUTION_UNIT)) { if(isset($exif_data[Exif::tag(Exif::IFD_IFD0, Exif::TAG_IFD0_RESOLUTION_UNIT)])) { @@ -1248,13 +1212,7 @@ protected function importExif(): void else { $resolution_unit = $this->get(self::IMG_RESOLUTION_UNIT); } - switch($resolution_unit) { - case 1: - case 3: - $resolution_per_cm = 1.0; break; - default: - $resolution_per_cm = 2.54; break; - } + $resolution_per_cm = match($resolution_unit) { 1, 3 => 1.0, default => 2.54 }; // Note: We give preference to XResolution ofer YResolution if(isset($exif_data[Exif::tag(Exif::IFD_IFD0, Exif::TAG_IFD0_XRESOLUTION)]) && !$this->isset(self::IMG_RESOLUTION)) { @@ -1277,7 +1235,7 @@ protected function importExif(): void if(!$this->isset(self::IMG_WIDTH)) { if(isset($exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXIF_IMAGE_WIDTH)])) { $width = (int)$exif_data[Exif::tag(Exif::IFD_EXIF, Exif::TAG_EXIF_EXIF_IMAGE_WIDTH)]; - $this->setRW(self::IMG_WIDTH, $height); + $this->setRW(self::IMG_WIDTH, $width); } } else { @@ -1300,9 +1258,9 @@ protected function importExif(): void if($this->isset(self::FILE_SIZE)) { $file_size = $this->get(self::FILE_SIZE); if($file_size > 1024 * 1024) - $size_fmt .= ' ('.number_format($file_size / 1024 / 1024, 0).' MB)'; + $size_fmt .= ' ('.number_format($file_size / 1024 / 1024, 1).' MB)'; elseif($file_size > 1024) - $size_fmt .= ' ('.number_format($file_size / 1024, 0).' KB)'; + $size_fmt .= ' ('.number_format($file_size / 1024, 1).' KB)'; else $size_fmt .= ' ('.number_format($file_size / 1024, 2).' KB)'; } @@ -1328,7 +1286,7 @@ protected function importExif(): void */ protected function exportExif(): void { - $exif_ary = array(); + $exif_ary = []; if($this->isset(self::CAPTION)) $exif_ary[Exif::tag(Exif::IFD_IFD0, Exif::TAG_IFD0_IMAGE_DESCRIPTION)] = $this->get(self::CAPTION); if($this->isset(self::COPYRIGHT)) diff --git a/src/Metadata/Exception.php b/src/Metadata/Exception.php index 7b42120..2372539 100644 --- a/src/Metadata/Exception.php +++ b/src/Metadata/Exception.php @@ -3,7 +3,7 @@ * Exception.php - Metadata error handling * * @package Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -15,26 +15,23 @@ class Exception extends \Exception { /** Error costants */ - const INTERNAL_ERROR = 0; - const NOT_IMPLEMENTED = 1; + public const INTERNAL_ERROR = 0; + public const NOT_IMPLEMENTED = 1; /** - File specific errors */ - const FILE_ERROR = 10; - const FILE_NOT_FOUND = 11; - const FILE_TYPE_ERROR = 12; - const FILE_CORRUPT = 13; - const FILE_COPY_ERROR = 14; + public const FILE_ERROR = 10; + public const FILE_NOT_FOUND = 11; + public const FILE_TYPE_ERROR = 12; + public const FILE_CORRUPT = 13; + public const FILE_COPY_ERROR = 14; /** - Data specitic errors */ - const DATA_NOT_FOUND = 21; - const DATA_FORMAT_ERROR = 22; - const INVALID_FIELD_ID = 23; - const INVALID_FIELD_WRITE = 24; - const INVALID_FIELD_DATA = 25; - const INVALID_LANG = 26; - - /** Internal variables */ - protected mixed $data; + public const DATA_NOT_FOUND = 21; + public const DATA_FORMAT_ERROR = 22; + public const INVALID_FIELD_ID = 23; + public const INVALID_FIELD_WRITE = 24; + public const INVALID_FIELD_DATA = 25; + public const INVALID_LANG = 26; /** * Constructor @@ -44,9 +41,9 @@ class Exception extends \Exception { * @param mixed $data Exception specific data * @param \Throwable $previous Previously thrown exception */ - public function __construct(string $message = '', int $code = 0, mixed $data = null, ?\Throwable $previous = null) + public function __construct(string $message = '', int $code = 0, protected mixed $data = null, + ?\Throwable $previous = null) { - $this->data = $data; parent::__construct($message, $code, $previous); } diff --git a/src/Metadata/Exif.php b/src/Metadata/Exif.php index e0c5783..0bc1981 100644 --- a/src/Metadata/Exif.php +++ b/src/Metadata/Exif.php @@ -3,7 +3,7 @@ * Exif.php - Decode EXIF data from JPG segment APP1 * * @package Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -27,57 +27,57 @@ class Exif { /** Tag names */ - const IFD_ROOT = 'IFD'; /** IFD0/IFD1: Root IFD bock(s) */ - const IFD_IFD0 = 'IFD0'; - const IFD_EXIF = 'EXIF'; + public const IFD_ROOT = 'IFD'; /** IFD0/IFD1: Root IFD bock(s) */ + public const IFD_IFD0 = 'IFD0'; + public const IFD_EXIF = 'EXIF'; // - IFD Pointers - const TAG_PTR_SUB_IFD = 0x014a; - const TAG_PTR_GLOBAL_PARAMETERS_IFD = 0x0190; - const TAG_PTR_KODAK_IFD = 0x8290; - const TAG_PTR_JPL_CARTO_IFD = 0x85d7; - const TAG_PTR_EXIF_IFD = 0x8769; - const TAG_PTR_LEAF_SUB_IFD = 0x888a; - const TAG_PTR_KDC_IFD = 0xfe00; + public const TAG_PTR_SUB_IFD = 0x014a; + public const TAG_PTR_GLOBAL_PARAMETERS_IFD = 0x0190; + public const TAG_PTR_KODAK_IFD = 0x8290; + public const TAG_PTR_JPL_CARTO_IFD = 0x85d7; + public const TAG_PTR_EXIF_IFD = 0x8769; + public const TAG_PTR_LEAF_SUB_IFD = 0x888a; + public const TAG_PTR_KDC_IFD = 0xfe00; // - IFD0 (image) - const TAG_IFD0_PROCESSING_SOFTWARE = 0x000b; - const TAG_IFD0_IMAGE_DESCRIPTION = 0x010e; - const TAG_IFD0_CAMERA_MAKE = 0x010f; - const TAG_IFD0_CAMERA_MODEL = 0x0110; - const TAG_IFD0_ORIENTATION = 0x0112; - const TAG_IFD0_XRESOLUTION = 0x011a; - const TAG_IFD0_YRESOLUTION = 0x011b; - const TAG_IFD0_RESOLUTION_UNIT = 0x0128; - const TAG_IFD0_SOFTWARE = 0x0131; - const TAG_IFD0_ARTIST = 0x013b; - const TAG_IFD0_COPYRIGHT = 0x8298; + public const TAG_IFD0_PROCESSING_SOFTWARE = 0x000b; + public const TAG_IFD0_IMAGE_DESCRIPTION = 0x010e; + public const TAG_IFD0_CAMERA_MAKE = 0x010f; + public const TAG_IFD0_CAMERA_MODEL = 0x0110; + public const TAG_IFD0_ORIENTATION = 0x0112; + public const TAG_IFD0_XRESOLUTION = 0x011a; + public const TAG_IFD0_YRESOLUTION = 0x011b; + public const TAG_IFD0_RESOLUTION_UNIT = 0x0128; + public const TAG_IFD0_SOFTWARE = 0x0131; + public const TAG_IFD0_ARTIST = 0x013b; + public const TAG_IFD0_COPYRIGHT = 0x8298; // - ExifIFD - const TAG_EXIF_EXPOSURE_TIME = 0x829a; - const TAG_EXIF_FNUMBER = 0x829d; - const TAG_EXIF_EXPOSURE_PROGRAM = 0x8822; - const TAG_EXIF_ISO_SPEED = 0x8833; - const TAG_EXIF_PHOTO_SENSITIVITY = 0x8827; - const TAX_EXIF_SENSITIVITY_TYPE = 0x8830; - const TAG_EXIF_DATE_TIME_ORIGINAL = 0x9003; - const TAG_EXIF_CREATE_DATE = 0x9004; - const TAG_EXIF_APERTURE_VALUE = 0x9202; - const TAG_EXIF_METERING_MODE = 0x9207; - const TAG_EXIF_FLASH = 0x9209; - const TAG_EXIF_FOCAL_LENGTH = 0x920a; - const TAG_EXIF_COLOR_SPACE = 0xa001; - const TAG_EXIF_EXIF_IMAGE_WIDTH = 0xa002; - const TAG_EXIF_EXIF_IMAGE_HEIGHT = 0xa003; - const TAG_EXIF_EXPOSURE_MODE = 0xa402; - const TAG_EXIF_OWNER_NAME = 0xa430; - const TAG_EXIF_CAMERA_SERIAL_NUMBER = 0xa431; - const TAG_EXIF_LENS_INFO = 0xa432; - const TAG_EXIF_LENS_MAKE = 0xa433; - const TAG_EXIF_LENS_MODEL = 0xa434; - const TAG_EXIF_LENS_SERIAL_NUMBER = 0xa435; - const TAG_EXIF_LENS = 0xfdea; - const TAG_EXIF_SENSIVITY_TYPE = 0x8830; + public const TAG_EXIF_EXPOSURE_TIME = 0x829a; + public const TAG_EXIF_FNUMBER = 0x829d; + public const TAG_EXIF_EXPOSURE_PROGRAM = 0x8822; + public const TAG_EXIF_ISO_SPEED = 0x8833; + public const TAG_EXIF_PHOTO_SENSITIVITY = 0x8827; + public const TAX_EXIF_SENSITIVITY_TYPE = 0x8830; + public const TAG_EXIF_DATE_TIME_ORIGINAL = 0x9003; + public const TAG_EXIF_CREATE_DATE = 0x9004; + public const TAG_EXIF_APERTURE_VALUE = 0x9202; + public const TAG_EXIF_METERING_MODE = 0x9207; + public const TAG_EXIF_FLASH = 0x9209; + public const TAG_EXIF_FOCAL_LENGTH = 0x920a; + public const TAG_EXIF_COLOR_SPACE = 0xa001; + public const TAG_EXIF_EXIF_IMAGE_WIDTH = 0xa002; + public const TAG_EXIF_EXIF_IMAGE_HEIGHT = 0xa003; + public const TAG_EXIF_EXPOSURE_MODE = 0xa402; + public const TAG_EXIF_OWNER_NAME = 0xa430; + public const TAG_EXIF_CAMERA_SERIAL_NUMBER = 0xa431; + public const TAG_EXIF_LENS_INFO = 0xa432; + public const TAG_EXIF_LENS_MAKE = 0xa433; + public const TAG_EXIF_LENS_MODEL = 0xa434; + public const TAG_EXIF_LENS_SERIAL_NUMBER = 0xa435; + public const TAG_EXIF_LENS = 0xfdea; + public const TAG_EXIF_SENSIVITY_TYPE = 0x8830; /** Data types */ private const TYPE_UBYTE = 1; @@ -98,7 +98,7 @@ class Exif { private const EXIF_BYTE_ALIGN_BE = 1; /** MM: Motorola - Big endian */ /** TIFF identifiers */ - const EXIF_TYPE = 'APP1'; + public const EXIF_TYPE = 'APP1'; private const TIFF_ID = 42; /** @@ -112,7 +112,7 @@ public static function decode(array $segments): array|false { if(empty($segments)) return false; - $exif_data_ary = array(); $any_data = false; + $exif_data_ary = []; $any_data = false; foreach($segments as $segment_id => $segment) { $exif_data_ary[$segment_id] = self::decodeSegment($segment); $exif_data_ary[$segment_id] = empty($exif_data_ary[$segment_id]) ? false : $exif_data_ary[$segment_id]; @@ -152,10 +152,10 @@ public static function encode(array $segments, array $exif_data_ary, array|false } } // Overwrite existing data with \x00 if set - $data_ary = array(array('block' => 'IFD0', 'tag' => self::TAG_IFD0_IMAGE_DESCRIPTION), - array('block' => 'IFD0', 'tag' => self::TAG_IFD0_COPYRIGHT), - array('block' => 'EXIF', 'tag' => self::TAG_EXIF_OWNER_NAME), - array('block' => 'IFD0', 'tag' => self::TAG_IFD0_ARTIST)); + $data_ary = [['block' => 'IFD0', 'tag' => self::TAG_IFD0_IMAGE_DESCRIPTION], + ['block' => 'IFD0', 'tag' => self::TAG_IFD0_COPYRIGHT], + ['block' => 'EXIF', 'tag' => self::TAG_EXIF_OWNER_NAME], + ['block' => 'IFD0', 'tag' => self::TAG_IFD0_ARTIST]]; foreach($exif_data_ary as $seg_id => $seg_data) { foreach($seg_data as $elt) { foreach($data_ary as $data) { @@ -192,14 +192,11 @@ public static function tag(string $ifd, int $tag): string */ private static function byteAlign(string $byte_align_str): int { - switch($byte_align_str) { - case 'II': - return self::EXIF_BYTE_ALIGN_LE; - case 'MM': - return self::EXIF_BYTE_ALIGN_BE; - default: - throw new Exception(_('Invalid byte alignment read'), Exception::DATA_FORMAT_ERROR, $byte_align_str); - } + return match($byte_align_str) { + 'II' => self::EXIF_BYTE_ALIGN_LE, + 'MM' => self::EXIF_BYTE_ALIGN_BE, + default => throw new Exception(_('Invalid byte alignment read'), Exception::DATA_FORMAT_ERROR, $byte_align_str) + }; } /** @@ -247,7 +244,7 @@ private static function decodeSegment(string $segment): array private static function decodeIFDBlock(string $segment, string $ifd_block_name, int $ifd_block_pos, int $byte_align): array { - $exif_ary = array(); + $exif_ary = []; $ifd_block_id = 0; do { $block_name = $ifd_block_name === self::IFD_ROOT ? $ifd_block_name.$ifd_block_id : $ifd_block_name; @@ -294,13 +291,13 @@ private static function decodeIFDBlock(string $segment, string $ifd_block_name, // Save data if($ifd_tag_nb * self::getIFDTypeSize($ifd_tag_type) > 4) { - $exif_ary[] = array('block' => $block_name, 'tag' => $ifd_tag_id, 'data' => $ifd_tag_str, - 'ptr' => $ifd_tag_data, 'size' => $ifd_tag_nb * self::getIFDTypeSize($ifd_tag_type)); + $exif_ary[] = ['block' => $block_name, 'tag' => $ifd_tag_id, 'data' => $ifd_tag_str, + 'ptr' => $ifd_tag_data, 'size' => $ifd_tag_nb * self::getIFDTypeSize($ifd_tag_type)]; } else { - $exif_ary[] = array('block' => $block_name, 'tag' => $ifd_tag_id, 'data' => $ifd_tag_str, - 'ptr' => $ifd_block_pos + 12 * $tag, - 'size' => $ifd_tag_nb * self::getIFDTypeSize($ifd_tag_type)); + $exif_ary[] = ['block' => $block_name, 'tag' => $ifd_tag_id, 'data' => $ifd_tag_str, + 'ptr' => $ifd_block_pos + 12 * $tag, + 'size' => $ifd_tag_nb * self::getIFDTypeSize($ifd_tag_type)]; } } @@ -322,26 +319,14 @@ private static function decodeIFDBlock(string $segment, string $ifd_block_name, */ private static function getIFDTypeSize(int $type): int { - switch($type) { - case self::TYPE_UBYTE: - case self::TYPE_SBYTE: - case self::TYPE_ASCII: - case self::TYPE_UNDEFINED: - return 1; - case self::TYPE_USHORT: - case self::TYPE_SSHORT: - return 2; - case self::TYPE_ULONG: - case self::TYPE_SLONG: - case self::TYPE_FLOAT: - return 4; - case self::TYPE_URAT: - case self::TYPE_SRAT: - case self::TYPE_DOUBLE: - return 8; - default: - throw new Exception(_('Cannot calculate data size of invalid data type'), Exception::DATA_FORMAT_ERROR, $type); - } + return match($type) { + self::TYPE_UBYTE, self::TYPE_SBYTE, self::TYPE_ASCII, self::TYPE_UNDEFINED => 1, + self::TYPE_USHORT, self::TYPE_SSHORT => 2, + self::TYPE_ULONG, self::TYPE_SLONG, self::TYPE_FLOAT => 4, + self::TYPE_URAT, self::TYPE_SRAT, self::TYPE_DOUBLE => 8, + default => throw new Exception(_('Cannot calculate data size of invalid data type'), + Exception::DATA_FORMAT_ERROR, $type) + }; } /** @@ -386,11 +371,11 @@ protected static function decodeIFDData(string $data, int $type, int $byte_align return $value > 2147483648 ? $value - 4294967296 : $value; case self::TYPE_URAT: - return array('num' => self::decodeIFDData(substr($data, 0, 4), self::TYPE_ULONG, $byte_align), - 'denom' => self::decodeIFDData(substr($data, 4, 4), self::TYPE_ULONG, $byte_align)); + return ['num' => self::decodeIFDData(substr($data, 0, 4), self::TYPE_ULONG, $byte_align), + 'denom' => self::decodeIFDData(substr($data, 4, 4), self::TYPE_ULONG, $byte_align)]; case self::TYPE_SRAT: - $value = array('num' => self::decodeIFDData(substr($data, 0, 4), self::TYPE_ULONG, $byte_align), - 'denom' => self::decodeIFDData(substr($data, 4, 4), self::TYPE_ULONG, $byte_align)); + $value = ['num' => self::decodeIFDData(substr($data, 0, 4), self::TYPE_ULONG, $byte_align), + 'denom' => self::decodeIFDData(substr($data, 4, 4), self::TYPE_ULONG, $byte_align)]; if($value['num'] > 2147483648) $value['num'] -= 4294967296; if($value['denom'] > 2147483648) $value['denom'] -= 4294967296; return $value; @@ -418,26 +403,16 @@ protected static function decodeIFDData(string $data, int $type, int $byte_align */ protected static function getIFDString(string|int|array $data, int $type): string { - switch($type) { - case self::TYPE_UBYTE: - case self::TYPE_SBYTE: - case self::TYPE_USHORT: - case self::TYPE_SSHORT: - case self::TYPE_ULONG: - case self::TYPE_SLONG: - return (string)$data; - case self::TYPE_ASCII: - return trim($data); - case self::TYPE_URAT: - case self::TYPE_SRAT: - return isset($data['num']) && isset($data['denom']) ? $data['num'].'/'.$data['denom'] : 'N/A'; - case self::TYPE_UNDEFINED: - case self::TYPE_FLOAT: - case self::TYPE_DOUBLE: - return strlen($data).' '._('bytes of binary data').': '.self::bin2hex($data); - default: - throw new Exception(_('Invalid IFD data type found'), Exception::DATA_FORMAT_ERROR); - } + return match($type) { + self::TYPE_UBYTE, self::TYPE_SBYTE, self::TYPE_USHORT, self::TYPE_SSHORT, self::TYPE_ULONG, self::TYPE_SLONG + => (string)$data, + self::TYPE_ASCII => trim($data), + self::TYPE_URAT, self::TYPE_SRAT + => isset($data['num']) && isset($data['denom']) ? $data['num'].'/'.$data['denom'] : 'N/A', + self::TYPE_UNDEFINED, self::TYPE_FLOAT, self::TYPE_DOUBLE + => strlen($data).' '._('bytes of binary data').': '.self::bin2hex($data), + default => throw new Exception(_('Invalid IFD data type found'), Exception::DATA_FORMAT_ERROR) + }; } /** @@ -449,17 +424,16 @@ protected static function getIFDString(string|int|array $data, int $type): strin */ protected static function blockName(int $tag): string|false { - switch($tag) { - case self::TAG_PTR_SUB_IFD: return 'SUB'; - case self::TAG_PTR_GLOBAL_PARAMETERS_IFD: return 'GLOBAL'; - case self::TAG_PTR_KODAK_IFD: return 'KODAK'; - case self::TAG_PTR_JPL_CARTO_IFD: return 'JPL'; - case self::TAG_PTR_EXIF_IFD: return 'EXIF'; - case self::TAG_PTR_LEAF_SUB_IFD: return 'LEAF'; - case self::TAG_PTR_KDC_IFD: return 'KDC'; - default: - return false; - } + return match($tag) { + self::TAG_PTR_SUB_IFD => 'SUB', + self::TAG_PTR_GLOBAL_PARAMETERS_IFD => 'GLOBAL', + self::TAG_PTR_KODAK_IFD => 'KODAK', + self::TAG_PTR_JPL_CARTO_IFD => 'JPL', + self::TAG_PTR_EXIF_IFD => 'EXIF', + self::TAG_PTR_LEAF_SUB_IFD => 'LEAF', + self::TAG_PTR_KDC_IFD => 'KDC', + default => false + }; } /** @@ -473,9 +447,7 @@ protected static function bin2hex(string|false $bin): string { if($bin === false) return '** false **'; $str = ''; - for($pos = 0; $pos < strlen($bin); $pos ++) { - $str .= bin2hex($bin[$pos]).' '; - } + for($pos = 0; $pos < strlen($bin); $pos ++) $str .= bin2hex($bin[$pos]).' '; return trim($str); } diff --git a/src/Metadata/Iptc.php b/src/Metadata/Iptc.php index 2755145..1f87517 100644 --- a/src/Metadata/Iptc.php +++ b/src/Metadata/Iptc.php @@ -3,7 +3,7 @@ * Iptc.php - Encoding and decode IPTC data from segment APP13 * * @package Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -22,37 +22,37 @@ class Iptc { /** IPTC application record tags: IPTC Core Metadata 1.3 / most relevant ones */ - const DATA_ENCODING = '1:090'; /** Read only: Coded Character Set - Max 32 characters */ + public const DATA_ENCODING = '1:090'; /** Read only: Coded Character Set - Max 32 characters */ - const AUTHOR = '2:080'; /** By-Line (Author) - Max 32 Characters */ - const AUTHOR_TITLE = '2:085'; /** By-Line Title (Author Position) - Max 32 characters */ - const CAPTION = '2:120'; /** Caption/Abstract - Max 2000 Characters */ - const CAPTION_WRITER = '2:122'; /** Caption Writer/Editor - Max 32 Characters */ - const CATEGORY = '2:015'; /** Category - Max 3 characters */ - const CITY = '2:090'; /** City - Max 32 Characters */ - const COPYRIGHT = '2:116'; /** Copyright Notice - Max 128 Characters */ - const COUNTRY = '2:101'; /** Country/Primary Location Name - Max 64 characters */ - const COUNTRY_CODE = '2:100'; /** Country/Primary Location Code - 3 alphabetic characters */ - const CREATED_DATE = '2:055'; /** Read only: Date Created - 8 numeric characters CCYYMMDD */ - const CREATED_TIME = '2:060'; /** Read only: Time Created - 11 characters HHMMSS±HHMM */ - const CREDIT = '2:110'; /** Credit - Max 32 Characters */ - const EDIT_STATUS = '2:007'; /** Edit Status - Max 64 characters */ - const GENRE = '2:004'; /** Genres - Max 64 Characters */ - const HEADLINE = '2:105'; /** Headline - Max 256 Characters */ - const INSTRUCTIONS = '2:040'; /** Special Instructions - Max 256 Characters */ - const KEYWORDS = '2:025'; /** Keywords - Max 64 characters */ - const LOCATION = '2:092'; /** Sub-Location - Max 32 characters */ - const OBJECT = '2:005'; /** Object Name (Title) - Max 64 characters */ - const PRIORITY = '2:010'; /** Urgency - 1 numeric character */ - const SOURCE = '2:115'; /** Source - Max 32 Characters */ - const STATE = '2:095'; /** Province/State - Max 32 Characters */ - const SUBJECT_CODE = '2:012'; /** Subject Reference - 13 to 236 characters */ - const SUPP_CATEGORY = '2:020'; /** Supplemental Category - Max 32 characters */ - const TRANSFER_REF = '2:103'; /** Original Transmission Reference - Max 32 characters */ + public const AUTHOR = '2:080'; /** By-Line (Author) - Max 32 Characters */ + public const AUTHOR_TITLE = '2:085'; /** By-Line Title (Author Position) - Max 32 characters */ + public const CAPTION = '2:120'; /** Caption/Abstract - Max 2000 Characters */ + public const CAPTION_WRITER = '2:122'; /** Caption Writer/Editor - Max 32 Characters */ + public const CATEGORY = '2:015'; /** Category - Max 3 characters */ + public const CITY = '2:090'; /** City - Max 32 Characters */ + public const COPYRIGHT = '2:116'; /** Copyright Notice - Max 128 Characters */ + public const COUNTRY = '2:101'; /** Country/Primary Location Name - Max 64 characters */ + public const COUNTRY_CODE = '2:100'; /** Country/Primary Location Code - 3 alphabetic characters */ + public const CREATED_DATE = '2:055'; /** Read only: Date Created - 8 numeric characters CCYYMMDD */ + public const CREATED_TIME = '2:060'; /** Read only: Time Created - 11 characters HHMMSS±HHMM */ + public const CREDIT = '2:110'; /** Credit - Max 32 Characters */ + public const EDIT_STATUS = '2:007'; /** Edit Status - Max 64 characters */ + public const GENRE = '2:004'; /** Genres - Max 64 Characters */ + public const HEADLINE = '2:105'; /** Headline - Max 256 Characters */ + public const INSTRUCTIONS = '2:040'; /** Special Instructions - Max 256 Characters */ + public const KEYWORDS = '2:025'; /** Keywords - Max 64 characters */ + public const LOCATION = '2:092'; /** Sub-Location - Max 32 characters */ + public const OBJECT = '2:005'; /** Object Name (Title) - Max 64 characters */ + public const PRIORITY = '2:010'; /** Urgency - 1 numeric character */ + public const SOURCE = '2:115'; /** Source - Max 32 Characters */ + public const STATE = '2:095'; /** Province/State - Max 32 Characters */ + public const SUBJECT_CODE = '2:012'; /** Subject Reference - 13 to 236 characters */ + public const SUPP_CATEGORY = '2:020'; /** Supplemental Category - Max 32 characters */ + public const TRANSFER_REF = '2:103'; /** Original Transmission Reference - Max 32 characters */ - const IPTC_TYPE = 'APP13'; - const IPTC_HEADER = "Photoshop 3.0\x00"; - const IPTC_HEADER_LEN = 14; + public const IPTC_TYPE = 'APP13'; + public const IPTC_HEADER = "Photoshop 3.0\x00"; + public const IPTC_HEADER_LEN = 14; private const IPTC_DATA_ENCODING_UTF8 = "\x1B\x25\x47"; // UTF-8 encoding @@ -86,36 +86,33 @@ public static function encode(string $segment, array|false $iptc_ary): string // Decode original data $irb_data = self::unpackSegmentToIRB($segment); $iptc_data = self::decodeIRBToIPTC($irb_data); - $new_iptc_data = array(); + $new_iptc_data = []; // Recover non-editable data $encoding = false; foreach($iptc_data as $iptc_elt) { if(!self::isEditable($iptc_elt['tag'])) { - if($iptc_elt['tag'] !== self::DATA_ENCODING) { - $new_iptc_data[] = array('tag' => $iptc_elt['tag'], 'data' => $iptc_elt['data']); - } - else { + if($iptc_elt['tag'] !== self::DATA_ENCODING) + $new_iptc_data[] = ['tag' => $iptc_elt['tag'], 'data' => $iptc_elt['data']]; + else $encoding = $iptc_elt['data']; - } } } // Set edited data (and look for 'caption', 'copyright', and 'author' data) - $found_data = array(self::CAPTION => false, self::COPYRIGHT => false, self::AUTHOR => false); + $found_data = [self::CAPTION => false, self::COPYRIGHT => false, self::AUTHOR => false]; if($iptc_ary !== false) { foreach($iptc_ary as $iptc_elt){ if(self::isEditable($iptc_elt['tag'])) { - $new_iptc_data[] = array('tag' => $iptc_elt['tag'], 'data' => $iptc_elt['data']); - foreach($found_data as $tag => $value) { - if($iptc_elt['tag'] === $tag) $found_data[$tag] = true; - } + $new_iptc_data[] = ['tag' => $iptc_elt['tag'], 'data' => $iptc_elt['data']]; + foreach($found_data as $tag => $value) if($iptc_elt['tag'] === $tag) $found_data[$tag] = true; } } } + // Set data encoding to UTF-8 and encode data if($encoding !== false && $encoding !== self::IPTC_DATA_ENCODING_UTF8) - throw new Exception(_('Found IPTC encoding not supported'), Exception::NOT_IMPLEMENTED); + throw new Exception(_('Found not supported IPTC encoding'), Exception::NOT_IMPLEMENTED); foreach($new_iptc_data as $key => $new_iptc_elt) { if($encoding === false && self::isEditable($new_iptc_data[$key]['tag']) && @@ -123,18 +120,17 @@ public static function encode(string $segment, array|false $iptc_ary): string $new_iptc_data[$key]['data'] = mb_convert_encoding($new_iptc_data[$key]['data'], 'UTF-8', 'ISO-8859-1'); } } - array_unshift($new_iptc_data, array('tag' => self::DATA_ENCODING, 'data' => self::IPTC_DATA_ENCODING_UTF8)); - + array_unshift($new_iptc_data, ['tag' => self::DATA_ENCODING, 'data' => self::IPTC_DATA_ENCODING_UTF8]); // Ensure that 'caption', 'copyright', and 'author' data contain data, so that their EXIF values never get picked-up foreach($found_data as $tag => $value) { - if($value === false) $new_iptc_data[] = array('tag' => $tag, 'data' => ''); + if($value === false) $new_iptc_data[] = ['tag' => $tag, 'data' => '']; } // Convert IPTC data into IPTC block $iptc_block = ''; foreach($new_iptc_data as $iptc_rec) { - list($rec, $dat) = sscanf($iptc_rec['tag'], '%d:%d'); + [$rec, $dat] = sscanf($iptc_rec['tag'], '%d:%d'); $iptc_block .= pack("CCCn", 28, $rec, $dat, strlen($iptc_rec['data'])).$iptc_rec['data']; } @@ -144,7 +140,7 @@ public static function encode(string $segment, array|false $iptc_ary): string if($irb_value['id'] === 0x0404) $iptc_block_pos = $irb_pos; } if($iptc_block_pos === -1) $iptc_block_pos = count($irb_data); - $irb_data[$iptc_block_pos] = array('id' => 0x0404, 'name' => "\x00\x00", 'data' => $iptc_block); + $irb_data[$iptc_block_pos] = ['id' => 0x0404, 'name' => "\x00\x00", 'data' => $iptc_block]; // Pack IRB data into the segment string $irb_packed $irb_packed = ''; @@ -182,7 +178,7 @@ public static function encode(string $segment, array|false $iptc_ary): string private static function unpackSegmentToIRB(string $segment): array { $pos = 0; - $data_irb = array(); + $data_irb = []; while($pos $id, 'name' => $name, 'data' => $data); + $data_irb[] = ['id' => $id, 'name' => $name, 'data' => $data]; } return $data_irb; } @@ -225,27 +221,26 @@ private static function unpackSegmentToIRB(string $segment): array */ private static function decodeIRBToIPTC(array $data_irb): array { - $data_iptc = array(); + $data_iptc = []; foreach($data_irb as $data_irb_elt) { if($data_irb_elt['id'] === 0x0404) { // IRB IPTC block $pos = 0; - $data_elt = $data_irb_elt['data']; + $data_elt = $data_irb_elt['data']; while($pos < strlen($data_elt)) { // Check if there is still data to read if(strlen(substr($data_elt, $pos)) < 5) break; $raw = unpack("Ctag/Crec/Cdat/nsize", substr($data_elt, $pos)); $pos += 5; - + // Decode data tag $tag = sprintf("%01d:%03d", $raw['rec'], $raw['dat']); - if(strlen(substr($data_elt, $pos, $raw['size'])) !== $raw['size']) { + if(strlen(substr($data_elt, $pos, $raw['size'])) !== $raw['size']) throw new Exception(_('IPTC data seems to be corrupt while decoding data tag'), Exception::FILE_CORRUPT); - } // Decode and save actual data $data = substr($data_elt, $pos, $raw['size']); - $data_iptc[] = array('tag' => $tag, 'data' => $data); + $data_iptc[] = ['tag' => $tag, 'data' => $data]; $pos += $raw['size']; } } @@ -254,19 +249,16 @@ private static function decodeIRBToIPTC(array $data_irb): array // Find current encoding $encoding = false; foreach($data_iptc as $iptc_elt) { - if($iptc_elt['tag'] === self::DATA_ENCODING) { - $encoding = $iptc_elt['data']; - } + if($iptc_elt['tag'] === self::DATA_ENCODING) $encoding = $iptc_elt['data']; } if($encoding !== false && $encoding !== self::IPTC_DATA_ENCODING_UTF8) - throw new Exception(_('Found IPTC encoding not supported'), Exception::NOT_IMPLEMENTED); + throw new Exception(_('Found not supported IPTC encoding'), Exception::NOT_IMPLEMENTED); // Encode data into UTF-8 format, if the current format is Latin1 foreach($data_iptc as $key => $iptc_elt) { if($encoding == false && self::isEditable($data_iptc[$key]['tag']) && - mb_detect_encoding($data_iptc[$key]['data'], ['ASCII', 'UTF-8'], strict: true) === false) { + mb_detect_encoding($data_iptc[$key]['data'], ['ASCII', 'UTF-8'], strict: true) === false) $data_iptc[$key]['data'] = mb_convert_encoding($data_iptc[$key]['data'], 'UTF-8', 'ISO-8859-1'); - } } return $data_iptc; } @@ -280,36 +272,32 @@ private static function decodeIRBToIPTC(array $data_irb): array */ private static function isEditable(string $tag): bool { - switch($tag) { - case self::AUTHOR: - case self::AUTHOR_TITLE: - case self::CAPTION: - case self::CAPTION_WRITER: - case self::CATEGORY: - case self::CITY: - case self::COPYRIGHT: - case self::COUNTRY: - case self::COUNTRY_CODE: - case self::CREDIT: - case self::EDIT_STATUS: - case self::GENRE: - case self::HEADLINE: - case self::INSTRUCTIONS: - case self::KEYWORDS: - case self::LOCATION: - case self::OBJECT: - case self::PRIORITY: - case self::SOURCE: - case self::STATE: - case self::SUBJECT_CODE: - case self::SUPP_CATEGORY: - case self::TRANSFER_REF: - return true; - default: - return false; - } + return match($tag) { + self::AUTHOR, + self::AUTHOR_TITLE, + self::CAPTION, + self::CAPTION_WRITER, + self::CATEGORY, + self::CITY, + self::COPYRIGHT, + self::COUNTRY, + self::COUNTRY_CODE, + self::CREDIT, + self::EDIT_STATUS, + self::GENRE, + self::HEADLINE, + self::INSTRUCTIONS, + self::KEYWORDS, + self::LOCATION, + self::OBJECT, + self::PRIORITY, + self::SOURCE, + self::STATE, + self::SUBJECT_CODE, + self::SUPP_CATEGORY, + self::TRANSFER_REF => true, + default => false + }; } - } - diff --git a/src/Metadata/Jpeg.php b/src/Metadata/Jpeg.php index c18dd42..ef25ab8 100644 --- a/src/Metadata/Jpeg.php +++ b/src/Metadata/Jpeg.php @@ -3,7 +3,7 @@ * Jpeg.php - JPG metadata encode and decoding functions (IPTC and XMP fields) * * @package Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -29,7 +29,7 @@ class Jpeg { */ public function __construct() { - $this->filename = false; $this->header = array(); $this->img = ''; + $this->filename = false; $this->header = []; $this->img = ''; $this->iptc_data = false; $this->xmp_data = false; $this->exif_data = false; $this->data_read = false; $this->read_only = true; } @@ -54,6 +54,7 @@ public function read(string $filename, bool $read_only = false): void // Initrialize all variables self::__construct(); $this->read_only = $read_only; + $eoi_pos = false; // Open image file for reading $handle = fopen($filename, 'rb'); @@ -74,7 +75,7 @@ public function read(string $filename, bool $read_only = false): void } // Read image header data containing metadata) - $this->header = array(); + $this->header = []; $hit_img_data = false; while($data[1] !== "\xD9" && !$hit_img_data && !feof($handle)) { @@ -85,7 +86,7 @@ public function read(string $filename, bool $read_only = false): void $seg_data = $this->dataRead($handle, $size_dec['size'] - 2); // Save data segment - $this->header[] = array('name' => self::segmentName(ord($data[1])),'tag' => ord($data[1]), 'data' => $seg_data); + $this->header[] = ['name' => self::segmentName(ord($data[1])),'tag' => ord($data[1]), 'data' => $seg_data]; } // Check if the segment was the last one @@ -142,8 +143,7 @@ public function read(string $filename, bool $read_only = false): void */ public function write(string $filename): void { - if(!$this->data_read) - throw new Exception(_('No image and metadata read'), Exception::DATA_NOT_FOUND); + if(!$this->data_read) throw new Exception(_('No image and metadata read'), Exception::DATA_NOT_FOUND); if($this->read_only) throw new Exception(_('Cannot write file because data was read in read-only mode'), Exception::DATA_NOT_FOUND); @@ -155,8 +155,7 @@ public function write(string $filename): void // Write file $handle = fopen($filename, 'wb'); - if($handle === false) - throw new Exception(_('Could not open file for writing'), Exception::FILE_ERROR, $filename); + if($handle === false) throw new Exception(_('Could not open file for writing'), Exception::FILE_ERROR, $filename); // Write SOI for JPEG file fwrite($handle, "\xFF\xD8"); @@ -202,9 +201,8 @@ private function getIptcSegment(): string $nb_header = count($this->header); for($pos = 0; $pos < $nb_header; $pos++) { if($this->header[$pos]['name'] === Iptc::IPTC_TYPE && - strncmp($this->header[$pos]['data'], Iptc::IPTC_HEADER, Iptc::IPTC_HEADER_LEN) === 0) { + str_starts_with($this->header[$pos]['data'], Iptc::IPTC_HEADER)) $segment .= substr($this->header[$pos]['data'], Iptc::IPTC_HEADER_LEN); - } } return $segment; } @@ -218,11 +216,10 @@ private function getIptcSegment(): string public function getIptcData(): array|false { if(!$this->data_read) throw new Exception(_('No image and metadata read'), Exception::DATA_NOT_FOUND); - if($this->iptc_data === false) return false; // Re-format IPTC for easier access: $iptc[$tag] = array($data) - $output = array(); + $output = []; foreach($this->iptc_data as $iptc_elt) $output[$iptc_elt['tag']][] = $iptc_elt['data']; return $output; } @@ -238,11 +235,11 @@ public function setIptcData(array|false $iptc_data_ary): void if(!$this->data_read) throw new Exception(_('No image and metadata read'), Exception::DATA_NOT_FOUND); // Re-format IPTC data to internal format and save it for future reference - $iptc_ary = array(); + $iptc_ary = []; if($iptc_data_ary !== false) { foreach($iptc_data_ary as $tag => $iptc_elt_ary) { foreach($iptc_elt_ary as $iptc_elt) { - $iptc_ary[] = array('tag' => $tag, 'data' => $iptc_elt); + $iptc_ary[] = ['tag' => $tag, 'data' => $iptc_elt]; } } } @@ -254,26 +251,24 @@ public function setIptcData(array|false $iptc_data_ary): void // Delete all existing IPTC IRB blocks (new ones will replace them) for($pos = 0; $pos < count($this->header); $pos++) { if($this->header[$pos]['name'] === Iptc::IPTC_TYPE && - strncmp($this->header[$pos]['data'], Iptc::IPTC_HEADER, Iptc::IPTC_HEADER_LEN) === 0) { + str_starts_with($this->header[$pos]['data'], Iptc::IPTC_HEADER)) { array_splice($this->header, $pos, 1); } } // Find position where to insert IRB data segment into header $pos = count($this->header) - 1; - while($pos >= 0 && ($this->header[$pos]['tag'] > 0xED || $this->header[$pos]['tag'] < 0xE0)) { - $pos--; - } + while($pos >= 0 && ($this->header[$pos]['tag'] > 0xED || $this->header[$pos]['tag'] < 0xE0)) $pos--; // Output blocks of size maximal 32000 while(strlen($irb_packed) > 32000) { - array_splice($this->header, $pos + 1, 0, array('tag' => 0xED, 'name' => Iptc::IPTC_TYPE, - 'data' => Iptc::IPTC_HEADER.substr($irb_packed, 0, 32000))); + array_splice($this->header, $pos + 1, 0, ['tag' => 0xED, 'name' => Iptc::IPTC_TYPE, + 'data' => Iptc::IPTC_HEADER.substr($irb_packed, 0, 32000)]); $irb_packed = substr_replace($irb_packed, '', 0, 32000); $pos++; } array_splice($this->header, $pos + 1, 0, ""); - $this->header[$pos + 1] = array('tag' => 0xED, 'name' => Iptc::IPTC_TYPE, 'data' => Iptc::IPTC_HEADER.$irb_packed); + $this->header[$pos + 1] = ['tag' => 0xED, 'name' => Iptc::IPTC_TYPE, 'data' => Iptc::IPTC_HEADER.$irb_packed]; } @@ -288,9 +283,8 @@ private function getXmpSegment(): string $nb_header = count($this->header); for($pos = 0; $pos < $nb_header; $pos++) { if($this->header[$pos]['name'] === Xmp::XMP_TYPE && - strncmp($this->header[$pos]['data'], Xmp::XMP_HEADER, Xmp::XMP_HEADER_LEN) === 0){ + str_starts_with($this->header[$pos]['data'], Xmp::XMP_HEADER)) return substr($this->header[$pos]['data'], Xmp::XMP_HEADER_LEN); - } } return ''; } @@ -304,7 +298,6 @@ private function getXmpSegment(): string public function getXmpData(): XmpDocument|false { if(!$this->data_read) throw new Exception(_('No image and metadata read'), Exception::DATA_NOT_FOUND); - return $this->xmp_data; } @@ -326,7 +319,7 @@ public function setXmpData(XmpDocument|false $xmp_data): void $nb_header = count($this->header); for($pos = 0; $pos < $nb_header; $pos++) { if($this->header[$pos]['name'] === Xmp::XMP_TYPE && - strncmp($this->header[$pos]['data'], Xmp::XMP_HEADER, Xmp::XMP_HEADER_LEN) === 0) { + str_starts_with($this->header[$pos]['data'], Xmp::XMP_HEADER)) { if($xmp_data === false || $xmp_block === false) { // Remove segment unset($this->header[$pos]); @@ -343,8 +336,8 @@ public function setXmpData(XmpDocument|false $xmp_data): void $pos = 0; while($this->header[$pos]['name'] === Xmp::XMP_TYPE_PRV || $this->header[$pos]['name'] === Xmp::XMP_TYPE) $pos++; - array_splice($this->header, $pos, 0, array(array('tag' => Xmp::XMP_TYPE_TAG, 'name' => Xmp::XMP_TYPE, - 'data' => Xmp::XMP_HEADER.$xmp_block))); + array_splice($this->header, $pos, 0, [['tag' => Xmp::XMP_TYPE_TAG, 'name' => Xmp::XMP_TYPE, + 'data' => Xmp::XMP_HEADER.$xmp_block]]); $this->xmp_data = $xmp_data; } @@ -353,14 +346,13 @@ public function setXmpData(XmpDocument|false $xmp_data): void */ private function getExifSegments(): array { - $exif_segments = array(); + $exif_segments = []; $nb_header = count($this->header); for($pos = 0; $pos < $nb_header; $pos++) { if($this->header[$pos]['name'] === Exif::EXIF_TYPE && - (strncmp($this->header[$pos]['data'], "Exif\x00\x00", 6) === 0 || - strncmp($this->header[$pos]['data'], "Exif\x00\xFF", 6) === 0)) { + (str_starts_with($this->header[$pos]['data'], "Exif\x00\x00") || + str_starts_with($this->header[$pos]['data'], "Exif\x00\xFF"))) $exif_segments[$pos] = substr($this->header[$pos]['data'], 6); - } } return $exif_segments; } @@ -378,7 +370,7 @@ public function getExifData(): array|false if($this->exif_data === false) return false; // Re-format IPTC for easier access - $output = array(); + $output = []; foreach($this->exif_data as $segment_data) { foreach($segment_data as $elt) { $output[$elt['block'].':'.substr('0000'.dechex($elt['tag']), -4)] = $elt['data']; @@ -396,11 +388,11 @@ public function getExifData(): array|false public function setExifData(array|false $exif_data_ary): void { // Re-format IPTC data to internal format - $exif_ary = array(); + $exif_ary = []; if($exif_data_ary !== false) { foreach($exif_data_ary as $block_tag => $exif_elt) { - list($block, $tag) = explode(':', $block_tag); - $exif_ary[] = array('block' => $block, 'tag' => hexdec("0x$tag"), 'data' => $exif_elt); + [$block, $tag] = explode(':', $block_tag); + $exif_ary[] = ['block' => $block, 'tag' => hexdec("0x$tag"), 'data' => $exif_elt]; } } $exif_ary = empty($exif_ary) ? false : $exif_ary; @@ -408,9 +400,8 @@ public function setExifData(array|false $exif_data_ary): void // Encode data and replace old segments if($this->exif_data === false) return; $exif_segments = Exif::encode($this->getExifSegments(), $this->exif_data, $exif_ary); - foreach($exif_segments as $segment_pos => $segment_data) { + foreach($exif_segments as $segment_pos => $segment_data) $this->header[$segment_pos]['data'] = "Exif\x00\x00".$segment_data; - } } /** @@ -428,9 +419,7 @@ public function setExifData(array|false $exif_data_ary): void private function dataRead(mixed $handle, int $length): string { $data = ''; - while(!feof($handle) && strlen($data) < $length) { - $data .= fread($handle, $length - strlen($data)); - } + while(!feof($handle) && strlen($data) < $length) $data .= fread($handle, $length - strlen($data)); return $data; } diff --git a/src/Metadata/Xmp.php b/src/Metadata/Xmp.php index af576d1..958843d 100644 --- a/src/Metadata/Xmp.php +++ b/src/Metadata/Xmp.php @@ -3,7 +3,7 @@ * Xmp.php - Encode and decode XMP data from JPG segment APP1 * * @package Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -16,63 +16,64 @@ class Xmp { - const DESCRIPTION = 'rdf:Description'; /** Main element */ + public const DESCRIPTION = 'rdf:Description'; /** Main element */ // - IPTC Core Metadata 1.3 (may by in any of the namespaces: aux, dc, Iptc4xmpCode, photoshop, xmp) - const AUTHOR = "dc:creator"; /** Seq: Creator (name of photographer) */ - const CAPTION = "dc:description"; /** Alt Lang: Description/Caption */ - const CITY = "Iptc4xmpCore:City"; /** Text: City */ - const COPYRIGHT = "dc:rights"; /** Alt Lang: Copyright notice */ - const COUNTRY = "Iptc4xmpCore:CountryName"; /** Text: Country name */ - const COUNTRY_CODE = "Iptc4xmpCore:CountryCode"; /** Text: ISO country code*/ - const CREATED_DATETIME = "xmp:CreatedDate"; /** Read only: Date and time YYYY-MM-DD HH:MM:SS+HH:MM */ - const CREDIT = "photoshop:Credit"; /** Text: Credit Line */ - const GENRE = "Iptc4xmpCore:IntellectualGenre"; /** Text: Genre */ - const INSTRUCTIONS = "photoshop:Instructions"; /** Text: Instructions */ - const KEYWORDS = "dc:subject"; /** Bag: Keywords */ - const LOCATION = "Iptc4xmpCore:Location"; /** Text: Location */ - const OBJECT = "dc:title"; /** Alt Lang: Object name (Title)*/ - const SCENES = "Iptc4xmpCore:Scene"; /** Bag: Scene codes*/ - const SOURCE = "dc:source"; /** Text: Source */ - const STATE = "Iptc4xmpCore:ProvinceState"; /** Text: Providence/State */ - const SUBJECT_CODE = "Iptc4xmpCore:SubjectCode"; /** Bag: Subject code */ - const USAGE_TERMS = "xmpRights:UsageTerms"; /** Alt Lang: Rights Usage Terms */ - const PM_EDIT_STATUS = "photomechanic:EditStatus"; /** Text: Edit status */ - const PS_AUTHOR_TITLE = "photoshop:AuthorsPosition"; /** Text: Creator's job title */ - const PS_CAPTION_WRITER = "photoshop:CaptionWriter"; /** Text: Caption Writer */ - const PS_CATEGORY = "photoshop:Category"; /** Text: Category */ - const PS_CITY = "photoshop:City"; /** - Text: City */ - const PS_COUNTRY = "photoshop:Country"; /** - Text: Country name */ - const PS_CREATED_DATETIME = "photoshop:DateCreated"; /** - Read only: Date and time YYYY-MM-DD HH:MM:SS+HH:MM */ - const PS_HEADLINE = "photoshop:Headline"; /** - Text: Headline */ - const PS_PRIORITY = "photoshop:Urgency"; /** Text/Int: Urgency */ - const PS_SOURCE = "photoshop:Source"; /** - Text: Source */ - const PS_STATE = "photoshop:State"; /** - Text: Providence/State */ - const PS_SUPP_CATEGORY = "photoshop:SupplementalCategories"; /** Bag: Supplemental categories */ - const PS_TRANSFER_REF = "photoshop:TransmissionReference"; /** Text: Transmission reference */ + public const AUTHOR = "dc:creator"; /** Seq: Creator (name of photographer) */ + public const CAPTION = "dc:description"; /** Alt Lang: Description/Caption */ + public const CITY = "Iptc4xmpCore:City"; /** Text: City */ + public const COPYRIGHT = "dc:rights"; /** Alt Lang: Copyright notice */ + public const COUNTRY = "Iptc4xmpCore:CountryName"; /** Text: Country name */ + public const COUNTRY_CODE = "Iptc4xmpCore:CountryCode"; /** Text: ISO country code*/ + public const CREATED_DATETIME = "xmp:CreatedDate"; /** Read only: Date and time YYYY-MM-DD HH:MM:SS+HH:MM */ + public const CREDIT = "photoshop:Credit"; /** Text: Credit Line */ + public const GENRE = "Iptc4xmpCore:IntellectualGenre"; /** Text: Genre */ + public const INSTRUCTIONS = "photoshop:Instructions"; /** Text: Instructions */ + public const KEYWORDS = "dc:subject"; /** Bag: Keywords */ + public const LOCATION = "Iptc4xmpCore:Location"; /** Text: Location */ + public const OBJECT = "dc:title"; /** Alt Lang: Object name (Title)*/ + public const SCENES = "Iptc4xmpCore:Scene"; /** Bag: Scene codes*/ + public const SOURCE = "dc:source"; /** Text: Source */ + public const STATE = "Iptc4xmpCore:ProvinceState"; /** Text: Providence/State */ + public const SUBJECT_CODE = "Iptc4xmpCore:SubjectCode"; /** Bag: Subject code */ + public const USAGE_TERMS = "xmpRights:UsageTerms"; /** Alt Lang: Rights Usage Terms */ + public const PM_EDIT_STATUS = "photomechanic:EditStatus"; /** Text: Edit status */ + public const PS_AUTHOR_TITLE = "photoshop:AuthorsPosition";/** Text: Creator's job title */ + public const PS_CAPTION_WRITER = "photoshop:CaptionWriter";/** Text: Caption Writer */ + public const PS_CATEGORY = "photoshop:Category"; /** Text: Category */ + public const PS_CITY = "photoshop:City"; /** - Text: City */ + public const PS_COUNTRY = "photoshop:Country"; /** - Text: Country name */ + public const PS_CREATED_DATETIME = "photoshop:DateCreated";/** - Read only: Date and time YYYY-MM-DD HH:MM:SS+HH:MM */ + public const PS_HEADLINE = "photoshop:Headline"; /** - Text: Headline */ + public const PS_PRIORITY = "photoshop:Urgency"; /** Text/Int: Urgency */ + public const PS_SOURCE = "photoshop:Source"; /** - Text: Source */ + public const PS_STATE = "photoshop:State"; /** - Text: Providence/State */ + public const PS_SUPP_CATEGORY = "photoshop:SupplementalCategories"; /** Bag: Supplemental categories */ + public const PS_TRANSFER_REF = "photoshop:TransmissionReference"; /** Text: Transmission reference */ // - IPTC Extension Metadata 1.6 - const EVENT = "Iptc4xmpExt:Event"; /** Alt Lang: Event identifier */ - const ORG_CODE = "Iptc4xmpExt:OrganisationInImageCode"; /** Bag: Code of Organization in image */ - const ORG_NAME = "Iptc4xmpExt:OrganisationInImageName"; /** Bag: Name of Organization in image */ - const PERSON = "Iptc4xmpExt:PersonInImage"; /** Bag: Person shown in image*/ - const RATING = "xmp:Rating"; /** Text: Numeric image rating, -1 (rejected), 0..5 */ + public const EVENT = "Iptc4xmpExt:Event"; /** Alt Lang: Event identifier */ + public const ORG_CODE = "Iptc4xmpExt:OrganisationInImageCode"; /** Bag: Code of Organization in image */ + public const ORG_NAME = "Iptc4xmpExt:OrganisationInImageName"; /** Bag: Name of Organization in image */ + public const PERSON = "Iptc4xmpExt:PersonInImage"; /** Bag: Person shown in image*/ + public const RATING = "xmp:Rating"; /** Text: Numeric image rating, -1 (rejected), 0..5 */ - // - Camera specific records (although aux: has benn dropped in 2021 in favor of exifEX:, but it is still used) - const CAMERA_SERIAL = "aux:SerialNumber"; /** Text: Camera serial number */ - const LENS_MODEL = "aux:Lens"; /** Text: Lens description */ - const LENS_SERIAL = "aux:LensSerialNumber"; /** Text: Lens serial number */ - const COLOR_SPACE = "photoshop:ICCProfile"; /** Text: Color Profile */ - const COLOR_MODE = "photoshop:ColorMode"; /** Text: Color Mode */ + // - Camera specific records (although aux: has been dropped in 2021 in favor of exifEX:, it is still used) + public const CAMERA_SERIAL = "aux:SerialNumber"; /** Text: Camera serial number */ + public const LENS_MODEL = "aux:Lens"; /** Text: Lens description */ + public const LENS_SERIAL = "aux:LensSerialNumber"; /** Text: Lens serial number */ + public const COLOR_SPACE = "photoshop:ICCProfile"; /** Text: Color Profile */ + public const COLOR_MODE = "photoshop:ColorMode"; /** Text: Color Mode */ // History specification - const EDIT_HISTORY = 'xmpMM:History'; /** Seq: ResourceEvebt */ + public const EDIT_HISTORY = 'xmpMM:History'; /** Seq: ResourceEvebt */ + + public const XMP_TYPE_PRV = 'APP0'; + public const XMP_TYPE = 'APP1'; + public const XMP_HEADER = "http://ns.adobe.com/xap/1.0/\x00"; + public const XMP_HEADER_LEN = 29; + public const XMP_TYPE_TAG = 0xE1; - const XMP_TYPE_PRV = 'APP0'; - const XMP_TYPE = 'APP1'; - const XMP_HEADER = "http://ns.adobe.com/xap/1.0/\x00"; - const XMP_HEADER_LEN = 29; - const XMP_TYPE_TAG = 0xE1; private const XMP_XML_HEADER = ''; private const XMP_XML_HEADER_LEN = 21; diff --git a/src/Metadata/XmpDocument.php b/src/Metadata/XmpDocument.php index 06fab5d..9602a31 100644 --- a/src/Metadata/XmpDocument.php +++ b/src/Metadata/XmpDocument.php @@ -3,7 +3,7 @@ * XmpDocument.php - Functions for reading and writing XMP specific data * * @package Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -17,47 +17,44 @@ class XmpDocument { - /** DOMDocument to which to add functions */ - private \DOMDocument $dom; - /** Namespaces that may contain IPTC Code Metadata 1.3 (in descending order of preference) */ - const NS_IPTC4XMPCORE = 'Iptc4xmpCore'; - const NS_DC = 'dc'; - const NS_AUX = 'aux'; - const NS_XMP = 'xmp'; - const NS_PHOTOSHOP = 'photoshop'; - const NS_PHOTOMECHANIC = 'photomechanic'; - - /** Supported Languages */ - const LANG_ALL = 'x-all'; /** All languages */ - const LANG_DEFAULT = 'x-default'; /** Default language: English */ + public const NS_IPTC4XMPCORE = 'Iptc4xmpCore'; + public const NS_DC = 'dc'; + public const NS_AUX = 'aux'; + public const NS_XMP = 'xmp'; + public const NS_PHOTOSHOP = 'photoshop'; + public const NS_PHOTOMECHANIC = 'photomechanic'; + + /** Languages (non exchaustive) */ + private const LANG_ALL = 'x-all'; /** All languages */ + private const LANG_DEFAULT = 'x-default'; /** Default language: English */ /** Private variables */ - private array $nsPriorityAry; /** Prioritized array of name spaces for core metadata */ + private array $nsPriorityAry; /** Prioritized array of name spaces for core metadata */ /** * Constructor * * @param DOMDocument DOM of XMP data */ - public function __construct(\DOMDocument $dom) + public function __construct(private \DOMDocument $dom) { - $this->nsPriorityAry = array(self::NS_IPTC4XMPCORE, self::NS_DC, self::NS_AUX, self::NS_XMP, self::NS_PHOTOSHOP, - self::NS_PHOTOMECHANIC); + $this->nsPriorityAry = [self::NS_IPTC4XMPCORE, self::NS_DC, self::NS_AUX, self::NS_XMP, self::NS_PHOTOSHOP, + self::NS_PHOTOMECHANIC]; + // All namespaces supported by default, others may be added before use using 'setXmpNamespace' - $all_ns = array('Iptc4xmpCore' => 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/', - 'aux' => 'http://ns.adobe.com/exif/1.0/aux/', - 'dc' => 'http://purl.org/dc/elements/1.1/', - 'xmp' => 'http://ns.adobe.com/xap/1.0/', - 'photoshop' => 'http://ns.adobe.com/photoshop/1.0/', - 'photomechanic' => 'http://ns.camerabits.com/photomechanic/1.0/', - 'Iptc4xmpExt' => 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/', - 'GettyImagesGIFT' => 'http://xmp.gettyimages.com/gift/1.0/', - 'exifEX' => 'http://cipa.jp/exif/1.0/', - 'plus' => 'http://ns.useplus.org/ldf/xmp/1.0/', - 'xmpMM' => 'http://ns.adobe.com/xap/1.0/mm/', - 'xmpRights' => 'http://ns.adobe.com/xap/1.0/rights/'); - $this->dom = $dom; + $all_ns = ['Iptc4xmpCore' => 'http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/', + 'aux' => 'http://ns.adobe.com/exif/1.0/aux/', + 'dc' => 'http://purl.org/dc/elements/1.1/', + 'xmp' => 'http://ns.adobe.com/xap/1.0/', + 'photoshop' => 'http://ns.adobe.com/photoshop/1.0/', + 'photomechanic' => 'http://ns.camerabits.com/photomechanic/1.0/', + 'Iptc4xmpExt' => 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/', + 'GettyImagesGIFT' => 'http://xmp.gettyimages.com/gift/1.0/', + 'exifEX' => 'http://cipa.jp/exif/1.0/', + 'plus' => 'http://ns.useplus.org/ldf/xmp/1.0/', + 'xmpMM' => 'http://ns.adobe.com/xap/1.0/mm/', + 'xmpRights' => 'http://ns.adobe.com/xap/1.0/rights/']; $this->validateXmpDocument($all_ns); } @@ -142,10 +139,10 @@ public function isXmpBag(string $name): bool */ public function getXmpText(string $name, string|false $lang = false): string|false { - if(strpos($name, ':') === false) + if(!str_contains($name, ':')) throw new Exception(_('Node name without prefix found'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); // Seach first in specified name space (if any) $result = $this->getXmpTextNS($prefix, $name, lang: $lang); @@ -175,10 +172,10 @@ public function getXmpText(string $name, string|false $lang = false): string|fal */ public function getXmpLangAlt(string $name, string|false $lang = false): array|false { - if(strpos($name, ':') === false) + if(!str_contains($name, ':')) throw new Exception(_('Node name without prefix found'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); // Seach first in specified name space (if any) $result = $this->getXmpLangAltNS($prefix, $name, lang: $lang); @@ -203,10 +200,10 @@ public function getXmpLangAlt(string $name, string|false $lang = false): array|f */ public function getXmpBag(string $name): array|false { - if(strpos($name, ':') === false) + if(!str_contains($name, ':')) throw new Exception(_('Node name without prefix found'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); // Seach first in specified name space (if any) $result = $this->getXmpBagNS($prefix, $name); @@ -269,18 +266,17 @@ public function setXmpNamespace(string $ns, string $uri): void */ public function setXmpText(string $name, string|int|false $data): void { - if(strpos($name, ':') === false) + if(!str_contains($name, ':')) throw new Exception(_('Node name without prefix found'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); $this->setXmpTextNS($prefix, $name, $data, update_only: false); // Update nodes is associated namespaces if(in_array($prefix, $this->nsPriorityAry)) { foreach($this->nsPriorityAry as $ns_prefix) { - if($this->isXmpText("$ns_prefix:$name") && $ns_prefix !== $prefix) { + if($this->isXmpText("$ns_prefix:$name") && $ns_prefix !== $prefix) $this->setXmpTextNS($prefix, $name, $data, update_only: true); - } } } } @@ -298,15 +294,14 @@ public function setXmpSeq(string $name, string|int|false $data): void throw new Exception(_('Cannot set')." 'rdf:Seq' "._('node value if an attribute with the same name exists'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); $this->setXmpLiNS('Seq', $prefix, $name, $data, lang: false, update_only: false); // Update nodes is associated namespaces if(in_array($prefix, $this->nsPriorityAry)) { foreach($this->nsPriorityAry as $ns_prefix) { - if($this->isXmpLi('Seq', "$ns_prefix:$name") && $ns_prefix !== $prefix) { + if($this->isXmpLi('Seq', "$ns_prefix:$name") && $ns_prefix !== $prefix) $this->setXmpLiNS('Seq', $ns_prefix, $name, $data, lang: false, update_only: true); - } } } } @@ -325,15 +320,14 @@ public function setXmpAlt(string $name, string|int|false $data, string|false $la throw new Exception(_('Cannot set')." 'rdf:Alt' "._('node value if an attribute with the same name exists'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); $this->setXmpLiNS('Alt', $prefix, $name, $data, lang: $lang, update_only: false); // Update nodes is associated namespaces if(in_array($prefix, $this->nsPriorityAry)) { foreach($this->nsPriorityAry as $ns_prefix) { - if($this->isXmpLi('Alt', "$ns_prefix:$name") && $ns_prefix !== $prefix) { + if($this->isXmpLi('Alt', "$ns_prefix:$name") && $ns_prefix !== $prefix) $this->setXmpLiNS('Alt', $ns_prefix, $name, $data, lang: $lang, update_only: true); - } } } } @@ -351,15 +345,14 @@ public function setXmpLangAlt(string $name, array|false $data): void throw new Exception(_('Cannot set')." 'rdf:Alt' "._('node value if an attribute with the same name exists'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); $this->setXmpLiLangNS($prefix, $name, $data); // Update nodes is associated namespaces if(in_array($prefix, $this->nsPriorityAry)) { foreach($this->nsPriorityAry as $ns_prefix) { - if($this->isXmpLi('Alt', "$ns_prefix:$name") && $ns_prefix !== $prefix) { + if($this->isXmpLi('Alt', "$ns_prefix:$name") && $ns_prefix !== $prefix) $this->setXmpLiLangNS($ns_prefix, $name, $data); - } } } } @@ -377,15 +370,14 @@ public function setXmpBag(string $name, array|false $data): void throw new Exception(_('Cannot set')." 'rdf:Bag' "._('node value if an attribute with the same name exists'), Exception::INVALID_FIELD_ID, $name); - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); $this->setXmpLiNS('Bag', $prefix, $name, $data, lang: false, update_only: false); // Update nodes is associated namespaces if(in_array($prefix, $this->nsPriorityAry)) { foreach($this->nsPriorityAry as $ns_prefix) { - if($this->isXmpLi('Bag', "$ns_prefix:$name") && $ns_prefix !== $prefix) { + if($this->isXmpLi('Bag', "$ns_prefix:$name") && $ns_prefix !== $prefix) $this->setXmpLiNS('Bag', $ns_prefix, $name, $data, lang: false, update_only: true); - } } } } @@ -408,7 +400,7 @@ public function addLanguages(): void */ private function recFindLanguages(\DOMElement $dom): array { - $result = array(); + $result = []; if($dom->hasAttribute('xml:lang')) { $lang = $dom->getAttribute('xml:lang'); if($lang !== 'x-default') $result[$lang] = $lang; @@ -498,7 +490,7 @@ protected function getXmpTextNS(string $ns, string $name, string|false $lang = f if($node === false) return false; //Try rdf:Alt - We only read the first list element - $lang_result = array(); + $lang_result = []; $child_alt = self::getXmpFirstNodeByName($node, 'rdf:Alt'); if($child_alt !== false) { $subchildren = self::getXmpAllNodeByName($child_alt, 'rdf:li'); @@ -567,7 +559,7 @@ protected function getXmpLangAltNS(string $ns, string $name, string|false $lang) if($node === false) return false; // Read rdf:Alt - We only read the first list element - $lang_result = array(); + $lang_result = []; $child_alt = self::getXmpFirstNodeByName($node, 'rdf:Alt'); if($child_alt !== false) { $subchildren = self::getXmpAllNodeByName($child_alt, 'rdf:li'); @@ -576,14 +568,12 @@ protected function getXmpLangAltNS(string $ns, string $name, string|false $lang) Exception::DATA_FORMAT_ERROR, $name); foreach($subchildren as $subchild) { if($subchild->hasAttribute('xml:lang')) { - if($subchild->getAttribute('xml:lang') === $lang || $lang === self::LANG_ALL) { + if($subchild->getAttribute('xml:lang') === $lang || $lang === self::LANG_ALL) $lang_result[$subchild->getAttribute('xml:lang')] = (string)$subchild->nodeValue; - } } else { - if(!isset($lang_result[self::LANG_DEFAULT])) { + if(!isset($lang_result[self::LANG_DEFAULT])) $lang_result[self::LANG_DEFAULT] = (string)$subchild->nodeValue; - } } } } @@ -610,11 +600,10 @@ protected function getXmpBagNS(string $ns, string $name): array|false throw new Exception(_('Cannot find')." 'rdf:Bag' "._('node'), Exception::DATA_FORMAT_ERROR, $name); // Find all rdf:li elements - $result = array(); + $result = []; $grandchildren = $child->getElementsByTagName('li'); foreach($grandchildren as $grandchild) { - if($grandchild->prefix === 'rdf' && !empty($grandchild->nodeValue)) - $result[] = $grandchild->nodeValue; + if($grandchild->prefix === 'rdf' && !empty($grandchild->nodeValue)) $result[] = $grandchild->nodeValue; } return !empty($result) ? $result : false; } @@ -669,22 +658,18 @@ protected function setXmpTextNS(string $ns, string $name, string|int|false $data $node = self::getXmpFirstNodeByName($this->dom, $name); if($node !== false) { // Update node value - if($data === false) { + if($data === false) $node->parentNode->removeChild($node); - } - else { + else $node->nodeValue = (string)$data; - } - } else { if($data !== false && !$update_only){ // Search for rdf:Description in the relevant name space to add attribute to $status = false; foreach($descs as $desc) { - if($desc->hasAttribute("xmlns:$ns")) { + if($desc->hasAttribute("xmlns:$ns")) $status = $desc->setAttribute($name, (string)$data); - } } if($status === false) throw new Exception(_('Error creating new attribute'), Exception::INTERNAL_ERROR, $name); @@ -693,16 +678,14 @@ protected function setXmpTextNS(string $ns, string $name, string|int|false $data } // Check if there exist alternate nodes with the same name - list($prefix, $suffix) = explode(':', $name); + [$prefix, $suffix] = explode(':', $name); $nodes = $this->dom->getElementsByTagName($suffix); foreach($nodes as $node) { if($node->prefix !== $prefix) { - if($data === false) { + if($data === false) $node->parentNode->removeChild($node); - } - else { + else $node->nodeValue = (string)$data; - } } } } @@ -742,7 +725,7 @@ protected function setXmpLiNS(string $tag, string $ns, string $name, array|strin // Check if $node has children that are not of tye rdf:$tag if($node->childNodes->count() !== 0) { - $remove_children = array(); + $remove_children = []; foreach($node->childNodes as $child) { if($child->prefix === 'rdf' && $child->nodeName !== "rdf:$tag") { throw new Exception(_('Incorrect element tag found'), Exception::DATA_FORMAT_ERROR, $child->nodeName); @@ -750,9 +733,7 @@ protected function setXmpLiNS(string $tag, string $ns, string $name, array|strin $remove_children[] = $child; } if(!empty($remove_children)) { - foreach($remove_children as $remove_child) { - $node->removeChild($remove_child); - } + foreach($remove_children as $remove_child) $node->removeChild($remove_child); } } @@ -766,16 +747,13 @@ protected function setXmpLiNS(string $tag, string $ns, string $name, array|strin // Delete all sub-nodes of $rdf_tag that are in the specified language (if any language specified) $all_rdf_li = self::getXmpAllNodeByName($rdf_tag, 'rdf:li'); if($all_rdf_li !== false) { - $remove_children = array(); + $remove_children = []; foreach($all_rdf_li as $rdf_li) { - if($lang === false || $rdf_li->getAttribute('xml:lang') === $lang) { + if($lang === false || $rdf_li->getAttribute('xml:lang') === $lang) $remove_children[] = $rdf_li; - } } if(!empty($remove_children)) { - foreach($remove_children as $remove_child) { - $rdf_tag->removeChild($remove_child); - } + foreach($remove_children as $remove_child) $rdf_tag->removeChild($remove_child); } } @@ -826,17 +804,14 @@ protected function setXmpLiLangNS(string $ns, string $name, array|false $data): // Check if $node has children that are not of tye rdf:Alt if($node->childNodes->count() !== 0) { - $remove_children = array(); + $remove_children = []; foreach($node->childNodes as $child) { - if($child->prefix === 'rdf' && $child->nodeName !== "rdf:Alt") { + if($child->prefix === 'rdf' && $child->nodeName !== "rdf:Alt") throw new Exception(_('Incorrect element tag found'), Exception::DATA_FORMAT_ERROR, $child->nodeName); - } $remove_children[] = $child; } if(!empty($remove_children)) { - foreach($remove_children as $remove_child) { - $node->removeChild($remove_child); - } + foreach($remove_children as $remove_child) $node->removeChild($remove_child); } } @@ -850,14 +825,10 @@ protected function setXmpLiLangNS(string $ns, string $name, array|false $data): // Delete all sub-nodes of $rdf_tag $all_rdf_li = self::getXmpAllNodeByName($rdf_tag, 'rdf:li'); if($all_rdf_li !== false) { - $remove_children = array(); - foreach($all_rdf_li as $rdf_li) { - $remove_children[] = $rdf_li; - } + $remove_children = []; + foreach($all_rdf_li as $rdf_li) $remove_children[] = $rdf_li; if(!empty($remove_children)) { - foreach($remove_children as $remove_child) { - $rdf_tag->removeChild($remove_child); - } + foreach($remove_children as $remove_child) $rdf_tag->removeChild($remove_child); } } @@ -885,7 +856,7 @@ protected static function existXmpAttribute(\DOMDocument|\DOMElement|\DOMNode $d string $name, string $att_name): bool { // Search node with name $name - list($prefix, $name) = explode(':', $name, 2); + [$prefix, $name] = explode(':', $name, 2); $nodes = $dom->getElementsByTagName($name); foreach($nodes as $node) { if($node->prefix === $prefix && $node->hasAttribute($att_name)) return true; @@ -905,8 +876,8 @@ protected static function getXmpFirstNodeByName(\DOMDocument|\DOMElement|\DOMNod string $name, string $ns = ''): \DOMElement|false { $prefix = ''; - if(strpos($name, ':') !== false) list($prefix, $name) = explode(':', $name, 2); - if(strpos($ns, ':') !== false) list($ns, $dummy) = explode(':', $ns, 2); + if(str_contains($name, ':')) [$prefix, $name] = explode(':', $name, 2); + if(str_contains($ns, ':')) [$ns, $dummy] = explode(':', $ns, 2); $childs = $dom->getElementsByTagName($name); foreach($childs as $child) { if(empty($prefix) || $child->prefix === $prefix) { @@ -927,10 +898,10 @@ protected static function getXmpFirstNodeByName(\DOMDocument|\DOMElement|\DOMNod protected static function getXmpAllNodeByName(\DOMDocument|\DOMElement|\DOMNode $dom, string $name, string $ns = ''): array|false { - $result = array(); + $result = []; $prefix = ''; - if(strpos($name, ':') !== false) list($prefix, $name) = explode(':', $name, 2); - if(strpos($ns, ':') !== false) list($ns, $dummy) = explode(':', $ns, 2); + if(str_contains($name, ':')) [$prefix, $name] = explode(':', $name, 2); + if(str_contains($ns, ':')) [$ns, $dummy] = explode(':', $ns, 2); $childs = $dom->getElementsByTagName($name); foreach($childs as $child) { if(empty($prefix) || $child->prefix === $prefix) { @@ -965,16 +936,13 @@ protected function validateXmpDocument(array $ns_ary): void } // For each namespace not found, add a rdf:Document section xmlns:$ns="$uri" - foreach($ns_ary as $ns => $uri) { - $this->setXmpNamespace($ns, $uri); - } + foreach($ns_ary as $ns => $uri) $this->setXmpNamespace($ns, $uri); // Re-load to ensure namespaces are recognized // -- Disable warning messages from loadXML only $old_error_reporting = error_reporting(error_reporting() & ~E_WARNING); $status = $this->dom->loadXML($this->dom->saveXML()); error_reporting($old_error_reporting); - if($status === false) - throw new Exception(_('Internal error during XML re-validation'), Exception::INTERNAL_ERROR); + if($status === false) throw new Exception(_('Internal error during XML re-validation'), Exception::INTERNAL_ERROR); } } diff --git a/test/example.php b/test/example.php index dbd995e..d20c548 100644 --- a/test/example.php +++ b/test/example.php @@ -3,7 +3,7 @@ * example.php - Image file metadata handing exampe file * * @project Holiday\Metadata - * @version 1.1 + * @version 1.2 * @author Claude Diderich (cdiderich@cdsp.photo) * @copyright (c) 2022 by Claude Diderich * @license https://opensource.org/licenses/mit MIT @@ -28,14 +28,14 @@ function my__autoload(string $name): void use \Holiday\Metadata; /*** EXAMPLE ***/ -$testfiles_ary = array('img.example.jpg', 'img.mlexample.jpg'); -$exif_data_ary = array(Metadata::IMG_CAMERA_MAKE => 'CAMERA MAKE', Metadata::IMG_CAMERA_MODEL => 'CAMERA MODEL', - Metadata::IMG_CAMERA_SERIAL => 'CAMERA SERIAL', Metadata::IMG_LENS_MODEL => 'LENS MODEL', - Metadata::IMG_COLOR_SPACE_FMT => 'COLOR SPACE', Metadata::IMG_ISO => 'ISO SETTING', - Metadata::IMG_APERTURE_FMT => 'APERTURE', Metadata::IMG_EXPOSURE_FMT => 'EXPOSURE', - Metadata::IMG_FOCAL_LENGTH_FMT => 'FOCAL LENGTH', Metadata::IMG_FLASH_FMT => 'FLASH USED', - Metadata::IMG_SIZE_FMT => 'IMAGE SIZE', Metadata::IMG_RESOLUTION_FMT => 'RESOLUTION', - Metadata::IMG_SOFTWARE => 'SOFTWARE'); +$testfiles_ary = ['img.example.jpg', 'img.mlexample.jpg']; +$exif_data_ary = [Metadata::IMG_CAMERA_MAKE => 'CAMERA MAKE', Metadata::IMG_CAMERA_MODEL => 'CAMERA MODEL', + Metadata::IMG_CAMERA_SERIAL => 'CAMERA SERIAL', Metadata::IMG_LENS_MODEL => 'LENS MODEL', + Metadata::IMG_COLOR_SPACE_FMT => 'COLOR SPACE', Metadata::IMG_ISO => 'ISO SETTING', + Metadata::IMG_APERTURE_FMT => 'APERTURE', Metadata::IMG_EXPOSURE_FMT => 'EXPOSURE', + Metadata::IMG_FOCAL_LENGTH_FMT => 'FOCAL LENGTH', Metadata::IMG_FLASH_FMT => 'FLASH USED', + Metadata::IMG_SIZE_FMT => 'IMAGE SIZE', Metadata::IMG_RESOLUTION_FMT => 'RESOLUTION', + Metadata::IMG_SOFTWARE => 'SOFTWARE']; /** * Use of class Metadata