From 05bba65e2a476ac18b953367f49dd4b379c12971 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Dec 2024 15:25:23 +0100 Subject: [PATCH 1/3] Make it possible to set default time zone for EXIF / IPTC data --- src/main/php/img/io/ImageMetaData.class.php | 16 ++++++--- .../img/unittest/MetaDataReaderTest.class.php | 35 +++++++++++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/main/php/img/io/ImageMetaData.class.php b/src/main/php/img/io/ImageMetaData.class.php index 2c03cd84..c9af9ee7 100755 --- a/src/main/php/img/io/ImageMetaData.class.php +++ b/src/main/php/img/io/ImageMetaData.class.php @@ -85,9 +85,10 @@ public function imageDimensions() { * Returns an IptcData instance or NULL if this image does not contain * IPTC Data * + * @param ?util.TimeZone $tz * @return ?img.util.IptcData */ - public function iptcData() { + public function iptcData($tz= null) { if (!($seg= $this->segmentsOf(IptcSegment::class))) return null; $data= new IptcData(); @@ -95,7 +96,12 @@ public function iptcData() { // Parse creation date if (3 === sscanf($iptc['2#055'][0] ?? '', '%4d%2d%d', $year, $month, $day)) { - $created= Date::create($year, $month, $day, 0, 0, 0); + if (isset($iptc['2#060'])) { + sscanf($iptc['2#060'][0], '%2d%2d%2d', $hour, $minute, $second); + } else { + $hour= $minute= $second= 0; + } + $created= Date::create($year, $month, $day, $hour, $minute, $second, $tz); } else { $created= null; } @@ -141,9 +147,10 @@ protected static function lookup($exif, ... $keys) { * Returns an ExifData instance or NULL if this image does not contain * EXIF Data * + * @param ?util.TimeZone $tz * @return ?img.util.ExifData */ - public function exifData() { + public function exifData($tz= null) { if (!($seg= $this->segmentsOf(ExifSegment::class))) return null; // Populate ExifData instance from ExifSegment's raw data @@ -205,8 +212,7 @@ public function exifData() { $data->withFlash(self::lookup($exif, 'Flash')); if (null !== ($date= self::lookup($exif, 'DateTimeOriginal', 'DateTimeDigitized', 'DateTime'))) { - $t= sscanf($date, '%4d:%2d:%2d %2d:%2d:%2d'); - $data->withDateTime(new Date(mktime($t[3], $t[4], $t[5], $t[1], $t[2], $t[0]))); + $data->withDateTime(new Date($date, $tz)); } if (null !== ($o= self::lookup($exif, 'Orientation'))) { diff --git a/src/test/php/img/unittest/MetaDataReaderTest.class.php b/src/test/php/img/unittest/MetaDataReaderTest.class.php index a6af9067..26f51c95 100755 --- a/src/test/php/img/unittest/MetaDataReaderTest.class.php +++ b/src/test/php/img/unittest/MetaDataReaderTest.class.php @@ -5,8 +5,8 @@ use img\io\{Segment, CommentSegment, MetaDataReader, SOFNSegment, XMPSegment, ExifSegment, IptcSegment}; use img\util\{ExifData, IptcData}; use lang\ArrayType; -use test\{Assert, Before, Expect, Test}; -use util\Date; +use test\{Assert, Before, Expect, Test, Values}; +use util\{Date, TimeZone}; class MetaDataReaderTest { private $fixture; @@ -18,7 +18,7 @@ class MetaDataReaderTest { * @param string $sub default NULL subpackage * @return io.File */ - protected function resourceAsFile($name, $sub= null) { + private function resourceAsFile($name, $sub= null) { $package= typeof($this)->getPackage(); $container= $sub ? $package->getPackage($sub) : $package; return $container->getResourceAsStream($name); @@ -31,7 +31,7 @@ protected function resourceAsFile($name, $sub= null) { * @param string $sub default NULL subpackage * @return lang.Generic the instance */ - protected function extractFromFile($name, $sub= null) { + private function extractFromFile($name, $sub= null) { return $this->fixture->read($this->resourceAsFile($name, $sub)->in(), $name); } @@ -43,11 +43,18 @@ protected function extractFromFile($name, $sub= null) { * @param var $value The value * @throws unittest.AssertionFailedError */ - protected function assertArrayOf($type, $size, $value) { + private function assertArrayOf($type, $size, $value) { Assert::instance(new ArrayType($type), $value); Assert::equals($size, sizeof($value)); } + /** @return iterable */ + private function timezones() { + yield null; + yield TimeZone::getByName('UTC'); + yield TimeZone::getByName('Europe/Berlin'); + } + #[Before] public function fixture() { $this->fixture= new MetaDataReader(); @@ -519,7 +526,7 @@ public function detailed_iptc_data() { ->withUrgency(null) ->withCategory(null) ->withKeywords(null) - ->withDateCreated(new Date('2011-12-07 00:00:00')) + ->withDateCreated(new Date('2011-12-07 14:08:24')) ->withAuthor(null) ->withAuthorPosition(null) ->withCity(null) @@ -559,4 +566,20 @@ public function gps_data() { ] ); } + + #[Test, Values(from: 'timezones')] + public function iptc_with_timezone($tz) { + Assert::equals( + new Date('2011-12-07 14:08:24', $tz), + $this->extractFromFile('detailed-iptc-embedded.jpg')->iptcData($tz)->dateCreated + ); + } + + #[Test, Values(from: 'timezones')] + public function exif_with_timezone($tz) { + Assert::equals( + new Date('2001:06:09 15:17:32', $tz), + $this->extractFromFile('canon-ixus.jpg', 'exif_org')->exifData($tz)->dateTime + ); + } } \ No newline at end of file From 1cecca8f9342e2a89034d2f2622c2da9168838f2 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Dec 2024 15:31:31 +0100 Subject: [PATCH 2/3] QA: Simplify unittests --- .../img/unittest/MetaDataReaderTest.class.php | 58 +++++++------------ 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/src/test/php/img/unittest/MetaDataReaderTest.class.php b/src/test/php/img/unittest/MetaDataReaderTest.class.php index 26f51c95..762970b3 100755 --- a/src/test/php/img/unittest/MetaDataReaderTest.class.php +++ b/src/test/php/img/unittest/MetaDataReaderTest.class.php @@ -12,40 +12,16 @@ class MetaDataReaderTest { private $fixture; /** - * Returns a file for a classloader resource + * Extract meta data from a given resource * * @param string $name - * @param string $sub default NULL subpackage - * @return io.File + * @param ?string $sub + * @return img.io.ImageMetaData */ - private function resourceAsFile($name, $sub= null) { + private function extractFromFile($name, $sub= null) { $package= typeof($this)->getPackage(); $container= $sub ? $package->getPackage($sub) : $package; - return $container->getResourceAsStream($name); - } - - /** - * Extract from file and return the instance - * - * @param string $name - * @param string $sub default NULL subpackage - * @return lang.Generic the instance - */ - private function extractFromFile($name, $sub= null) { - return $this->fixture->read($this->resourceAsFile($name, $sub)->in(), $name); - } - - /** - * Assertion helper - * - * @param string $type The expected type - * @param int $size The expected size - * @param var $value The value - * @throws unittest.AssertionFailedError - */ - private function assertArrayOf($type, $size, $value) { - Assert::instance(new ArrayType($type), $value); - Assert::equals($size, sizeof($value)); + return $this->fixture->read($container->getResourceAsStream($name)->in(), $name); } /** @return iterable */ @@ -72,12 +48,18 @@ public function empty_file() { #[Test] public function all_segments() { - $this->assertArrayOf(Segment::class, 9, $this->extractFromFile('1x1.jpg')->allSegments()); + $segments= $this->extractFromFile('1x1.jpg')->allSegments(); + + Assert::instance('img.io.Segment[]', $segments); + Assert::equals(9, sizeof($segments)); } #[Test] public function segments_named_dqt() { - $this->assertArrayOf(Segment::class, 2, $this->extractFromFile('1x1.jpg')->segmentsNamed('DQT')); + $segments= $this->extractFromFile('1x1.jpg')->segmentsNamed('DQT'); + + Assert::instance('img.io.Segment[]', $segments); + Assert::equals(2, sizeof($segments)); } #[Test] @@ -107,8 +89,8 @@ public function com_segment() { #[Test] public function xmp_segment() { $segments= $this->extractFromFile('xmp.jpg')->segmentsOf(XMPSegment::class); - $this->assertArrayOf(XMPSegment::class, 1, $segments); + Assert::instance('img.io.XMPSegment[]', $segments); Assert::matches('/^<.+/', $segments[0]->source); Assert::instance(DOMDocument::class, $segments[0]->document()); } @@ -145,16 +127,16 @@ public function no_iptc_data_in_1x1() { #[Test] public function exif_data_segments() { - $this->assertArrayOf( - ExifSegment::class, 1, + Assert::instance( + 'img.io.ExifSegment[]', $this->extractFromFile('exif-only.jpg')->segmentsOf(ExifSegment::class) ); } #[Test] public function iptc_data_segments() { - $this->assertArrayOf( - IptcSegment::class, 1, + Assert::instance( + 'img.io.IptcSegment[]', $this->extractFromFile('iptc-only.jpg')->segmentsOf(IptcSegment::class) ); } @@ -162,8 +144,8 @@ public function iptc_data_segments() { #[Test] public function exif_and_iptc_data_segments() { $meta= $this->extractFromFile('exif-and-iptc.jpg'); - $this->assertArrayOf(ExifSegment::class, 1, $meta->segmentsOf(ExifSegment::class)); - $this->assertArrayOf(IptcSegment::class, 1, $meta->segmentsOf(IptcSegment::class)); + Assert::instance('img.io.ExifSegment[]', $meta->segmentsOf(ExifSegment::class)); + Assert::instance('img.io.IptcSegment[]', $meta->segmentsOf(IptcSegment::class)); } #[Test] From bff076e4a152fd651172b4ad12206d6449cd72cc Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 22 Dec 2024 15:42:13 +0100 Subject: [PATCH 3/3] QA: Remove unused import --- src/test/php/img/unittest/MetaDataReaderTest.class.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/php/img/unittest/MetaDataReaderTest.class.php b/src/test/php/img/unittest/MetaDataReaderTest.class.php index 762970b3..9015fda2 100755 --- a/src/test/php/img/unittest/MetaDataReaderTest.class.php +++ b/src/test/php/img/unittest/MetaDataReaderTest.class.php @@ -4,7 +4,6 @@ use img\ImagingException; use img\io\{Segment, CommentSegment, MetaDataReader, SOFNSegment, XMPSegment, ExifSegment, IptcSegment}; use img\util\{ExifData, IptcData}; -use lang\ArrayType; use test\{Assert, Before, Expect, Test, Values}; use util\{Date, TimeZone};