diff --git a/.gitignore b/.gitignore index 28fa55a..9fbb44c 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ composer.phar .DS_Store .idea .phpunit.result.cache +.phpunit.cache +coverage.xml infection.html # Otherwise we won't be able to install it on different php version. composer.lock \ No newline at end of file diff --git a/composer.json b/composer.json index 93b4e2a..e27c369 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,7 @@ "lychee-org/phpstan-lychee": "^2.0.1|^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpmd/phpmd": "^2.15", - "phpunit/phpunit": "^9.5.10", + "phpunit/phpunit": "^11.5 || ^12.0 || ^13.0", "squizlabs/php_codesniffer": "^3.9", "thecodingmachine/phpstan-safe-rule": "^1.2" }, @@ -65,6 +65,7 @@ "vendor/bin/phpcs --standard=PSR2 ./lib/" ], "run-tests": [ + "@putenv XDEBUG_MODE=coverage", "vendor/bin/phpunit -c phpunit.xml.dist", "vendor/bin/phpunit --coverage-clover=coverage.xml" ], diff --git a/lib/PHPExif/Exif.php b/lib/PHPExif/Exif.php index 35536eb..37434ae 100644 --- a/lib/PHPExif/Exif.php +++ b/lib/PHPExif/Exif.php @@ -26,14 +26,21 @@ class Exif public const COUNTRY = 'country'; public const CREATION_DATE = 'creationdate'; public const CREDIT = 'credit'; + public const CROPPEDAREAIMAGEHEIGHTPIXELS = 'CroppedAreaImageHeightPixels'; + public const CROPPEDAREAIMAGEWIDTHPIXELS = 'CroppedAreaImageWidthPixels'; + public const CROPPEDAREALEFTPIXELS = 'CroppedAreaLeftPixels'; + public const CROPPEDAREATOPPIXELS = 'CroppedAreaTopPixels'; public const DESCRIPTION = 'description'; public const DURATION = 'duration'; public const EXPOSURE = 'exposure'; + public const EXPOSURE_BIAS = 'exposureBias'; public const FILESIZE = 'FileSize'; public const FILENAME = 'FileName'; public const FOCAL_LENGTH = 'focalLength'; public const FOCAL_DISTANCE = 'focalDistance'; public const FRAMERATE = 'framerate'; + public const FULLPANOHEIGHTPIXELS = 'FullPanoHeightPixels'; + public const FULLPANOWIDTHPIXELS = 'FullPanoWidthPixels'; public const GPS = 'gps'; public const HEADLINE = 'headline'; public const HEIGHT = 'height'; @@ -49,12 +56,16 @@ class Exif public const MICROVIDEOOFFSET = 'MicroVideoOffset'; public const MIMETYPE = 'MimeType'; public const ORIENTATION = 'Orientation'; + public const PROJECTIONTYPE = 'ProjectionType'; public const SOFTWARE = 'software'; public const SOURCE = 'source'; public const STATE = 'state'; public const SUBLOCATION = 'Sublocation'; public const TITLE = 'title'; + public const USEPANORAMAVIEWER = 'UsePanoramaViewer'; public const VERTICAL_RESOLUTION = 'verticalResolution'; + public const WHITE_BALANCE = 'whiteBalance'; + public const WHITE_BALANCE_TEMPERATURE = 'whiteBalanceTemperature'; public const WIDTH = 'width'; @@ -341,6 +352,91 @@ public function setExposure(string $value): Exif return $this; } + /** + * Returns the exposure bias (exposure compensation) in EV, if it exists + * e.g. -0.67 for -2/3 EV + * + * @return float|false + */ + public function getExposureBias(): float|false + { + if (!isset($this->data[self::EXPOSURE_BIAS])) { + return false; + } + + return $this->data[self::EXPOSURE_BIAS]; + } + + /** + * Sets the exposure bias (exposure compensation) in EV + * + * @param float $value + * @return Exif + */ + public function setExposureBias(float $value): Exif + { + $this->data[self::EXPOSURE_BIAS] = $value; + + return $this; + } + + /** + * Returns the EXIF white balance mode, if it exists + * 0 = auto, 1 = manual + * + * @return int|false + */ + public function getWhiteBalance(): int|false + { + if (!isset($this->data[self::WHITE_BALANCE])) { + return false; + } + + return $this->data[self::WHITE_BALANCE]; + } + + /** + * Sets the EXIF white balance mode (0 = auto, 1 = manual) + * + * @param int $value + * @return Exif + */ + public function setWhiteBalance(int $value): Exif + { + $this->data[self::WHITE_BALANCE] = $value; + + return $this; + } + + /** + * Returns the white balance colour temperature in Kelvin, if it exists. + * Only the exiftool adapter provides it, as it comes from the maker notes + * (or Lightroom's XMP-crs data). + * + * @return int|false + */ + public function getWhiteBalanceTemperature(): int|false + { + if (!isset($this->data[self::WHITE_BALANCE_TEMPERATURE])) { + return false; + } + + return $this->data[self::WHITE_BALANCE_TEMPERATURE]; + } + + /** + * Sets the white balance colour temperature in Kelvin + * + * @param int $value + * @return Exif + */ + public function setWhiteBalanceTemperature(int $value): Exif + { + $this->data[self::WHITE_BALANCE_TEMPERATURE] = $value; + + return $this; + } + /** * Returns the Exposure * @@ -1174,6 +1270,224 @@ public function getMicroVideoOffset(): string|false return $this->data[self::MICROVIDEOOFFSET]; } + /** + * Sets the GPano projection type (lower-cased by the mappers) + * + * @param string $value + * @return Exif + */ + public function setProjectionType(string $value): Exif + { + $this->data[self::PROJECTIONTYPE] = $value; + + return $this; + } + + /** + * Returns the GPano projection type, if it exists + * e.g. "equirectangular" for a photo sphere + * + * @return string|false + */ + public function getProjectionType(): string|false + { + if (!isset($this->data[self::PROJECTIONTYPE])) { + return false; + } + + return $this->data[self::PROJECTIONTYPE]; + } + + /** + * Sets the GPano UsePanoramaViewer flag + * + * @param bool $value + * @return Exif + */ + public function setUsePanoramaViewer(bool $value): Exif + { + $this->data[self::USEPANORAMAVIEWER] = $value; + + return $this; + } + + /** + * Returns the GPano UsePanoramaViewer flag, or null if it does not exist. + * Unlike the other getters, false is a meaningful value here. + * + * @return bool|null + */ + public function getUsePanoramaViewer(): ?bool + { + if (!isset($this->data[self::USEPANORAMAVIEWER])) { + return null; + } + + return $this->data[self::USEPANORAMAVIEWER]; + } + + /** + * Sets the GPano full panorama width, in pixels + * + * @param int $value + * @return Exif + */ + public function setFullPanoWidthPixels(int $value): Exif + { + $this->data[self::FULLPANOWIDTHPIXELS] = $value; + + return $this; + } + + /** + * Returns the GPano full panorama width, in pixels, if it exists + * + * @return int|false + */ + public function getFullPanoWidthPixels(): int|false + { + if (!isset($this->data[self::FULLPANOWIDTHPIXELS])) { + return false; + } + + return $this->data[self::FULLPANOWIDTHPIXELS]; + } + + /** + * Sets the GPano full panorama height, in pixels + * + * @param int $value + * @return Exif + */ + public function setFullPanoHeightPixels(int $value): Exif + { + $this->data[self::FULLPANOHEIGHTPIXELS] = $value; + + return $this; + } + + /** + * Returns the GPano full panorama height, in pixels, if it exists + * + * @return int|false + */ + public function getFullPanoHeightPixels(): int|false + { + if (!isset($this->data[self::FULLPANOHEIGHTPIXELS])) { + return false; + } + + return $this->data[self::FULLPANOHEIGHTPIXELS]; + } + + /** + * Sets the GPano left offset of the cropped area, in pixels + * + * @param int $value + * @return Exif + */ + public function setCroppedAreaLeftPixels(int $value): Exif + { + $this->data[self::CROPPEDAREALEFTPIXELS] = $value; + + return $this; + } + + /** + * Returns the GPano left offset of the cropped area, in pixels, if it exists + * + * @return int|false + */ + public function getCroppedAreaLeftPixels(): int|false + { + if (!isset($this->data[self::CROPPEDAREALEFTPIXELS])) { + return false; + } + + return $this->data[self::CROPPEDAREALEFTPIXELS]; + } + + /** + * Sets the GPano top offset of the cropped area, in pixels + * + * @param int $value + * @return Exif + */ + public function setCroppedAreaTopPixels(int $value): Exif + { + $this->data[self::CROPPEDAREATOPPIXELS] = $value; + + return $this; + } + + /** + * Returns the GPano top offset of the cropped area, in pixels, if it exists + * + * @return int|false + */ + public function getCroppedAreaTopPixels(): int|false + { + if (!isset($this->data[self::CROPPEDAREATOPPIXELS])) { + return false; + } + + return $this->data[self::CROPPEDAREATOPPIXELS]; + } + + /** + * Sets the GPano cropped area width, in pixels + * + * @param int $value + * @return Exif + */ + public function setCroppedAreaImageWidthPixels(int $value): Exif + { + $this->data[self::CROPPEDAREAIMAGEWIDTHPIXELS] = $value; + + return $this; + } + + /** + * Returns the GPano cropped area width, in pixels, if it exists + * + * @return int|false + */ + public function getCroppedAreaImageWidthPixels(): int|false + { + if (!isset($this->data[self::CROPPEDAREAIMAGEWIDTHPIXELS])) { + return false; + } + + return $this->data[self::CROPPEDAREAIMAGEWIDTHPIXELS]; + } + + /** + * Sets the GPano cropped area height, in pixels + * + * @param int $value + * @return Exif + */ + public function setCroppedAreaImageHeightPixels(int $value): Exif + { + $this->data[self::CROPPEDAREAIMAGEHEIGHTPIXELS] = $value; + + return $this; + } + + /** + * Returns the GPano cropped area height, in pixels, if it exists + * + * @return int|false + */ + public function getCroppedAreaImageHeightPixels(): int|false + { + if (!isset($this->data[self::CROPPEDAREAIMAGEHEIGHTPIXELS])) { + return false; + } + + return $this->data[self::CROPPEDAREAIMAGEHEIGHTPIXELS]; + } + /** * Sets the sublocation value * diff --git a/lib/PHPExif/Mapper/AbstractMapper.php b/lib/PHPExif/Mapper/AbstractMapper.php index b8d1c13..ded11a1 100644 --- a/lib/PHPExif/Mapper/AbstractMapper.php +++ b/lib/PHPExif/Mapper/AbstractMapper.php @@ -34,4 +34,88 @@ public function trim(mixed $data): mixed } return $data; } + + /** + * Normalize a boolean XMP value: true/false (any case), 1/0 or a PHP bool + * + * @param mixed $value + * @return bool|null null if the value is not a recognised boolean + */ + protected function normalizeBool(mixed $value): ?bool + { + if (is_bool($value)) { + return $value; + } + if (!is_int($value) && !is_string($value)) { + return null; + } + + return match (strtolower(trim((string) $value))) { + 'true', '1' => true, + 'false', '0' => false, + default => null, + }; + } + + /** + * Normalize an integer value + * + * @param mixed $value + * @return int|null null if the value is not numeric + */ + protected function normalizeInt(mixed $value): ?int + { + if (!is_numeric($value)) { + return null; + } + + return (int) $value; + } + + /** + * Normalize the exposure bias to EV, rounded to 2 decimals. + * Accepts a number or a rational string such as "-2/3". + * Rounding keeps the readers consistent: exiftool -n only + * prints 10 significant digits, e.g. -0.6666666667. + * + * @param mixed $value + * @return float|null null if the value is not a valid number or rational + */ + protected function normalizeExposureBias(mixed $value): ?float + { + if (is_numeric($value)) { + return round((float) $value, 2); + } + if (!is_string($value)) { + return null; + } + + $parts = explode('/', $value, 2); + if (count($parts) !== 2 || !is_numeric($parts[0]) || !is_numeric($parts[1]) || (float) $parts[1] === 0.0) { + return null; + } + + return round((float) $parts[0] / (float) $parts[1], 2); + } + + /** + * Normalize the EXIF white balance mode: 0 = auto, 1 = manual. + * Accepts the numeric code or the exiftool labels "Auto" and "Manual". + * + * @param mixed $value + * @return int|null null if the value is not recognised + */ + protected function normalizeWhiteBalance(mixed $value): ?int + { + if (is_string($value)) { + switch (strtolower($value)) { + case 'auto': + return 0; + case 'manual': + return 1; + } + } + + return $this->normalizeInt($value); + } } diff --git a/lib/PHPExif/Mapper/Exiftool.php b/lib/PHPExif/Mapper/Exiftool.php index f2e15a1..ead8420 100644 --- a/lib/PHPExif/Mapper/Exiftool.php +++ b/lib/PHPExif/Mapper/Exiftool.php @@ -37,6 +37,7 @@ class Exiftool extends AbstractMapper public const DATETIMEORIGINAL = 'ExifIFD:DateTimeOriginal'; public const CREDIT = 'IPTC:Credit'; public const EXPOSURETIME = 'ExifIFD:ExposureTime'; + public const EXPOSUREBIAS = 'ExifIFD:ExposureCompensation'; public const FILESIZE = 'System:FileSize'; public const FILENAME = 'System:FileName'; public const FOCALLENGTH = 'ExifIFD:FocalLength'; @@ -55,6 +56,7 @@ class Exiftool extends AbstractMapper public const TITLE_XMP = 'XMP-dc:Title'; public const XRESOLUTION = 'IFD0:XResolution'; public const YRESOLUTION = 'IFD0:YResolution'; + public const WHITEBALANCE = 'ExifIFD:WhiteBalance'; public const GPSLATITUDE = 'GPS:GPSLatitude'; public const GPSLONGITUDE = 'GPS:GPSLongitude'; public const GPSALTITUDE = 'GPS:GPSAltitude'; @@ -100,6 +102,29 @@ class Exiftool extends AbstractMapper public const DURATION_WMV = 'ASF:SendDuration'; public const DATETIMEORIGINAL_PNG = 'PNG:CreationTime'; + // White balance colour temperature (Kelvin), from the maker notes or Lightroom. + // Listed in priority order: measured / as-shot values first, user settings last. + public const WHITEBALANCETEMPERATURE_CANON_ASSHOT = 'Canon:ColorTempAsShot'; + public const WHITEBALANCETEMPERATURE_APPLE = 'Apple:ColorTemperature'; + public const WHITEBALANCETEMPERATURE_SONY = 'Sony:ColorTemperature'; + public const WHITEBALANCETEMPERATURE_FUJIFILM = 'FujiFilm:ColorTemperature'; + public const WHITEBALANCETEMPERATURE_PANASONIC = 'Panasonic:ColorTempKelvin'; + public const WHITEBALANCETEMPERATURE_PENTAX = 'Pentax:ColorTemperature'; + public const WHITEBALANCETEMPERATURE_CANON = 'Canon:ColorTemperature'; + public const WHITEBALANCETEMPERATURE_LIGHTROOM = 'XMP-crs:ColorTemperature'; + + public const WHITEBALANCETEMPERATURE_MIN = 1000; + public const WHITEBALANCETEMPERATURE_MAX = 50000; + + public const PROJECTIONTYPE = 'XMP-GPano:ProjectionType'; + public const USEPANORAMAVIEWER = 'XMP-GPano:UsePanoramaViewer'; + public const FULLPANOWIDTHPIXELS = 'XMP-GPano:FullPanoWidthPixels'; + public const FULLPANOHEIGHTPIXELS = 'XMP-GPano:FullPanoHeightPixels'; + public const CROPPEDAREALEFTPIXELS = 'XMP-GPano:CroppedAreaLeftPixels'; + public const CROPPEDAREATOPPIXELS = 'XMP-GPano:CroppedAreaTopPixels'; + public const CROPPEDAREAIMAGEWIDTHPIXELS = 'XMP-GPano:CroppedAreaImageWidthPixels'; + public const CROPPEDAREAIMAGEHEIGHTPIXELS = 'XMP-GPano:CroppedAreaImageHeightPixels'; + /** * Maps the ExifTool fields to the fields of * the \PHPExif\Exif class @@ -114,6 +139,7 @@ class Exiftool extends AbstractMapper self::DATETIMEORIGINAL => Exif::CREATION_DATE, self::CREDIT => Exif::CREDIT, self::EXPOSURETIME => Exif::EXPOSURE, + self::EXPOSUREBIAS => Exif::EXPOSURE_BIAS, self::FILESIZE => Exif::FILESIZE, self::FILENAME => Exif::FILENAME, self::FOCALLENGTH => Exif::FOCAL_LENGTH, @@ -132,6 +158,15 @@ class Exiftool extends AbstractMapper self::TITLE_XMP => Exif::TITLE, self::YRESOLUTION => Exif::VERTICAL_RESOLUTION, self::IMAGEWIDTH => Exif::WIDTH, + self::WHITEBALANCE => Exif::WHITE_BALANCE, + self::WHITEBALANCETEMPERATURE_CANON_ASSHOT => Exif::WHITE_BALANCE_TEMPERATURE, + self::WHITEBALANCETEMPERATURE_APPLE => Exif::WHITE_BALANCE_TEMPERATURE, + self::WHITEBALANCETEMPERATURE_SONY => Exif::WHITE_BALANCE_TEMPERATURE, + self::WHITEBALANCETEMPERATURE_FUJIFILM => Exif::WHITE_BALANCE_TEMPERATURE, + self::WHITEBALANCETEMPERATURE_PANASONIC => Exif::WHITE_BALANCE_TEMPERATURE, + self::WHITEBALANCETEMPERATURE_PENTAX => Exif::WHITE_BALANCE_TEMPERATURE, + self::WHITEBALANCETEMPERATURE_CANON => Exif::WHITE_BALANCE_TEMPERATURE, + self::WHITEBALANCETEMPERATURE_LIGHTROOM => Exif::WHITE_BALANCE_TEMPERATURE, self::CAPTIONABSTRACT => Exif::CAPTION, self::GPSLATITUDE => Exif::LATITUDE, self::GPSLONGITUDE => Exif::LONGITUDE, @@ -175,7 +210,29 @@ class Exiftool extends AbstractMapper self::CITY => Exif::CITY, self::STATE => Exif::STATE, self::COUNTRY => Exif::COUNTRY, - self::DATETIMEORIGINAL_PNG => Exif::CREATION_DATE + self::DATETIMEORIGINAL_PNG => Exif::CREATION_DATE, + self::PROJECTIONTYPE => Exif::PROJECTIONTYPE, + self::USEPANORAMAVIEWER => Exif::USEPANORAMAVIEWER, + self::FULLPANOWIDTHPIXELS => Exif::FULLPANOWIDTHPIXELS, + self::FULLPANOHEIGHTPIXELS => Exif::FULLPANOHEIGHTPIXELS, + self::CROPPEDAREALEFTPIXELS => Exif::CROPPEDAREALEFTPIXELS, + self::CROPPEDAREATOPPIXELS => Exif::CROPPEDAREATOPPIXELS, + self::CROPPEDAREAIMAGEWIDTHPIXELS => Exif::CROPPEDAREAIMAGEWIDTHPIXELS, + self::CROPPEDAREAIMAGEHEIGHTPIXELS => Exif::CROPPEDAREAIMAGEHEIGHTPIXELS + ); + + /** + * Tags holding the white balance colour temperature, in priority order + */ + protected array $whiteBalanceTemperatureTags = array( + self::WHITEBALANCETEMPERATURE_CANON_ASSHOT, + self::WHITEBALANCETEMPERATURE_APPLE, + self::WHITEBALANCETEMPERATURE_SONY, + self::WHITEBALANCETEMPERATURE_FUJIFILM, + self::WHITEBALANCETEMPERATURE_PANASONIC, + self::WHITEBALANCETEMPERATURE_PENTAX, + self::WHITEBALANCETEMPERATURE_CANON, + self::WHITEBALANCETEMPERATURE_LIGHTROOM, ); protected bool $numeric = true; @@ -386,6 +443,58 @@ public function mapRawData(array $data): array $mappedData[Exif::LENS] = $value; } continue 2; + case self::EXPOSUREBIAS: + $value = $this->normalizeExposureBias($value); + if ($value === null) { + continue 2; + } + break; + case self::WHITEBALANCE: + $value = $this->normalizeWhiteBalance($value); + if ($value === null) { + continue 2; + } + break; + case self::WHITEBALANCETEMPERATURE_CANON_ASSHOT: + case self::WHITEBALANCETEMPERATURE_APPLE: + case self::WHITEBALANCETEMPERATURE_SONY: + case self::WHITEBALANCETEMPERATURE_FUJIFILM: + case self::WHITEBALANCETEMPERATURE_PANASONIC: + case self::WHITEBALANCETEMPERATURE_PENTAX: + case self::WHITEBALANCETEMPERATURE_CANON: + case self::WHITEBALANCETEMPERATURE_LIGHTROOM: + // resolved once from all available tags, by priority + if (array_key_exists(Exif::WHITE_BALANCE_TEMPERATURE, $mappedData)) { + continue 2; + } + $value = $this->extractWhiteBalanceTemperature($data); + if ($value === null) { + continue 2; + } + break; + case self::PROJECTIONTYPE: + if (!is_string($value) || $value === '') { + continue 2; + } + $value = strtolower($value); + break; + case self::USEPANORAMAVIEWER: + $value = $this->normalizeBool($value); + if ($value === null) { + continue 2; + } + break; + case self::FULLPANOWIDTHPIXELS: + case self::FULLPANOHEIGHTPIXELS: + case self::CROPPEDAREALEFTPIXELS: + case self::CROPPEDAREATOPPIXELS: + case self::CROPPEDAREAIMAGEWIDTHPIXELS: + case self::CROPPEDAREAIMAGEHEIGHTPIXELS: + $value = $this->normalizeInt($value); + if ($value === null) { + continue 2; + } + break; } // set end result $mappedData[$key] = $value; @@ -400,6 +509,32 @@ public function mapRawData(array $data): array return $mappedData; } + /** + * Returns the first valid colour temperature (Kelvin) among the known tags, + * in priority order. Values outside a plausible Kelvin range are skipped: + * this drops Sony's 0 ("Auto") and 0xffffffff ("n/a"), and the text + * exiftool prints for them without -n. + * + * @param array $data + * @return int|null + */ + protected function extractWhiteBalanceTemperature(array $data): ?int + { + foreach ($this->whiteBalanceTemperatureTags as $tag) { + if (!array_key_exists($tag, $data)) { + continue; + } + $kelvin = $this->normalizeInt($this->trim($data[$tag])); + if ($kelvin !== null + && $kelvin >= self::WHITEBALANCETEMPERATURE_MIN + && $kelvin <= self::WHITEBALANCETEMPERATURE_MAX) { + return $kelvin; + } + } + + return null; + } + /** * Extract GPS coordinates from formatted string * diff --git a/lib/PHPExif/Mapper/ImageMagick.php b/lib/PHPExif/Mapper/ImageMagick.php index bf0f60f..018acf4 100644 --- a/lib/PHPExif/Mapper/ImageMagick.php +++ b/lib/PHPExif/Mapper/ImageMagick.php @@ -26,6 +26,7 @@ class ImageMagick extends AbstractMapper public const DATETIMEORIGINAL = 'exif:DateTimeOriginal'; public const DESCRIPTION = 'exif:ImageDescription'; public const EXPOSURETIME = 'exif:ExposureTime'; + public const EXPOSUREBIAS = 'exif:ExposureBiasValue'; public const FILESIZE = 'filesize'; public const FILENAME = 'filename'; public const FOCALLENGTH = 'exif:FocalLength'; @@ -48,6 +49,7 @@ class ImageMagick extends AbstractMapper public const SOFTWARE = 'exif:Software'; public const XRESOLUTION = 'exif:XResolution'; public const YRESOLUTION = 'exif:YResolution'; + public const WHITEBALANCE = 'exif:WhiteBalance'; public const TITLE = 'iptc:title'; public const KEYWORDS = 'iptc:keywords'; public const COPYRIGHT_IPTC = 'iptc:copyright'; @@ -61,6 +63,15 @@ class ImageMagick extends AbstractMapper public const STATE = 'iptc:state'; public const COUNTRY = 'iptc:country'; + public const PROJECTIONTYPE = 'GPano:ProjectionType'; + public const USEPANORAMAVIEWER = 'GPano:UsePanoramaViewer'; + public const FULLPANOWIDTHPIXELS = 'GPano:FullPanoWidthPixels'; + public const FULLPANOHEIGHTPIXELS = 'GPano:FullPanoHeightPixels'; + public const CROPPEDAREALEFTPIXELS = 'GPano:CroppedAreaLeftPixels'; + public const CROPPEDAREATOPPIXELS = 'GPano:CroppedAreaTopPixels'; + public const CROPPEDAREAIMAGEWIDTHPIXELS = 'GPano:CroppedAreaImageWidthPixels'; + public const CROPPEDAREAIMAGEHEIGHTPIXELS = 'GPano:CroppedAreaImageHeightPixels'; + /** * Maps the ExifTool fields to the fields of @@ -76,6 +87,7 @@ class ImageMagick extends AbstractMapper self::DATETIMEORIGINAL => Exif::CREATION_DATE, self::DESCRIPTION => Exif::DESCRIPTION, self::EXPOSURETIME => Exif::EXPOSURE, + self::EXPOSUREBIAS => Exif::EXPOSURE_BIAS, self::FILESIZE => Exif::FILESIZE, self::FILENAME => Exif::FILENAME, self::FOCALLENGTH => Exif::FOCAL_LENGTH, @@ -98,6 +110,7 @@ class ImageMagick extends AbstractMapper self::SOFTWARE => Exif::SOFTWARE, self::XRESOLUTION => Exif::HORIZONTAL_RESOLUTION, self::YRESOLUTION => Exif::VERTICAL_RESOLUTION, + self::WHITEBALANCE => Exif::WHITE_BALANCE, self::TITLE => Exif::TITLE, self::KEYWORDS => Exif::KEYWORDS, self::COPYRIGHT_IPTC => Exif::COPYRIGHT, @@ -109,7 +122,15 @@ class ImageMagick extends AbstractMapper self::CITY => Exif::CITY, self::SUBLOCATION => Exif::SUBLOCATION, self::STATE => Exif::STATE, - self::COUNTRY => Exif::COUNTRY + self::COUNTRY => Exif::COUNTRY, + self::PROJECTIONTYPE => Exif::PROJECTIONTYPE, + self::USEPANORAMAVIEWER => Exif::USEPANORAMAVIEWER, + self::FULLPANOWIDTHPIXELS => Exif::FULLPANOWIDTHPIXELS, + self::FULLPANOHEIGHTPIXELS => Exif::FULLPANOHEIGHTPIXELS, + self::CROPPEDAREALEFTPIXELS => Exif::CROPPEDAREALEFTPIXELS, + self::CROPPEDAREATOPPIXELS => Exif::CROPPEDAREATOPPIXELS, + self::CROPPEDAREAIMAGEWIDTHPIXELS => Exif::CROPPEDAREAIMAGEWIDTHPIXELS, + self::CROPPEDAREAIMAGEHEIGHTPIXELS => Exif::CROPPEDAREAIMAGEHEIGHTPIXELS ); @@ -250,6 +271,41 @@ public function mapRawData(array $data): array $value = [$value]; } break; + case self::EXPOSUREBIAS: + $value = $this->normalizeExposureBias($value); + if ($value === null) { + continue 2; + } + break; + case self::WHITEBALANCE: + $value = $this->normalizeWhiteBalance($value); + if ($value === null) { + continue 2; + } + break; + case self::PROJECTIONTYPE: + if (!is_string($value) || $value === '') { + continue 2; + } + $value = strtolower($value); + break; + case self::USEPANORAMAVIEWER: + $value = $this->normalizeBool($value); + if ($value === null) { + continue 2; + } + break; + case self::FULLPANOWIDTHPIXELS: + case self::FULLPANOHEIGHTPIXELS: + case self::CROPPEDAREALEFTPIXELS: + case self::CROPPEDAREATOPPIXELS: + case self::CROPPEDAREAIMAGEWIDTHPIXELS: + case self::CROPPEDAREAIMAGEHEIGHTPIXELS: + $value = $this->normalizeInt($value); + if ($value === null) { + continue 2; + } + break; } // set end result $mappedData[$key] = $value; diff --git a/lib/PHPExif/Mapper/Native.php b/lib/PHPExif/Mapper/Native.php index 1f51f82..0c53240 100644 --- a/lib/PHPExif/Mapper/Native.php +++ b/lib/PHPExif/Mapper/Native.php @@ -26,6 +26,7 @@ class Native extends AbstractMapper public const DATETIMEORIGINAL = 'DateTimeOriginal'; public const CREDIT = 'credit'; public const EXPOSURETIME = 'ExposureTime'; + public const EXPOSUREBIAS = 'ExposureBiasValue'; public const FILESIZE = 'FileSize'; public const FILENAME = 'FileName'; public const FOCALLENGTH = 'FocalLength'; @@ -41,6 +42,7 @@ class Native extends AbstractMapper public const SOFTWARE = 'Software'; public const SOURCE = 'source'; public const TITLE = 'title'; + public const WHITEBALANCE = 'WhiteBalance'; public const WIDTH = 'Width'; public const XRESOLUTION = 'XResolution'; public const YRESOLUTION = 'YResolution'; @@ -110,6 +112,7 @@ class Native extends AbstractMapper self::COLORSPACE => Exif::COLORSPACE, self::DATETIMEORIGINAL => Exif::CREATION_DATE, self::EXPOSURETIME => Exif::EXPOSURE, + self::EXPOSUREBIAS => Exif::EXPOSURE_BIAS, self::FILESIZE => Exif::FILESIZE, self::FILENAME => Exif::FILENAME, self::FOCALLENGTH => Exif::FOCAL_LENGTH, @@ -119,6 +122,7 @@ class Native extends AbstractMapper self::SOFTWARE => Exif::SOFTWARE, self::XRESOLUTION => Exif::HORIZONTAL_RESOLUTION, self::YRESOLUTION => Exif::VERTICAL_RESOLUTION, + self::WHITEBALANCE => Exif::WHITE_BALANCE, self::GPSLATITUDE => Exif::LATITUDE, self::GPSLONGITUDE => Exif::LONGITUDE, self::GPSALTITUDE => Exif::ALTITUDE, @@ -213,6 +217,18 @@ public function mapRawData(array $data): array $value = preg_replace('/.0$/', '', $value); } break; + case self::EXPOSUREBIAS: + $value = $this->normalizeExposureBias($value); + if ($value === null) { + continue 2; + } + break; + case self::WHITEBALANCE: + $value = $this->normalizeWhiteBalance($value); + if ($value === null) { + continue 2; + } + break; case self::FOCALLENGTH: $parts = explode('/', $value); // Avoid division by zero if focal length is invalid diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c6794d3..6ba4200 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,6 @@ - + - - ./lib - - - ./autoload.php - *Interface.php - @@ -20,4 +13,13 @@ + + + ./lib + + + ./autoload.php + *Interface.php + + diff --git a/tests/PHPExif/Adapter/AdapterAbstractTest.php b/tests/PHPExif/Adapter/AbstractAdapterTest.php similarity index 90% rename from tests/PHPExif/Adapter/AdapterAbstractTest.php rename to tests/PHPExif/Adapter/AbstractAdapterTest.php index 8d03417..a8b1e16 100644 --- a/tests/PHPExif/Adapter/AdapterAbstractTest.php +++ b/tests/PHPExif/Adapter/AbstractAdapterTest.php @@ -3,6 +3,7 @@ use PHPExif\Adapter\AbstractAdapter; use PHPExif\Adapter\Exiftool; use PHPExif\Adapter\Native; +use PHPUnit\Framework\Attributes\Group; class AbstractAdapterTest extends PHPUnit\Framework\TestCase { @@ -16,18 +17,14 @@ protected function setUp(): void $this->adapter = new Native(); } - /** - * @group adapter - */ + #[Group('adapter')] public function testSetOptionsReturnsCurrentInstance() { $result = $this->adapter->setOptions([]); $this->assertSame($this->adapter, $result); } - /** - * @group adapter - */ + #[Group('adapter')] public function testSetOptionsCorrectlySetsProperties() { $expected = array( @@ -43,9 +40,7 @@ public function testSetOptionsCorrectlySetsProperties() } } - /** - * @group adapter - */ + #[Group('adapter')] public function testSetOptionsIgnoresPropertiesWithoutSetters() { $expected = array( @@ -60,9 +55,7 @@ public function testSetOptionsIgnoresPropertiesWithoutSetters() } - /** - * @group adapter - */ + #[Group('adapter')] public function testConstructorSetsOptions() { $expected = array( @@ -78,9 +71,7 @@ public function testConstructorSetsOptions() } } - /** - * @group adapter - */ + #[Group('adapter')] public function testSetMapperReturnsCurrentInstance() { $mapper = new \PHPExif\Mapper\Native(); @@ -88,9 +79,7 @@ public function testSetMapperReturnsCurrentInstance() $this->assertSame($this->adapter, $result); } - /** - * @group adapter - */ + #[Group('adapter')] public function testSetMapperCorrectlySetsInProperty() { $mapper = new \PHPExif\Mapper\Native(); @@ -100,9 +89,7 @@ public function testSetMapperCorrectlySetsInProperty() $this->assertSame($mapper, $reflProp->getValue($this->adapter)); } - /** - * @group adapter - */ + #[Group('adapter')] public function testGetMapperCorrectlyReturnsFromProperty() { $mapper = new \PHPExif\Mapper\Native(); @@ -111,9 +98,7 @@ public function testGetMapperCorrectlyReturnsFromProperty() $this->assertSame($mapper, $this->adapter->getMapper()); } - /** - * @group adapter - */ + #[Group('adapter')] public function testGetMapperLazyLoadsMapperWhenNotPresent() { $reflProp = new \ReflectionProperty( @@ -127,9 +112,7 @@ public function testGetMapperLazyLoadsMapperWhenNotPresent() $this->assertInstanceOf($mapperClass, $this->adapter->getMapper()); } - /** - * @group adapter - */ + #[Group('adapter')] public function testGetMapperLazyLoadingSetsInProperty() { $reflProp = new \ReflectionProperty( @@ -148,9 +131,7 @@ public function testGetMapperLazyLoadingSetsInProperty() $this->assertInstanceOf($mapperClass, $reflProp2->getValue($this->adapter)); } - /** - * @group adapter - */ + #[Group('adapter')] public function testSetHydratorReturnsCurrentInstance() { $hydrator = new \PHPExif\Hydrator\Mutator(); @@ -158,9 +139,7 @@ public function testSetHydratorReturnsCurrentInstance() $this->assertSame($this->adapter, $result); } - /** - * @group adapter - */ + #[Group('adapter')] public function testSetHydratorCorrectlySetsInProperty() { $hydrator = new \PHPExif\Hydrator\Mutator(); @@ -170,9 +149,7 @@ public function testSetHydratorCorrectlySetsInProperty() $this->assertSame($hydrator, $reflProp->getValue($this->adapter)); } - /** - * @group adapter - */ + #[Group('adapter')] public function testGetHydratorCorrectlyReturnsFromProperty() { $hydrator = new \PHPExif\Hydrator\Mutator(); @@ -181,18 +158,14 @@ public function testGetHydratorCorrectlyReturnsFromProperty() $this->assertSame($hydrator, $this->adapter->getHydrator()); } - /** - * @group adapter - */ + #[Group('adapter')] public function testGetHydratorLazyLoadsHydratorWhenNotPresent() { $hydratorClass = '\\PHPExif\\Hydrator\\Mutator'; $this->assertInstanceOf($hydratorClass, $this->adapter->getHydrator()); } - /** - * @group adapter - */ + #[Group('adapter')] public function testGetHydratorLazyLoadingSetsInProperty() { $hydratorClass = '\\PHPExif\\Hydrator\\Mutator'; diff --git a/tests/PHPExif/Adapter/ExiftoolProcOpenTest.php b/tests/PHPExif/Adapter/ExiftoolProcOpenTest.php index c5cc813..a5d4141 100644 --- a/tests/PHPExif/Adapter/ExiftoolProcOpenTest.php +++ b/tests/PHPExif/Adapter/ExiftoolProcOpenTest.php @@ -14,6 +14,7 @@ // stub the function use PHPExif\Reader\PhpExifReaderException; + use PHPUnit\Framework\Attributes\Group; function proc_open($cmd, array $descriptorspec, &$pipes = []) { @@ -48,9 +49,7 @@ public function tearDown(): void $mockProcOpen = false; } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testGetCliOutput() { $this->expectException(PhpExifReaderException::class); diff --git a/tests/PHPExif/Adapter/ExiftoolTest.php b/tests/PHPExif/Adapter/ExiftoolTest.php index 53b603b..8d606ce 100644 --- a/tests/PHPExif/Adapter/ExiftoolTest.php +++ b/tests/PHPExif/Adapter/ExiftoolTest.php @@ -1,6 +1,7 @@ adapter = new Exiftool(); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testGetToolPathFromProperty() { $reflProperty = new \ReflectionProperty(Exiftool::class, 'toolPath'); @@ -23,9 +22,7 @@ public function testGetToolPathFromProperty() $this->assertEquals($expected, $this->adapter->getToolPath()); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testSetToolPathInProperty() { $reflProperty = new \ReflectionProperty(Exiftool::class, 'toolPath'); @@ -36,9 +33,7 @@ public function testSetToolPathInProperty() $this->assertEquals($expected, $reflProperty->getValue($this->adapter)); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testSetToolPathThrowsException() { $this->expectException('InvalidArgumentException'); @@ -46,17 +41,13 @@ public function testSetToolPathThrowsException() } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testGetToolPathLazyLoadsPath() { $this->assertIsString($this->adapter->getToolPath()); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testSetNumericInProperty() { $reflProperty = new \ReflectionProperty(Exiftool::class, 'numeric'); @@ -69,8 +60,8 @@ public function testSetNumericInProperty() /** * @see URI http://www.sno.phy.queensu.ca/~phil/exiftool/faq.html#Q10 - * @group exiftool */ + #[Group('exiftool')] public function testSetEncodingInProperty() { $reflProperty = new \ReflectionProperty(Exiftool::class, 'encoding'); @@ -82,9 +73,7 @@ public function testSetEncodingInProperty() $this->assertEquals($expected, $reflProperty->getValue($this->adapter)); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testGetExifFromFile() { $file = PHPEXIF_TEST_ROOT . '/files/morning_glory_pool_500.jpg'; @@ -95,9 +84,7 @@ public function testGetExifFromFile() $this->assertNotEmpty($result->getRawData()); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testGetExifFromFileWithUtf8() { $file = PHPEXIF_TEST_ROOT . '/files/utf8.jpg'; @@ -108,9 +95,7 @@ public function testGetExifFromFileWithUtf8() $this->assertNotEmpty($result->getRawData()); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testGetExifFromFileWithAvif() { $file = PHPEXIF_TEST_ROOT . '/files/fox.profile0.10bpc.yuv420.avif'; @@ -121,9 +106,7 @@ public function testGetExifFromFileWithAvif() $this->assertNotEmpty($result->getRawData()); } - /** - * @group exiftool - */ + #[Group('exiftool')] public function testGetCliOutput() { $reflMethod = new \ReflectionMethod(Exiftool::class, 'getCliOutput'); @@ -138,4 +121,58 @@ public function testGetCliOutput() $this->assertIsString($result); } + + /** + * Reads the XMP-GPano tags of a photo sphere. ImageMagickTest asserts the + * same values, so both adapters return identical getter values. + */ + #[Group('exiftool')] + public function testGetPanoramaDataFromFile() + { + if ($this->adapter->getToolPath() === '') { + $this->markTestSkipped('exiftool is not available.'); + } + $file = PHPEXIF_TEST_ROOT . '/files/gpano.jpg'; + $result = $this->adapter->getExifFromFile($file); + + $this->assertSame('equirectangular', $result->getProjectionType()); + $this->assertSame(true, $result->getUsePanoramaViewer()); + $this->assertSame(8000, $result->getFullPanoWidthPixels()); + $this->assertSame(4000, $result->getFullPanoHeightPixels()); + $this->assertSame(1000, $result->getCroppedAreaLeftPixels()); + $this->assertSame(1000, $result->getCroppedAreaTopPixels()); + $this->assertSame(6000, $result->getCroppedAreaImageWidthPixels()); + $this->assertSame(2000, $result->getCroppedAreaImageHeightPixels()); + } + + /** + * gpano.jpg carries ExposureCompensation -2/3 EV and manual white balance. + * The Native, Exiftool and ImageMagick adapter tests assert the same values. + */ + #[Group('exiftool')] + public function testGetExposureBiasAndWhiteBalanceFromFile() + { + if ($this->adapter->getToolPath() === '') { + $this->markTestSkipped('exiftool is not available.'); + } + $result = $this->adapter->getExifFromFile(PHPEXIF_TEST_ROOT . '/files/gpano.jpg'); + + $this->assertSame(-0.67, $result->getExposureBias()); + $this->assertSame(1, $result->getWhiteBalance()); + } + + /** + * gpano.jpg carries XMP-crs:ColorTemperature 5500. + * Only the exiftool adapter reads the colour temperature. + */ + #[Group('exiftool')] + public function testGetWhiteBalanceTemperatureFromFile() + { + if ($this->adapter->getToolPath() === '') { + $this->markTestSkipped('exiftool is not available.'); + } + $result = $this->adapter->getExifFromFile(PHPEXIF_TEST_ROOT . '/files/gpano.jpg'); + + $this->assertSame(5500, $result->getWhiteBalanceTemperature()); + } } diff --git a/tests/PHPExif/Adapter/FFprobeTest.php b/tests/PHPExif/Adapter/FFprobeTest.php index 146fb61..50b1068 100644 --- a/tests/PHPExif/Adapter/FFprobeTest.php +++ b/tests/PHPExif/Adapter/FFprobeTest.php @@ -3,6 +3,7 @@ use PHPExif\Adapter\FFprobe; use PHPExif\Exif; use PHPExif\Reader\PhpExifReaderException; +use PHPUnit\Framework\Attributes\Group; class FFprobeTest extends \PHPUnit\Framework\TestCase { @@ -17,9 +18,7 @@ public function setUp(): void } - /** - * @group ffprobe - */ + #[Group('ffprobe')] public function testGetToolPathFromProperty() { $reflProperty = new \ReflectionProperty(FFprobe::class, 'toolPath'); @@ -29,9 +28,7 @@ public function testGetToolPathFromProperty() $this->assertEquals($expected, $this->adapter->getToolPath()); } - /** - * @group ffprobe - */ + #[Group('ffprobe')] public function testSetToolPathInProperty() { $reflProperty = new \ReflectionProperty(FFprobe::class, 'toolPath'); @@ -42,26 +39,20 @@ public function testSetToolPathInProperty() $this->assertEquals($expected, $reflProperty->getValue($this->adapter)); } - /** - * @group ffprobe - */ + #[Group('ffprobe')] public function testSetToolPathThrowsException() { $this->expectException('InvalidArgumentException'); $this->adapter->setToolPath('/foo/bar'); } - /** - * @group ffprobe - */ + #[Group('ffprobe')] public function testGetToolPathLazyLoadsPath() { $this->assertIsString($this->adapter->getToolPath()); } - /** - * @group ffprobe - */ + #[Group('ffprobe')] public function testGetExifFromFileHasData() { $file = PHPEXIF_TEST_ROOT . '/files/IMG_3824.MOV'; @@ -77,9 +68,7 @@ public function testGetExifFromFileHasData() $this->assertNotEmpty($result->getRawData()); } - /** - * @group ffprobe - */ + #[Group('ffprobe')] public function testErrorImageUsed() { $file = PHPEXIF_TEST_ROOT . '/files/morning_glory_pool_500.jpg'; diff --git a/tests/PHPExif/Adapter/ImageMagickTest.php b/tests/PHPExif/Adapter/ImageMagickTest.php index f864712..d867080 100644 --- a/tests/PHPExif/Adapter/ImageMagickTest.php +++ b/tests/PHPExif/Adapter/ImageMagickTest.php @@ -2,6 +2,7 @@ use PHPExif\Adapter\ImageMagick; use PHPExif\Exif; +use PHPUnit\Framework\Attributes\Group; class ImageMagickTest extends \PHPUnit\Framework\TestCase { @@ -15,9 +16,7 @@ public function setUp(): void $this->adapter = new ImageMagick(); } - /** - * @group ImageMagick - */ + #[Group('ImageMagick')] public function testGetExifFromFile() { $file = PHPEXIF_TEST_ROOT . '/files/morning_glory_pool_500.jpg'; @@ -27,9 +26,7 @@ public function testGetExifFromFile() $this->assertNotEmpty($result->getRawData()); } - /** - * @group ImageMagick - */ + #[Group('ImageMagick')] public function testGetEmptyIptcData() { $result = $this->adapter->getIptcData(""); @@ -37,4 +34,57 @@ public function testGetEmptyIptcData() $this->assertEquals([], $result); } + /** + * Reads the XMP-GPano tags of a photo sphere. ExiftoolTest asserts the + * same values, so both adapters return identical getter values. + */ + #[Group('ImageMagick')] + public function testGetPanoramaDataFromFile() + { + if (!extension_loaded('imagick')) { + $this->markTestSkipped('The imagick extension is not available.'); + } + $file = PHPEXIF_TEST_ROOT . '/files/gpano.jpg'; + $result = $this->adapter->getExifFromFile($file); + + $this->assertSame('equirectangular', $result->getProjectionType()); + $this->assertSame(true, $result->getUsePanoramaViewer()); + $this->assertSame(8000, $result->getFullPanoWidthPixels()); + $this->assertSame(4000, $result->getFullPanoHeightPixels()); + $this->assertSame(1000, $result->getCroppedAreaLeftPixels()); + $this->assertSame(1000, $result->getCroppedAreaTopPixels()); + $this->assertSame(6000, $result->getCroppedAreaImageWidthPixels()); + $this->assertSame(2000, $result->getCroppedAreaImageHeightPixels()); + } + + /** + * gpano.jpg carries ExposureCompensation -2/3 EV and manual white balance. + * The Native, Exiftool and ImageMagick adapter tests assert the same values. + */ + #[Group('ImageMagick')] + public function testGetExposureBiasAndWhiteBalanceFromFile() + { + if (!extension_loaded('imagick')) { + $this->markTestSkipped('The imagick extension is not available.'); + } + $result = $this->adapter->getExifFromFile(PHPEXIF_TEST_ROOT . '/files/gpano.jpg'); + + $this->assertSame(-0.67, $result->getExposureBias()); + $this->assertSame(1, $result->getWhiteBalance()); + } + + /** + * The colour temperature is only read by the exiftool adapter, + * even though gpano.jpg carries XMP-crs:ColorTemperature 5500. + */ + #[Group('ImageMagick')] + public function testGetWhiteBalanceTemperatureIsNotAvailable() + { + if (!extension_loaded('imagick')) { + $this->markTestSkipped('The imagick extension is not available.'); + } + $result = $this->adapter->getExifFromFile(PHPEXIF_TEST_ROOT . '/files/gpano.jpg'); + + $this->assertFalse($result->getWhiteBalanceTemperature()); + } } diff --git a/tests/PHPExif/Adapter/NativeTest.php b/tests/PHPExif/Adapter/NativeTest.php index aa211e8..1dee127 100644 --- a/tests/PHPExif/Adapter/NativeTest.php +++ b/tests/PHPExif/Adapter/NativeTest.php @@ -1,6 +1,7 @@ adapter = new Native(); } - /** - * @group native - */ + #[Group('native')] public function testSetIncludeThumbnailInProperty() { $reflProperty = new \ReflectionProperty(Native::class, 'includeThumbnail'); @@ -28,9 +27,7 @@ public function testSetIncludeThumbnailInProperty() $this->assertEquals(Native::INCLUDE_THUMBNAIL, $reflProperty->getValue($this->adapter)); } - /** - * @group native - */ + #[Group('native')] public function testGetIncludeThumbnailFromProperty() { $reflProperty = new \ReflectionProperty(Native::class, 'includeThumbnail'); @@ -39,17 +36,13 @@ public function testGetIncludeThumbnailFromProperty() $this->assertEquals(Native::INCLUDE_THUMBNAIL, $this->adapter->getIncludeThumbnail()); } - /** - * @group native - */ + #[Group('native')] public function testGetIncludeThumbnailHasDefaultValue() { $this->assertEquals(Native::NO_THUMBNAIL, $this->adapter->getIncludeThumbnail()); } - /** - * @group native - */ + #[Group('native')] public function testGetRequiredSections() { $reflProperty = new \ReflectionProperty(Native::class, 'requiredSections'); @@ -57,9 +50,7 @@ public function testGetRequiredSections() $this->assertEquals($reflProperty->getValue($this->adapter), $this->adapter->getRequiredSections()); } - /** - * @group native - */ + #[Group('native')] public function testSetRequiredSections() { $reflProperty = new \ReflectionProperty(Native::class, 'requiredSections'); @@ -72,9 +63,7 @@ public function testSetRequiredSections() $this->assertEquals($this->adapter, $returnValue); } - /** - * @group native - */ + #[Group('native')] public function testAddRequiredSection() { $reflProperty = new \ReflectionProperty(Native::class, 'requiredSections'); @@ -89,9 +78,7 @@ public function testAddRequiredSection() $this->assertEquals($this->adapter, $returnValue); } - /** - * @group native - */ + #[Group('native')] public function testGetExifFromFileNoData() { $file = PHPEXIF_TEST_ROOT . '/files/empty.jpg'; @@ -102,9 +89,7 @@ public function testGetExifFromFileNoData() $this->assertEquals($expected, $result->getRawData()); } - /** - * @group native - */ + #[Group('native')] public function testGetExifFromFileHasData() { $file = PHPEXIF_TEST_ROOT . '/files/morning_glory_pool_500.jpg'; @@ -114,9 +99,7 @@ public function testGetExifFromFileHasData() $this->assertNotEmpty($result->getRawData()); } - /** - * @group native - */ + #[Group('native')] public function testGetIptcData() { $file = PHPEXIF_TEST_ROOT . '/files/morning_glory_pool_500.jpg'; @@ -131,9 +114,7 @@ public function testGetIptcData() $this->assertEquals($expected, $result); } - /** - * @group native - */ + #[Group('native')] public function testGetEmptyIptcData() { $file = PHPEXIF_TEST_ROOT . '/files/empty_iptc.jpg'; @@ -142,9 +123,7 @@ public function testGetEmptyIptcData() $this->assertEquals([], $result); } - /** - * @group native - */ + #[Group('native')] public function testSetSectionsAsArrayInProperty() { $reflProperty = new \ReflectionProperty(Native::class, 'sectionsAsArrays'); @@ -154,9 +133,7 @@ public function testSetSectionsAsArrayInProperty() $this->assertEquals($expected, $actual); } - /** - * @group native - */ + #[Group('native')] public function testSetSectionsAsArrayConvertsToBoolean() { $reflProperty = new \ReflectionProperty(Native::class, 'sectionsAsArrays'); @@ -166,9 +143,7 @@ public function testSetSectionsAsArrayConvertsToBoolean() $this->assertEquals($expected, $actual); } - /** - * @group native - */ + #[Group('native')] public function testGetSectionsAsArrayFromProperty() { $reflProperty = new \ReflectionProperty(Native::class, 'sectionsAsArrays'); @@ -176,4 +151,35 @@ public function testGetSectionsAsArrayFromProperty() $this->assertEquals(Native::SECTIONS_AS_ARRAYS, $this->adapter->getSectionsAsArrays()); } + + /** + * gpano.jpg carries ExposureCompensation -2/3 EV and manual white balance. + * The Native, Exiftool and ImageMagick adapter tests assert the same values. + */ + #[Group('native')] + public function testGetExposureBiasAndWhiteBalanceFromFile() + { + if (!extension_loaded('exif')) { + $this->markTestSkipped('The exif extension is not available.'); + } + $result = $this->adapter->getExifFromFile(PHPEXIF_TEST_ROOT . '/files/gpano.jpg'); + + $this->assertSame(-0.67, $result->getExposureBias()); + $this->assertSame(1, $result->getWhiteBalance()); + } + + /** + * The colour temperature is only read by the exiftool adapter, + * even though gpano.jpg carries XMP-crs:ColorTemperature 5500. + */ + #[Group('native')] + public function testGetWhiteBalanceTemperatureIsNotAvailable() + { + if (!extension_loaded('exif')) { + $this->markTestSkipped('The exif extension is not available.'); + } + $result = $this->adapter->getExifFromFile(PHPEXIF_TEST_ROOT . '/files/gpano.jpg'); + + $this->assertFalse($result->getWhiteBalanceTemperature()); + } } diff --git a/tests/PHPExif/ExifTest.php b/tests/PHPExif/ExifTest.php index ad920c7..a8a992e 100644 --- a/tests/PHPExif/ExifTest.php +++ b/tests/PHPExif/ExifTest.php @@ -1,6 +1,8 @@ exif = new Exif(); } - /** - * @group exif - */ + #[Group('exif')] public function testConstructorCallsSetData() { $input = []; @@ -42,9 +42,7 @@ public function testConstructorCallsSetData() $constructor->invoke($mock, $input); } - /** - * @group exif - */ + #[Group('exif')] public function testGetRawData() { $reflProperty = new \ReflectionProperty(Exif::class, 'rawData'); @@ -52,9 +50,7 @@ public function testGetRawData() $this->assertEquals($reflProperty->getValue($this->exif), $this->exif->getRawData()); } - /** - * @group exif - */ + #[Group('exif')] public function testSetRawData() { $testData = array('foo', 'bar', 'baz'); @@ -66,9 +62,7 @@ public function testSetRawData() $this->assertEquals($this->exif, $result); } - /** - * @group exif - */ + #[Group('exif')] public function testGetData() { $reflProperty = new \ReflectionProperty(Exif::class, 'data'); @@ -76,9 +70,7 @@ public function testGetData() $this->assertEquals($reflProperty->getValue($this->exif), $this->exif->getData()); } - /** - * @group exif - */ + #[Group('exif')] public function testSetData() { $testData = array('foo', 'bar', 'baz'); @@ -92,9 +84,9 @@ public function testSetData() /** * - * @dataProvider providerUndefinedPropertiesReturnFalse * @param string $accessor */ + #[DataProvider('providerUndefinedPropertiesReturnFalse')] public function testUndefinedPropertiesReturnFalse($accessor) { $expected = false; @@ -106,7 +98,7 @@ public function testUndefinedPropertiesReturnFalse($accessor) * * @return array */ - public function providerUndefinedPropertiesReturnFalse() + public static function providerUndefinedPropertiesReturnFalse() { return array( array('getAperture'), @@ -153,12 +145,29 @@ public function providerUndefinedPropertiesReturnFalse() array('getSublocation'), array('getState'), array('getCountry'), + array('getProjectionType'), + array('getFullPanoWidthPixels'), + array('getFullPanoHeightPixels'), + array('getCroppedAreaLeftPixels'), + array('getCroppedAreaTopPixels'), + array('getCroppedAreaImageWidthPixels'), + array('getCroppedAreaImageHeightPixels'), + array('getExposureBias'), + array('getWhiteBalance'), + array('getWhiteBalanceTemperature'), ); } /** - * @group exif + * UsePanoramaViewer distinguishes "absent" (null) from an explicit false */ + #[Group('exif')] + public function testUndefinedUsePanoramaViewerReturnsNull() + { + $this->assertNull($this->exif->getUsePanoramaViewer()); + } + + #[Group('exif')] public function testGetAperture() { $expected = 'f/8.0'; @@ -168,9 +177,7 @@ public function testGetAperture() $this->assertEquals($expected, $this->exif->getAperture()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetIso() { $expected = 200; @@ -179,9 +186,7 @@ public function testGetIso() $this->assertEquals($expected, $this->exif->getIso()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetExposure() { $expected = '1/320'; @@ -190,9 +195,7 @@ public function testGetExposure() $this->assertEquals($expected, $this->exif->getExposure()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetExposureMilliseconds() { $rawData = array( @@ -210,9 +213,7 @@ public function testGetExposureMilliseconds() } } - /** - * @group exif - */ + #[Group('exif')] public function testGetFocusDistance() { $expected = '7.94m'; @@ -221,9 +222,7 @@ public function testGetFocusDistance() $this->assertEquals($expected, $this->exif->getFocusDistance()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetWidth() { $expected = 500; @@ -232,9 +231,7 @@ public function testGetWidth() $this->assertEquals($expected, $this->exif->getWidth()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetHeight() { $expected = 332; @@ -243,9 +240,7 @@ public function testGetHeight() $this->assertEquals($expected, $this->exif->getHeight()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetTitle() { $expected = 'Morning Glory Pool'; @@ -254,9 +249,7 @@ public function testGetTitle() $this->assertEquals($expected, $this->exif->getTitle()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetCaption() { $expected = 'Foo Bar Baz'; @@ -265,9 +258,7 @@ public function testGetCaption() $this->assertEquals($expected, $this->exif->getCaption()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetCopyright() { $expected = 'Miljar'; @@ -276,9 +267,7 @@ public function testGetCopyright() $this->assertEquals($expected, $this->exif->getCopyright()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetKeywords() { $expected = array('18-200', 'D90', 'USA', 'Wyoming', 'Yellowstone'); @@ -287,9 +276,7 @@ public function testGetKeywords() $this->assertEquals($expected, $this->exif->getKeywords()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetCamera() { $expected = 'NIKON D90'; @@ -298,9 +285,7 @@ public function testGetCamera() $this->assertEquals($expected, $this->exif->getCamera()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetHorizontalResolution() { $expected = 240; @@ -309,9 +294,7 @@ public function testGetHorizontalResolution() $this->assertEquals($expected, $this->exif->getHorizontalResolution()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetVerticalResolution() { $expected = 240; @@ -320,9 +303,7 @@ public function testGetVerticalResolution() $this->assertEquals($expected, $this->exif->getVerticalResolution()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetSoftware() { $expected = 'Adobe Photoshop Lightroom'; @@ -331,9 +312,7 @@ public function testGetSoftware() $this->assertEquals($expected, $this->exif->getSoftware()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetFocalLength() { $expected = 18; @@ -342,9 +321,7 @@ public function testGetFocalLength() $this->assertEquals($expected, $this->exif->getFocalLength()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetCreationDate() { $expected = '2011-06-07 20:01:50'; @@ -354,9 +331,7 @@ public function testGetCreationDate() $this->assertEquals($expected, $this->exif->getCreationDate()->format('Y-m-d H:i:s')); } - /** - * @group exif - */ + #[Group('exif')] public function testGetAuthor() { $expected = 'John Smith'; @@ -365,9 +340,7 @@ public function testGetAuthor() $this->assertEquals($expected, $this->exif->getAuthor()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetHeadline() { $expected = 'Foobar Baz'; @@ -376,9 +349,7 @@ public function testGetHeadline() $this->assertEquals($expected, $this->exif->getHeadline()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetCredit() { $expected = 'john.smith@example.com'; @@ -387,9 +358,7 @@ public function testGetCredit() $this->assertEquals($expected, $this->exif->getCredit()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetSource() { $expected = 'FBB NEWS'; @@ -398,9 +367,7 @@ public function testGetSource() $this->assertEquals($expected, $this->exif->getSource()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetJobtitle() { $expected = 'Yellowstone\'s geysers and pools'; @@ -409,9 +376,7 @@ public function testGetJobtitle() $this->assertEquals($expected, $this->exif->getJobtitle()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetColorSpace() { $expected = 'RGB'; @@ -420,9 +385,7 @@ public function testGetColorSpace() $this->assertEquals($expected, $this->exif->getColorSpace()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetMimeType() { $expected = 'image/jpeg'; @@ -431,9 +394,7 @@ public function testGetMimeType() $this->assertEquals($expected, $this->exif->getMimeType()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetFileSize() { $expected = '27852365'; @@ -442,9 +403,7 @@ public function testGetFileSize() $this->assertEquals($expected, $this->exif->getFileSize()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetFileName() { $expected = '27852365.jpg'; @@ -453,9 +412,7 @@ public function testGetFileName() $this->assertEquals($expected, $this->exif->getFileName()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetOrientation() { $expected = 1; @@ -464,9 +421,7 @@ public function testGetOrientation() $this->assertEquals($expected, $this->exif->getOrientation()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetGPS() { $expected = '40.333452380556,-20.167314813889'; @@ -475,9 +430,7 @@ public function testGetGPS() $this->assertEquals($expected, $this->exif->getGPS()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetDescription() { $expected = 'Lorem ipsum'; @@ -486,9 +439,7 @@ public function testGetDescription() $this->assertEquals($expected, $this->exif->getDescription()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetMake() { $expected = 'Make'; @@ -497,9 +448,7 @@ public function testGetMake() $this->assertEquals($expected, $this->exif->getMake()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetAltitude() { $expected = '8848'; @@ -508,9 +457,7 @@ public function testGetAltitude() $this->assertEquals($expected, $this->exif->getAltitude()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetLatitude() { $expected = '40.333452380556'; @@ -519,9 +466,7 @@ public function testGetLatitude() $this->assertEquals($expected, $this->exif->getLatitude()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetLongitude() { $expected = '-20.167314813889'; @@ -530,9 +475,7 @@ public function testGetLongitude() $this->assertEquals($expected, $this->exif->getLongitude()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetImgDirection() { $expected = '180'; @@ -541,9 +484,7 @@ public function testGetImgDirection() $this->assertEquals($expected, $this->exif->getImgDirection()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetLens() { $expected = '70 - 200mm'; @@ -552,9 +493,7 @@ public function testGetLens() $this->assertEquals($expected, $this->exif->getLens()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetContentIdentifier() { $expected = 'C09DCB26-D321-4254-9F68-2E2E7FA16155'; @@ -563,9 +502,7 @@ public function testGetContentIdentifier() $this->assertEquals($expected, $this->exif->getContentIdentifier()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetFramerate() { $expected = '24'; @@ -574,9 +511,7 @@ public function testGetFramerate() $this->assertEquals($expected, $this->exif->getFramerate()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetDuration() { $expected = '1s'; @@ -585,9 +520,7 @@ public function testGetDuration() $this->assertEquals($expected, $this->exif->getDuration()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetMicroVideoOffset() { $expected = '3062730'; @@ -596,9 +529,7 @@ public function testGetMicroVideoOffset() $this->assertEquals($expected, $this->exif->getMicroVideoOffset()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetCity() { $expected = 'New York'; @@ -607,9 +538,7 @@ public function testGetCity() $this->assertEquals($expected, $this->exif->getCity()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetSublocation() { $expected = 'sublocation'; @@ -618,9 +547,7 @@ public function testGetSublocation() $this->assertEquals($expected, $this->exif->getSublocation()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetState() { $expected = 'New York'; @@ -629,9 +556,7 @@ public function testGetState() $this->assertEquals($expected, $this->exif->getState()); } - /** - * @group exif - */ + #[Group('exif')] public function testGetCountry() { $expected = 'USA'; @@ -640,9 +565,149 @@ public function testGetCountry() $this->assertEquals($expected, $this->exif->getCountry()); } + #[Group('exif')] + public function testGetExposureBias() + { + $expected = -0.67; + $data[Exif::EXPOSURE_BIAS] = $expected; + $this->exif->setData($data); + $this->assertSame($expected, $this->exif->getExposureBias()); + } + + #[Group('exif')] + public function testSetExposureBias() + { + foreach (array(-0.67, 0.0, 1.5) as $expected) { + $result = $this->exif->setExposureBias($expected); + + $this->assertSame($this->exif, $result); + $this->assertSame($expected, $this->exif->getExposureBias()); + $this->assertSame(array(Exif::EXPOSURE_BIAS => $expected), $this->exif->getData()); + } + } + + #[Group('exif')] + public function testGetWhiteBalance() + { + $expected = 1; + $data[Exif::WHITE_BALANCE] = $expected; + $this->exif->setData($data); + $this->assertSame($expected, $this->exif->getWhiteBalance()); + } + + #[Group('exif')] + public function testSetWhiteBalance() + { + foreach (array(0, 1) as $expected) { + $result = $this->exif->setWhiteBalance($expected); + + $this->assertSame($this->exif, $result); + $this->assertSame($expected, $this->exif->getWhiteBalance()); + $this->assertSame(array(Exif::WHITE_BALANCE => $expected), $this->exif->getData()); + } + } + + #[Group('exif')] + public function testGetWhiteBalanceTemperature() + { + $expected = 5500; + $data[Exif::WHITE_BALANCE_TEMPERATURE] = $expected; + $this->exif->setData($data); + $this->assertSame($expected, $this->exif->getWhiteBalanceTemperature()); + } + + #[Group('exif')] + public function testSetWhiteBalanceTemperature() + { + $result = $this->exif->setWhiteBalanceTemperature(5500); + + $this->assertSame($this->exif, $result); + $this->assertSame(5500, $this->exif->getWhiteBalanceTemperature()); + $this->assertSame(array(Exif::WHITE_BALANCE_TEMPERATURE => 5500), $this->exif->getData()); + } + + #[Group('exif')] + public function testGetProjectionType() + { + $expected = 'equirectangular'; + $data[Exif::PROJECTIONTYPE] = $expected; + $this->exif->setData($data); + $this->assertSame($expected, $this->exif->getProjectionType()); + } + + #[Group('exif')] + public function testGetUsePanoramaViewer() + { + foreach (array(true, false) as $expected) { + $data[Exif::USEPANORAMAVIEWER] = $expected; + $this->exif->setData($data); + $this->assertSame($expected, $this->exif->getUsePanoramaViewer()); + } + } + /** - * @group exif + * Data provider for the integer GPano crop fields + * + * @return array */ + public static function providerPanoCropFields() + { + return array( + array(Exif::FULLPANOWIDTHPIXELS, 'FullPanoWidthPixels', 8000), + array(Exif::FULLPANOHEIGHTPIXELS, 'FullPanoHeightPixels', 4000), + array(Exif::CROPPEDAREALEFTPIXELS, 'CroppedAreaLeftPixels', 1000), + array(Exif::CROPPEDAREATOPPIXELS, 'CroppedAreaTopPixels', 0), + array(Exif::CROPPEDAREAIMAGEWIDTHPIXELS, 'CroppedAreaImageWidthPixels', 6000), + array(Exif::CROPPEDAREAIMAGEHEIGHTPIXELS, 'CroppedAreaImageHeightPixels', 2000), + ); + } + + #[Group('exif')] + #[DataProvider('providerPanoCropFields')] + public function testGetPanoCropFields($constant, $name, $expected) + { + $data[$constant] = $expected; + $this->exif->setData($data); + $getter = 'get' . $name; + $this->assertSame($expected, $this->exif->$getter()); + } + + #[Group('exif')] + #[DataProvider('providerPanoCropFields')] + public function testSetPanoCropFields($constant, $name, $expected) + { + $setter = 'set' . $name; + $getter = 'get' . $name; + $result = $this->exif->$setter($expected); + + $this->assertSame($this->exif, $result); + $this->assertSame($expected, $this->exif->$getter()); + $this->assertSame(array($constant => $expected), $this->exif->getData()); + } + + #[Group('exif')] + public function testSetProjectionType() + { + $result = $this->exif->setProjectionType('equirectangular'); + + $this->assertSame($this->exif, $result); + $this->assertSame('equirectangular', $this->exif->getProjectionType()); + $this->assertSame(array(Exif::PROJECTIONTYPE => 'equirectangular'), $this->exif->getData()); + } + + #[Group('exif')] + public function testSetUsePanoramaViewer() + { + foreach (array(true, false) as $expected) { + $result = $this->exif->setUsePanoramaViewer($expected); + + $this->assertSame($this->exif, $result); + $this->assertSame($expected, $this->exif->getUsePanoramaViewer()); + $this->assertSame(array(Exif::USEPANORAMAVIEWER => $expected), $this->exif->getData()); + } + } + + #[Group('exif')] public function testMutatorMethodsSetInProperty() { $reflClass = new \ReflectionClass(get_class($this->exif)); @@ -683,6 +748,38 @@ public function testMutatorMethodsSetInProperty() $propertyValue = $reflProp->getValue($this->exif); $this->assertEquals($coords, $propertyValue[$value]); break; + case 'FullPanoWidthPixels': + case 'FullPanoHeightPixels': + case 'CroppedAreaLeftPixels': + case 'CroppedAreaTopPixels': + case 'CroppedAreaImageWidthPixels': + case 'CroppedAreaImageHeightPixels': + $pixels = 1234; + $this->exif->$setter($pixels); + $propertyValue = $reflProp->getValue($this->exif); + $this->assertSame($pixels, $propertyValue[$value]); + break; + case 'exposureBias': + $bias = -0.67; + $this->exif->$setter($bias); + $propertyValue = $reflProp->getValue($this->exif); + $this->assertSame($bias, $propertyValue[$value]); + break; + case 'whiteBalanceTemperature': + $this->exif->$setter(5500); + $propertyValue = $reflProp->getValue($this->exif); + $this->assertSame(5500, $propertyValue[$value]); + break; + case 'whiteBalance': + $this->exif->$setter(1); + $propertyValue = $reflProp->getValue($this->exif); + $this->assertSame(1, $propertyValue[$value]); + break; + case 'UsePanoramaViewer': + $this->exif->$setter(false); + $propertyValue = $reflProp->getValue($this->exif); + $this->assertSame(false, $propertyValue[$value]); + break; case 'focalDistance': $setter = 'setFocusDistance'; // no break @@ -697,9 +794,8 @@ public function testMutatorMethodsSetInProperty() /** * Test that the values returned by different adapters are equal - * - * @group consistency */ + #[Group('consistency')] public function testAdapterConsistency() { $reflClass = new \ReflectionClass(Exif::class); @@ -726,6 +822,8 @@ public function testAdapterConsistency() $name = $method->getName(); if ( strpos($name, 'get') !== 0 || $name === 'getRawData' || $name === 'getData' || $name === 'getColorSpace' || + // only the exiftool adapter reads the colour temperature (maker notes) + $name === 'getWhiteBalanceTemperature' || ($name === 'getLens' && $file === PHPEXIF_TEST_ROOT . '/files/dsc_5794.jpg') || ($file === PHPEXIF_TEST_ROOT . '/files/mongolia.jpeg' && ($name === 'getKeywords' || $name === 'getLens')) || ($file === PHPEXIF_TEST_ROOT . '/files/utf8.jpg' && ($name === 'getAuthor' || $name === 'getDescription')) @@ -771,7 +869,9 @@ public function testAdapterConsistency() // find all Getter methods on the results and compare its output foreach ($methods as $method) { $name = $method->getName(); - if (strpos($name, 'get') !== 0 || $name === 'getRawData' || $name === 'getData' || $name === 'getColorSpace') { + if (strpos($name, 'get') !== 0 || $name === 'getRawData' || $name === 'getData' || $name === 'getColorSpace' + || $name === 'getWhiteBalanceTemperature' + ) { continue; } $result_exif = $result_exif_exiftool->$name(); diff --git a/tests/PHPExif/Hydrator/MutatorTest.php b/tests/PHPExif/Hydrator/MutatorTest.php index a267b72..1664103 100644 --- a/tests/PHPExif/Hydrator/MutatorTest.php +++ b/tests/PHPExif/Hydrator/MutatorTest.php @@ -1,6 +1,7 @@ expects($this->exactly(count($input))) ->method('determineMutator') - ->will($this->returnValue('setFoo')); + ->willReturn('setFoo'); $object = new TestClass(); @@ -36,9 +35,7 @@ public function testHydrateCallsDetermineMutator() $mock->hydrate($object, $input); } - /** - * @group hydrator - */ + #[Group('hydrator')] public function testHydrateCallsMutatorsOnObject() { // input data @@ -48,7 +45,7 @@ public function testHydrateCallsMutatorsOnObject() // create mock $mock = $this->getMockBuilder('TestClass') - ->setMethods(array('setBar')) + ->onlyMethods(array('setBar')) ->getMock(); $mock->expects($this->once()) @@ -60,9 +57,7 @@ public function testHydrateCallsMutatorsOnObject() $hydrator->hydrate($mock, $input); } - /** - * @group hydrator - */ + #[Group('hydrator')] public function testHydrateCallsEmptyValues() { // input data diff --git a/tests/PHPExif/Mapper/ExiftoolMapperTest.php b/tests/PHPExif/Mapper/ExiftoolMapperTest.php index 8a3de57..bf57b76 100644 --- a/tests/PHPExif/Mapper/ExiftoolMapperTest.php +++ b/tests/PHPExif/Mapper/ExiftoolMapperTest.php @@ -1,7 +1,10 @@ mapper = new Exiftool(); } - /** - * @group mapper - */ + #[Group('mapper')] public function testClassImplementsCorrectInterface() { $this->assertInstanceOf(MapperInterface::class, $this->mapper); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataIgnoresFieldIfItDoesntExist() { $rawData = array('foo' => 'bar'); @@ -31,9 +30,7 @@ public function testMapRawDataIgnoresFieldIfItDoesntExist() $this->assertCount(0, $mapped); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataMapsFieldsCorrectly() { $reflProp = new \ReflectionProperty(get_class($this->mapper), 'map'); @@ -90,6 +87,24 @@ public function testMapRawDataMapsFieldsCorrectly() unset($map[Exiftool::SUBJECT]); unset($map[Exiftool::CONTENTIDENTIFIER]); unset($map[Exiftool::CONTENTIDENTIFIER_QUICKTIME]); + unset($map[Exiftool::PROJECTIONTYPE]); + unset($map[Exiftool::EXPOSUREBIAS]); + unset($map[Exiftool::WHITEBALANCE]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_CANON_ASSHOT]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_APPLE]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_SONY]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_FUJIFILM]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_PANASONIC]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_PENTAX]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_CANON]); + unset($map[Exiftool::WHITEBALANCETEMPERATURE_LIGHTROOM]); + unset($map[Exiftool::USEPANORAMAVIEWER]); + unset($map[Exiftool::FULLPANOWIDTHPIXELS]); + unset($map[Exiftool::FULLPANOHEIGHTPIXELS]); + unset($map[Exiftool::CROPPEDAREALEFTPIXELS]); + unset($map[Exiftool::CROPPEDAREATOPPIXELS]); + unset($map[Exiftool::CROPPEDAREAIMAGEWIDTHPIXELS]); + unset($map[Exiftool::CROPPEDAREAIMAGEHEIGHTPIXELS]); // create raw data $keys = array_keys($map); @@ -107,9 +122,7 @@ public function testMapRawDataMapsFieldsCorrectly() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsAperture() { $rawData = array( @@ -121,9 +134,7 @@ public function testMapRawDataCorrectlyFormatsAperture() $this->assertEquals('f/0.1', reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsFocusDistance() { $rawData = array( @@ -135,9 +146,7 @@ public function testMapRawDataCorrectlyFormatsFocusDistance() $this->assertEquals('50m', reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDate() { $rawData = array( @@ -154,9 +163,7 @@ public function testMapRawDataCorrectlyFormatsCreationDate() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() { $data = array( @@ -198,9 +205,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone2() { $rawData = array( @@ -226,9 +231,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone2() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectCreationDate() { $rawData = array( @@ -240,9 +243,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectCreationDate() $this->assertEquals(false, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectCreationDate2() { $rawData = array( @@ -254,9 +255,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectCreationDate2() $this->assertEquals(false, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectTimeZone() { $rawData = array( @@ -274,9 +273,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectTimeZone() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsExposureTime() { $rawData = array( @@ -295,9 +292,7 @@ public function testMapRawDataCorrectlyFormatsExposureTime() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsFocalLength() { $rawData = array( @@ -309,9 +304,7 @@ public function testMapRawDataCorrectlyFormatsFocalLength() $this->assertEquals(15, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsGPSData() { $this->mapper->setNumeric(false); @@ -333,9 +326,7 @@ public function testMapRawDataCorrectlyFormatsGPSData() $this->assertEquals($expected_lon, $result['longitude']); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataIncorrectlyFormatedGPSData() { $this->mapper->setNumeric(false); @@ -351,9 +342,7 @@ public function testMapRawDataIncorrectlyFormatedGPSData() $this->assertCount(0, $result); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsNumericGPSData() { $result = $this->mapper->mapRawData( @@ -374,9 +363,7 @@ public function testMapRawDataCorrectlyFormatsNumericGPSData() $this->assertEquals($expected_lon, $result['longitude']); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataOnlyLatitude() { $result = $this->mapper->mapRawData( @@ -389,9 +376,7 @@ public function testMapRawDataOnlyLatitude() $this->assertCount(1, $result); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresEmptyGPSData() { $result = $this->mapper->mapRawData( @@ -406,9 +391,7 @@ public function testMapRawDataCorrectlyIgnoresEmptyGPSData() $this->assertEquals(false, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectImageDirection() { $rawData = array( @@ -420,9 +403,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectImageDirection() $this->assertEquals(false, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectImageDirection() { $rawData = array( @@ -434,9 +415,7 @@ public function testMapRawDataCorrectImageDirection() $this->assertEquals('180.0', reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testSetNumericInProperty() { $reflProperty = new \ReflectionProperty(get_class($this->mapper), 'numeric'); @@ -482,9 +461,7 @@ public function testMapRawDataCorrectlyIgnoresInvalidCreateDate() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyAltitude() { $result = $this->mapper->mapRawData( @@ -497,9 +474,7 @@ public function testMapRawDataCorrectlyAltitude() $this->assertEquals($expected, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyNegativeAltitude() { $result = $this->mapper->mapRawData( @@ -512,9 +487,7 @@ public function testMapRawDataCorrectlyNegativeAltitude() $this->assertEquals($expected, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectAltitude() { $result = $this->mapper->mapRawData( @@ -526,9 +499,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectAltitude() $this->assertEquals(false, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsQuicktimeGPSData() { $result = $this->mapper->mapRawData( @@ -548,9 +519,7 @@ public function testMapRawDataCorrectlyFormatsQuicktimeGPSData() $this->assertEquals($expected_lon, $result['longitude']); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyQuicktimeAltitude() { $result = $this->mapper->mapRawData( @@ -563,9 +532,7 @@ public function testMapRawDataCorrectlyQuicktimeAltitude() $this->assertEquals($expected, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyHeightVideo() { $rawData = array( @@ -607,9 +574,7 @@ public function testMapRawDataCorrectlyHeightVideo() - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyWidthVideo() { $rawData = array( @@ -650,9 +615,7 @@ public function testMapRawDataCorrectlyWidthVideo() } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIsoFormats() { $expected = array( @@ -670,9 +633,7 @@ public function testMapRawDataCorrectlyIsoFormats() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyLensData() { $data = array( @@ -699,9 +660,7 @@ public function testMapRawDataCorrectlyLensData() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyLensData2() { $rawData = array( @@ -716,9 +675,7 @@ public function testMapRawDataCorrectlyLensData2() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyKeywords() { $rawData = array( @@ -733,9 +690,7 @@ public function testMapRawDataCorrectlyKeywords() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyKeywordsAndSubject() { $rawData = array( @@ -750,4 +705,263 @@ public function testMapRawDataCorrectlyKeywordsAndSubject() reset($mapped) ); } + + #[Group('mapper')] + public function testMapRawDataCorrectlyFormatsProjectionType() + { + $mapped = $this->mapper->mapRawData(array( + Exiftool::PROJECTIONTYPE => ' EquiRectangular ', + )); + + $this->assertSame(array(Exif::PROJECTIONTYPE => 'equirectangular'), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresEmptyProjectionType() + { + $mapped = $this->mapper->mapRawData(array( + Exiftool::PROJECTIONTYPE => ' ', + )); + + $this->assertSame(array(), $mapped); + } + + /** + * Data provider for testMapRawDataCorrectlyFormatsUsePanoramaViewer + * + * @return array + */ + public static function providerUsePanoramaViewer() + { + return array( + 'bool true' => array(true, true), + 'bool false' => array(false, false), + 'string True' => array('True', true), + 'string False' => array('False', false), + 'string false' => array('false', false), + 'string TRUE' => array(' TRUE ', true), + 'int 1' => array(1, true), + 'int 0' => array(0, false), + 'string 1' => array('1', true), + 'string 0' => array('0', false), + ); + } + + #[Group('mapper')] + #[DataProvider('providerUsePanoramaViewer')] + public function testMapRawDataCorrectlyFormatsUsePanoramaViewer($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + Exiftool::USEPANORAMAVIEWER => $raw, + )); + + $this->assertSame(array(Exif::USEPANORAMAVIEWER => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidUsePanoramaViewer() + { + foreach (array('abc', '', 2, 'yes') as $raw) { + $mapped = $this->mapper->mapRawData(array( + Exiftool::USEPANORAMAVIEWER => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider for the integer GPano crop fields + * + * @return array + */ + public static function providerPanoCropFields() + { + return array( + array(Exiftool::FULLPANOWIDTHPIXELS, Exif::FULLPANOWIDTHPIXELS), + array(Exiftool::FULLPANOHEIGHTPIXELS, Exif::FULLPANOHEIGHTPIXELS), + array(Exiftool::CROPPEDAREALEFTPIXELS, Exif::CROPPEDAREALEFTPIXELS), + array(Exiftool::CROPPEDAREATOPPIXELS, Exif::CROPPEDAREATOPPIXELS), + array(Exiftool::CROPPEDAREAIMAGEWIDTHPIXELS, Exif::CROPPEDAREAIMAGEWIDTHPIXELS), + array(Exiftool::CROPPEDAREAIMAGEHEIGHTPIXELS, Exif::CROPPEDAREAIMAGEHEIGHTPIXELS), + ); + } + + #[Group('mapper')] + #[DataProvider('providerPanoCropFields')] + public function testMapRawDataCorrectlyFormatsPanoCropFields($field, $key) + { + foreach (array(8000, '8000', ' 8000 ') as $raw) { + $mapped = $this->mapper->mapRawData(array($field => $raw)); + + $this->assertSame(array($key => 8000), $mapped, 'value: ' . var_export($raw, true)); + } + } + + #[Group('mapper')] + #[DataProvider('providerPanoCropFields')] + public function testMapRawDataIgnoresNonNumericPanoCropFields($field, $key) + { + foreach (array('abc', '', '8000px') as $raw) { + $mapped = $this->mapper->mapRawData(array($field => $raw)); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider for testMapRawDataCorrectlyFormatsExposureBias + * + * @return array + */ + public static function providerExposureBias() + { + return array( + 'numeric -2/3' => array(-0.6666666667, -0.67), + 'numeric 0' => array(0, 0.0), + 'numeric 1' => array(1, 1.0), + 'rational -2/3' => array('-2/3', -0.67), + 'rational +1/3' => array('+1/3', 0.33), + 'rational 0/6' => array('0/6', 0.0), + 'rational 3/2' => array('3/2', 1.5), + 'string 0' => array('0', 0.0), + ); + } + + #[Group('mapper')] + #[DataProvider('providerExposureBias')] + public function testMapRawDataCorrectlyFormatsExposureBias($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + Exiftool::EXPOSUREBIAS => $raw, + )); + + $this->assertSame(array(Exif::EXPOSURE_BIAS => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidExposureBias() + { + foreach (array('abc', '', '1/0', '1/abc') as $raw) { + $mapped = $this->mapper->mapRawData(array( + Exiftool::EXPOSUREBIAS => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider for testMapRawDataCorrectlyFormatsWhiteBalance + * + * @return array + */ + public static function providerWhiteBalance() + { + return array( + 'int 0' => array(0, 0), + 'int 1' => array(1, 1), + 'string 0' => array('0', 0), + 'string 1' => array('1', 1), + 'label Auto' => array('Auto', 0), + 'label Manual' => array('Manual', 1), + ); + } + + #[Group('mapper')] + #[DataProvider('providerWhiteBalance')] + public function testMapRawDataCorrectlyFormatsWhiteBalance($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + Exiftool::WHITEBALANCE => $raw, + )); + + $this->assertSame(array(Exif::WHITE_BALANCE => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidWhiteBalance() + { + foreach (array('abc', '') as $raw) { + $mapped = $this->mapper->mapRawData(array( + Exiftool::WHITEBALANCE => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider: every exiftool tag that can hold the colour temperature + * + * @return array + */ + public static function providerWhiteBalanceTemperatureTags() + { + return array( + 'CANON_ASSHOT' => array(Exiftool::WHITEBALANCETEMPERATURE_CANON_ASSHOT), + 'APPLE' => array(Exiftool::WHITEBALANCETEMPERATURE_APPLE), + 'SONY' => array(Exiftool::WHITEBALANCETEMPERATURE_SONY), + 'FUJIFILM' => array(Exiftool::WHITEBALANCETEMPERATURE_FUJIFILM), + 'PANASONIC' => array(Exiftool::WHITEBALANCETEMPERATURE_PANASONIC), + 'PENTAX' => array(Exiftool::WHITEBALANCETEMPERATURE_PENTAX), + 'CANON' => array(Exiftool::WHITEBALANCETEMPERATURE_CANON), + 'LIGHTROOM' => array(Exiftool::WHITEBALANCETEMPERATURE_LIGHTROOM), + ); + } + + #[Group('mapper')] + #[DataProvider('providerWhiteBalanceTemperatureTags')] + public function testMapRawDataCorrectlyFormatsWhiteBalanceTemperature($field) + { + foreach (array(5500, '5500', ' 5500 ') as $raw) { + $mapped = $this->mapper->mapRawData(array($field => $raw)); + + $this->assertSame( + array(Exif::WHITE_BALANCE_TEMPERATURE => 5500), + $mapped, + 'value: ' . var_export($raw, true) + ); + } + } + + /** + * Sony writes 0 for "Auto" and 0xffffffff for "n/a"; + * without -n exiftool prints those as text. + */ + #[Group('mapper')] + #[DataProvider('providerWhiteBalanceTemperatureTags')] + public function testMapRawDataIgnoresInvalidWhiteBalanceTemperature($field) + { + foreach (array(0, '0', 4294967295, 'Auto', 'n/a', '', 'abc', 999, 50001) as $raw) { + $mapped = $this->mapper->mapRawData(array($field => $raw)); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + #[Group('mapper')] + public function testMapRawDataPrefersMeasuredWhiteBalanceTemperature() + { + // Lightroom and the manual Canon setting appear first, but the as-shot value wins + $mapped = $this->mapper->mapRawData(array( + Exiftool::WHITEBALANCETEMPERATURE_LIGHTROOM => 6500, + Exiftool::WHITEBALANCETEMPERATURE_CANON => 5200, + Exiftool::WHITEBALANCETEMPERATURE_CANON_ASSHOT => 4830, + )); + + $this->assertSame(array(Exif::WHITE_BALANCE_TEMPERATURE => 4830), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataFallsBackWhenWhiteBalanceTemperatureIsAuto() + { + // Sony "Auto" (0) is skipped in favour of the next valid tag + $mapped = $this->mapper->mapRawData(array( + Exiftool::WHITEBALANCETEMPERATURE_SONY => 0, + Exiftool::WHITEBALANCETEMPERATURE_LIGHTROOM => 5000, + )); + + $this->assertSame(array(Exif::WHITE_BALANCE_TEMPERATURE => 5000), $mapped); + } } diff --git a/tests/PHPExif/Mapper/FFprobeMapperTest.php b/tests/PHPExif/Mapper/FFprobeMapperTest.php index 8e24020..32eaea6 100644 --- a/tests/PHPExif/Mapper/FFprobeMapperTest.php +++ b/tests/PHPExif/Mapper/FFprobeMapperTest.php @@ -3,6 +3,7 @@ use FFMpeg\FFProbe as FFMpegFFProbe; use PHPExif\Contracts\MapperInterface; use PHPExif\Mapper\FFprobe; +use PHPUnit\Framework\Attributes\Group; class FFprobeMapperTest extends \PHPUnit\Framework\TestCase { @@ -13,17 +14,13 @@ public function setUp(): void $this->mapper = new FFprobe(); } - /** - * @group mapper - */ + #[Group('mapper')] public function testClassImplementsCorrectInterface() { $this->assertInstanceOf(MapperInterface::class, $this->mapper); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataIgnoresFieldIfItDoesntExist() { $rawData = array('foo' => 'bar'); @@ -32,9 +29,7 @@ public function testMapRawDataIgnoresFieldIfItDoesntExist() $this->assertCount(0, $mapped); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataMapsFieldsCorrectly() { $reflProp = new \ReflectionProperty(get_class($this->mapper), 'map'); @@ -75,9 +70,7 @@ public function testMapRawDataMapsFieldsCorrectly() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsDateTimeOriginal() { $rawData = array( @@ -95,9 +88,7 @@ public function testMapRawDataCorrectlyFormatsDateTimeOriginal() } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateQuicktime() { $rawData = array( @@ -123,9 +114,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateQuicktime() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() { $rawData = array( @@ -150,9 +139,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal() { $rawData = array( @@ -164,9 +151,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal() $this->assertEquals(false, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal2() { $rawData = array( @@ -178,9 +163,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal2() $this->assertEquals(false, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsQuickTimeGPSData() { $expected = array( @@ -201,9 +184,7 @@ public function testMapRawDataCorrectlyFormatsQuickTimeGPSData() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyRotatesDimensions() { $expected = array( @@ -222,9 +203,7 @@ public function testMapRawDataCorrectlyRotatesDimensions() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsGPSData() { $expected = array( @@ -245,9 +224,7 @@ public function testMapRawDataCorrectlyFormatsGPSData() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFramerate() { $expected = array( @@ -300,9 +277,7 @@ public function testMapRawDataCorrectlyIgnoresInvalidCreateDate() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testNormalizeComponentCorrectly() { $reflMethod = new \ReflectionMethod(FFprobe::class, 'normalizeComponent'); @@ -326,9 +301,7 @@ public function testNormalizeComponentCorrectly() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataMatchesFieldsWithoutCaseSensibilityOnFirstLetter() { $rawData = array( @@ -346,9 +319,7 @@ public function testMapRawDataMatchesFieldsWithoutCaseSensibilityOnFirstLetter() $this->assertEquals($expected, $keys); } - /** - * @group mapper - */ + #[Group('mapper')] public function testreadISO6709() { $reflMethod = new \ReflectionMethod(FFprobe::class, 'readISO6709'); @@ -407,9 +378,7 @@ public function testreadISO6709() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testconvertDMStoDecimal() { @@ -479,10 +448,7 @@ public function testconvertDMStoDecimal() } } - /** - * @group mapper - * @covers \PHPExif\Mapper\FFprobe::mapRawData - */ + #[Group('mapper')] public function testMapRawDataCorrectlyKeywords() { $rawData = array( @@ -497,10 +463,7 @@ public function testMapRawDataCorrectlyKeywords() ); } - /** - * @group mapper - * @covers \PHPExif\Mapper\FFprobe::mapRawData - */ + #[Group('mapper')] public function testMapRawDataCorrectlySplitKeywords() { $rawData = array( @@ -515,10 +478,7 @@ public function testMapRawDataCorrectlySplitKeywords() ); } - /** - * @group mapper - * @covers \PHPExif\Mapper\FFprobe::mapRawData - */ + #[Group('mapper')] public function testMapRawDataCorrectlyArrayKeywords() { $rawData = array( diff --git a/tests/PHPExif/Mapper/ImageMagickMapperTest.php b/tests/PHPExif/Mapper/ImageMagickMapperTest.php index 4cbb8f7..45f2cba 100644 --- a/tests/PHPExif/Mapper/ImageMagickMapperTest.php +++ b/tests/PHPExif/Mapper/ImageMagickMapperTest.php @@ -1,7 +1,10 @@ mapper = new ImageMagick(); } - /** - * @group mapper - */ + #[Group('mapper')] public function testClassImplementsCorrectInterface() { $this->assertInstanceOf(MapperInterface::class, $this->mapper); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataIgnoresFieldIfItDoesntExist() { $rawData = array('foo' => 'bar'); @@ -31,9 +30,7 @@ public function testMapRawDataIgnoresFieldIfItDoesntExist() $this->assertCount(0, $mapped); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataMapsFieldsCorrectly() { $reflProp = new \ReflectionProperty(get_class($this->mapper), 'map'); @@ -53,6 +50,16 @@ public function testMapRawDataMapsFieldsCorrectly() unset($map[ImageMagick::IMAGEHEIGHT_PNG]); unset($map[ImageMagick::IMAGEWIDTH_PNG]); unset($map[ImageMagick::COPYRIGHT_IPTC]); + unset($map[ImageMagick::PROJECTIONTYPE]); + unset($map[ImageMagick::EXPOSUREBIAS]); + unset($map[ImageMagick::WHITEBALANCE]); + unset($map[ImageMagick::USEPANORAMAVIEWER]); + unset($map[ImageMagick::FULLPANOWIDTHPIXELS]); + unset($map[ImageMagick::FULLPANOHEIGHTPIXELS]); + unset($map[ImageMagick::CROPPEDAREALEFTPIXELS]); + unset($map[ImageMagick::CROPPEDAREATOPPIXELS]); + unset($map[ImageMagick::CROPPEDAREAIMAGEWIDTHPIXELS]); + unset($map[ImageMagick::CROPPEDAREAIMAGEHEIGHTPIXELS]); // create raw data $keys = array_unique(array_keys($map)); @@ -69,9 +76,7 @@ public function testMapRawDataMapsFieldsCorrectly() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsAperture() { $rawData = array( @@ -83,9 +88,7 @@ public function testMapRawDataCorrectlyFormatsAperture() $this->assertEquals('f/1.7', reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsDateTimeOriginal() { $rawData = array( @@ -102,9 +105,7 @@ public function testMapRawDataCorrectlyFormatsDateTimeOriginal() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() { $data = array( @@ -146,9 +147,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone2() { $rawData = array( @@ -174,9 +173,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone2() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal() { $rawData = array( @@ -188,9 +185,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal() $this->assertEquals(false, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectTimeZone() { $rawData = array( @@ -208,9 +203,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectTimeZone() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsExposureTime() { $rawData = array( @@ -229,9 +222,7 @@ public function testMapRawDataCorrectlyFormatsExposureTime() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsFocalLength() { $rawData = array( @@ -243,9 +234,7 @@ public function testMapRawDataCorrectlyFormatsFocalLength() $this->assertEquals(15, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsGPSData() { $result = $this->mapper->mapRawData( @@ -265,9 +254,7 @@ public function testMapRawDataCorrectlyFormatsGPSData() $this->assertEquals($expected_lon, $result['longitude']); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataIncorrectlyFormatedGPSData() { $result = $this->mapper->mapRawData( @@ -281,9 +268,7 @@ public function testMapRawDataIncorrectlyFormatedGPSData() $this->assertCount(0, $result); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsNumericGPSData() { $result = $this->mapper->mapRawData( @@ -304,9 +289,7 @@ public function testMapRawDataCorrectlyFormatsNumericGPSData() $this->assertEquals($expected_lon, $result['longitude']); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataOnlyLatitude() { $result = $this->mapper->mapRawData( @@ -319,9 +302,7 @@ public function testMapRawDataOnlyLatitude() $this->assertCount(1, $result); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresEmptyGPSData() { $result = $this->mapper->mapRawData( @@ -372,9 +353,7 @@ public function testMapRawDataCorrectlyIgnoresInvalidCreateDate() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyAltitude() { $result = $this->mapper->mapRawData( @@ -387,9 +366,7 @@ public function testMapRawDataCorrectlyAltitude() $this->assertEquals($expected, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyNegativeAltitude() { $result = $this->mapper->mapRawData( @@ -402,9 +379,7 @@ public function testMapRawDataCorrectlyNegativeAltitude() $this->assertEquals($expected, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectAltitude() { $result = $this->mapper->mapRawData( @@ -417,9 +392,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectAltitude() } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIsoFormats() { $expected = array( @@ -440,9 +413,7 @@ public function testMapRawDataCorrectlyIsoFormats() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyHeightPNG() { @@ -461,9 +432,7 @@ public function testMapRawDataCorrectlyHeightPNG() - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyWidthPNG() { @@ -480,9 +449,7 @@ public function testMapRawDataCorrectlyWidthPNG() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testNormalizeComponentCorrectly() { $reflMethod = new \ReflectionMethod(ImageMagick::class, 'normalizeComponent'); @@ -506,9 +473,7 @@ public function testNormalizeComponentCorrectly() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyKeywords() { $rawData = array( @@ -523,9 +488,7 @@ public function testMapRawDataCorrectlyKeywords() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyKeywordsAndSubject() { $rawData = array( @@ -540,9 +503,7 @@ public function testMapRawDataCorrectlyKeywordsAndSubject() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsXResolution() { $rawData = array( @@ -554,9 +515,7 @@ public function testMapRawDataCorrectlyFormatsXResolution() $this->assertEquals(1500, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsYResolution() { $rawData = array( @@ -567,4 +526,177 @@ public function testMapRawDataCorrectlyFormatsYResolution() $this->assertEquals(1500, reset($mapped)); } + + #[Group('mapper')] + public function testMapRawDataCorrectlyFormatsProjectionType() + { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::PROJECTIONTYPE => ' EquiRectangular ', + )); + + $this->assertSame(array(Exif::PROJECTIONTYPE => 'equirectangular'), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresEmptyProjectionType() + { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::PROJECTIONTYPE => ' ', + )); + + $this->assertSame(array(), $mapped); + } + + /** + * Data provider for testMapRawDataCorrectlyFormatsUsePanoramaViewer + * + * @return array + */ + public static function providerUsePanoramaViewer() + { + return array( + 'string True' => array('True', true), + 'string False' => array('False', false), + 'string false' => array('false', false), + 'string TRUE' => array(' TRUE ', true), + 'string 1' => array('1', true), + 'string 0' => array('0', false), + ); + } + + #[Group('mapper')] + #[DataProvider('providerUsePanoramaViewer')] + public function testMapRawDataCorrectlyFormatsUsePanoramaViewer($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::USEPANORAMAVIEWER => $raw, + )); + + $this->assertSame(array(Exif::USEPANORAMAVIEWER => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidUsePanoramaViewer() + { + foreach (array('abc', '', '2', 'yes') as $raw) { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::USEPANORAMAVIEWER => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider for the integer GPano crop fields + * + * @return array + */ + public static function providerPanoCropFields() + { + return array( + array(ImageMagick::FULLPANOWIDTHPIXELS, Exif::FULLPANOWIDTHPIXELS), + array(ImageMagick::FULLPANOHEIGHTPIXELS, Exif::FULLPANOHEIGHTPIXELS), + array(ImageMagick::CROPPEDAREALEFTPIXELS, Exif::CROPPEDAREALEFTPIXELS), + array(ImageMagick::CROPPEDAREATOPPIXELS, Exif::CROPPEDAREATOPPIXELS), + array(ImageMagick::CROPPEDAREAIMAGEWIDTHPIXELS, Exif::CROPPEDAREAIMAGEWIDTHPIXELS), + array(ImageMagick::CROPPEDAREAIMAGEHEIGHTPIXELS, Exif::CROPPEDAREAIMAGEHEIGHTPIXELS), + ); + } + + #[Group('mapper')] + #[DataProvider('providerPanoCropFields')] + public function testMapRawDataCorrectlyFormatsPanoCropFields($field, $key) + { + foreach (array('8000', ' 8000 ') as $raw) { + $mapped = $this->mapper->mapRawData(array($field => $raw)); + + $this->assertSame(array($key => 8000), $mapped, 'value: ' . var_export($raw, true)); + } + } + + #[Group('mapper')] + #[DataProvider('providerPanoCropFields')] + public function testMapRawDataIgnoresNonNumericPanoCropFields($field, $key) + { + foreach (array('abc', '', '8000px') as $raw) { + $mapped = $this->mapper->mapRawData(array($field => $raw)); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider for testMapRawDataCorrectlyFormatsExposureBias + * + * @return array + */ + public static function providerExposureBias() + { + return array( + 'rational -2/3' => array('-2/3', -0.67), + 'rational +1/3' => array('+1/3', 0.33), + 'rational 0/6' => array('0/6', 0.0), + 'rational 3/2' => array('3/2', 1.5), + ); + } + + #[Group('mapper')] + #[DataProvider('providerExposureBias')] + public function testMapRawDataCorrectlyFormatsExposureBias($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::EXPOSUREBIAS => $raw, + )); + + $this->assertSame(array(Exif::EXPOSURE_BIAS => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidExposureBias() + { + foreach (array('abc', '', '1/0', '1/abc') as $raw) { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::EXPOSUREBIAS => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider for testMapRawDataCorrectlyFormatsWhiteBalance + * + * @return array + */ + public static function providerWhiteBalance() + { + return array( + 'string 0' => array('0', 0), + 'string 1' => array('1', 1), + ); + } + + #[Group('mapper')] + #[DataProvider('providerWhiteBalance')] + public function testMapRawDataCorrectlyFormatsWhiteBalance($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::WHITEBALANCE => $raw, + )); + + $this->assertSame(array(Exif::WHITE_BALANCE => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidWhiteBalance() + { + foreach (array('abc', '') as $raw) { + $mapped = $this->mapper->mapRawData(array( + ImageMagick::WHITEBALANCE => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } } diff --git a/tests/PHPExif/Mapper/NativeMapperTest.php b/tests/PHPExif/Mapper/NativeMapperTest.php index ceffd7d..6c0911a 100644 --- a/tests/PHPExif/Mapper/NativeMapperTest.php +++ b/tests/PHPExif/Mapper/NativeMapperTest.php @@ -1,7 +1,10 @@ mapper = new Native(); } - /** - * @group mapper - */ + #[Group('mapper')] public function testClassImplementsCorrectInterface() { $this->assertInstanceOf(MapperInterface::class, $this->mapper); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataIgnoresFieldIfItDoesntExist() { $rawData = array('foo' => 'bar'); @@ -31,9 +30,7 @@ public function testMapRawDataIgnoresFieldIfItDoesntExist() $this->assertCount(0, $mapped); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataMapsFieldsCorrectly() { $reflProp = new \ReflectionProperty(get_class($this->mapper), 'map'); @@ -55,6 +52,8 @@ public function testMapRawDataMapsFieldsCorrectly() unset($map[Native::COUNTRY]); unset($map[Native::LENS_LR]); unset($map[Native::LENS_TYPE]); + unset($map[Native::EXPOSUREBIAS]); + unset($map[Native::WHITEBALANCE]); // create raw data $keys = array_keys($map); @@ -72,9 +71,7 @@ public function testMapRawDataMapsFieldsCorrectly() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsDateTimeOriginal() { $rawData = array( @@ -92,9 +89,7 @@ public function testMapRawDataCorrectlyFormatsDateTimeOriginal() } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() { $rawData = array( @@ -119,9 +114,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone2() { $rawData = array( @@ -147,9 +140,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone2() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone3() { $rawData = array( @@ -175,9 +166,7 @@ public function testMapRawDataCorrectlyFormatsCreationDateWithTimeZone3() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal() { $rawData = array( @@ -189,9 +178,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectDateTimeOriginal() $this->assertEquals(false, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectTimeZone() { $rawData = array( @@ -209,9 +196,7 @@ public function testMapRawDataCorrectlyIgnoresIncorrectTimeZone() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsExposureTime() { $rawData = array( @@ -230,9 +215,7 @@ public function testMapRawDataCorrectlyFormatsExposureTime() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsFocalLength() { $rawData = array( @@ -244,9 +227,7 @@ public function testMapRawDataCorrectlyFormatsFocalLength() $this->assertEquals(6, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsFocalLengthDivisionByZero() { $rawData = array( @@ -258,9 +239,7 @@ public function testMapRawDataCorrectlyFormatsFocalLengthDivisionByZero() $this->assertEquals(0, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsXResolution() { $rawData = array( @@ -272,9 +251,7 @@ public function testMapRawDataCorrectlyFormatsXResolution() $this->assertEquals(1500, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsYResolution() { $rawData = array( @@ -286,9 +263,7 @@ public function testMapRawDataCorrectlyFormatsYResolution() $this->assertEquals(1500, reset($mapped)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataFlattensRawDataWithSections() { $rawData = array( @@ -308,9 +283,7 @@ public function testMapRawDataFlattensRawDataWithSections() $this->assertEquals($expected, $keys); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataMatchesFieldsWithoutCaseSensibilityOnFirstLetter() { $rawData = array( @@ -328,9 +301,7 @@ public function testMapRawDataMatchesFieldsWithoutCaseSensibilityOnFirstLetter() $this->assertEquals($expected, $keys); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsGPSData() { $expected = array( @@ -360,9 +331,7 @@ public function testMapRawDataCorrectlyFormatsGPSData() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresEmptyGPSData() { $result = $this->mapper->mapRawData( @@ -377,9 +346,7 @@ public function testMapRawDataCorrectlyIgnoresEmptyGPSData() $this->assertEquals(false, reset($result)); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyFormatsAltitudeData() { $expected = array( @@ -399,9 +366,7 @@ public function testMapRawDataCorrectlyFormatsAltitudeData() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIgnoresIncorrectAltitude() { $result = $this->mapper->mapRawData( @@ -448,9 +413,7 @@ public function testMapRawDataCorrectlyIgnoresInvalidCreateDate() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testNormalizeComponentCorrectly() { $reflMethod = new \ReflectionMethod(Native::class, 'normalizeComponent'); @@ -473,9 +436,7 @@ public function testNormalizeComponentCorrectly() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyIsoFormats() { $expected = array( @@ -496,9 +457,7 @@ public function testMapRawDataCorrectlyIsoFormats() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyLensData() { $data = array( @@ -530,9 +489,7 @@ public function testMapRawDataCorrectlyLensData() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyLensData2() { $data = array( @@ -554,9 +511,7 @@ public function testMapRawDataCorrectlyLensData2() } } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyKeywords() { $rawData = array( @@ -571,9 +526,7 @@ public function testMapRawDataCorrectlyKeywords() ); } - /** - * @group mapper - */ + #[Group('mapper')] public function testMapRawDataCorrectlyKeywordsAndSubject() { $rawData = array( @@ -588,4 +541,80 @@ public function testMapRawDataCorrectlyKeywordsAndSubject() reset($mapped) ); } + + /** + * Data provider for testMapRawDataCorrectlyFormatsExposureBias + * + * @return array + */ + public static function providerExposureBias() + { + return array( + 'rational -2/3' => array('-2/3', -0.67), + 'rational +1/3' => array('+1/3', 0.33), + 'rational 0/6' => array('0/6', 0.0), + 'rational 3/2' => array('3/2', 1.5), + ); + } + + #[Group('mapper')] + #[DataProvider('providerExposureBias')] + public function testMapRawDataCorrectlyFormatsExposureBias($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + Native::EXPOSUREBIAS => $raw, + )); + + $this->assertSame(array(Exif::EXPOSURE_BIAS => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidExposureBias() + { + foreach (array('abc', '', '1/0', '1/abc') as $raw) { + $mapped = $this->mapper->mapRawData(array( + Native::EXPOSUREBIAS => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } + + /** + * Data provider for testMapRawDataCorrectlyFormatsWhiteBalance + * + * @return array + */ + public static function providerWhiteBalance() + { + return array( + 'int 0' => array(0, 0), + 'int 1' => array(1, 1), + 'string 0' => array('0', 0), + 'string 1' => array('1', 1), + ); + } + + #[Group('mapper')] + #[DataProvider('providerWhiteBalance')] + public function testMapRawDataCorrectlyFormatsWhiteBalance($raw, $expected) + { + $mapped = $this->mapper->mapRawData(array( + Native::WHITEBALANCE => $raw, + )); + + $this->assertSame(array(Exif::WHITE_BALANCE => $expected), $mapped); + } + + #[Group('mapper')] + public function testMapRawDataIgnoresInvalidWhiteBalance() + { + foreach (array('abc', '') as $raw) { + $mapped = $this->mapper->mapRawData(array( + Native::WHITEBALANCE => $raw, + )); + + $this->assertSame(array(), $mapped, 'value: ' . var_export($raw, true)); + } + } } diff --git a/tests/PHPExif/Reader/ReaderTest.php b/tests/PHPExif/Reader/ReaderTest.php index d000f81..17ccd84 100644 --- a/tests/PHPExif/Reader/ReaderTest.php +++ b/tests/PHPExif/Reader/ReaderTest.php @@ -7,16 +7,15 @@ use PHPExif\Contracts\AdapterInterface; use PHPExif\Exif; use PHPExif\Reader\Reader; +use PHPUnit\Framework\Attributes\Group; class ReaderTest extends \PHPUnit\Framework\TestCase { - /** - * @group reader - */ + #[Group('reader')] public function testConstructorWithAdapter() { /** @var AdapterInterface $mock */ - $mock = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $mock = $this->createStub(AdapterInterface::class); $reflProperty = new \ReflectionProperty(Reader::class, 'adapter'); $reader = new Reader($mock); @@ -24,29 +23,23 @@ public function testConstructorWithAdapter() $this->assertSame($mock, $reflProperty->getValue($reader)); } - /** - * @group reader - */ + #[Group('reader')] public function testGetExifPassedToAdapter() { - $adapter = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $adapter = $this->createMock(AdapterInterface::class); $adapter->expects($this->once())->method('getExifFromFile'); $reader = new Reader($adapter); $reader->read('/tmp/foo.bar'); } - /** - * @group reader - */ + #[Group('reader')] public function testFactoryThrowsException() { $this->expectException('TypeError'); Reader::factory('foo'); } - /** - * @group reader - */ + #[Group('reader')] public function testFactoryReturnsCorrectType() { $reader = Reader::factory(\PHPExif\Enum\ReaderType::NATIVE); @@ -54,9 +47,7 @@ public function testFactoryReturnsCorrectType() $this->assertInstanceOf(Reader::class, $reader); } - /** - * @group reader - */ + #[Group('reader')] public function testFactoryAdapterTypeNative() { $reader = Reader::factory(\PHPExif\Enum\ReaderType::NATIVE); @@ -67,9 +58,7 @@ public function testFactoryAdapterTypeNative() $this->assertInstanceOf(Native::class, $adapter); } - /** - * @group reader - */ + #[Group('reader')] public function testFactoryAdapterTypeExiftool() { $reader = Reader::factory(\PHPExif\Enum\ReaderType::EXIFTOOL); @@ -80,9 +69,7 @@ public function testFactoryAdapterTypeExiftool() $this->assertInstanceOf(Exiftool::class, $adapter); } - /** - * @group reader - */ + #[Group('reader')] public function testFactoryAdapterTypeFFprobe() { $reader = Reader::factory(\PHPExif\Enum\ReaderType::FFPROBE); @@ -94,9 +81,7 @@ public function testFactoryAdapterTypeFFprobe() } - /** - * @group reader - */ + #[Group('reader')] public function testFactoryAdapterTypeImageMagick() { $reader = Reader::factory(\PHPExif\Enum\ReaderType::IMAGICK); diff --git a/tests/files/gpano.jpg b/tests/files/gpano.jpg new file mode 100644 index 0000000..9b610fd Binary files /dev/null and b/tests/files/gpano.jpg differ