Skip to content

Commit 1c34f7f

Browse files
kamil4ildyria
andauthored
Various fixes (#51)
* Suppress errors from exif_read_data * Extract IPTC tags in the ImageMagick adapter * Fix support for XResolution/YResolution in the ImageMagick adapter * Support incomplete GPS data in the ImageMagick adapter * Add more comprehensive testing of ImageMagick Co-authored-by: Benoît Viguier <[email protected]>
1 parent 2bd2d34 commit 1c34f7f

File tree

7 files changed

+220
-15
lines changed

7 files changed

+220
-15
lines changed

lib/PHPExif/Adapter/ImageMagick.php

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Imagick;
1616

1717
use function Safe\filesize;
18+
use function Safe\iptcparse;
1819
use function Safe\mime_content_type;
1920

2021
/**
@@ -31,6 +32,23 @@ class ImageMagick extends AdapterAbstract
3132

3233
protected string $mapperClass = '\\PHPExif\\Mapper\\ImageMagick';
3334

35+
/**
36+
* Contains the mapping of names to IPTC field numbers
37+
*/
38+
protected array $iptcMapping = array(
39+
'iptc:title' => '2#005',
40+
'iptc:keywords' => '2#025',
41+
'iptc:copyright' => '2#116',
42+
'iptc:caption' => '2#120',
43+
'iptc:headline' => '2#105',
44+
'iptc:credit' => '2#110',
45+
'iptc:source' => '2#115',
46+
'iptc:jobtitle' => '2#085',
47+
'iptc:city' => '2#090',
48+
'iptc:sublocation' => '2#092',
49+
'iptc:state' => '2#095',
50+
'iptc:country' => '2#101'
51+
);
3452

3553
/**
3654
* Reads & parses the EXIF data from given file
@@ -57,8 +75,13 @@ public function getExifFromFile(string $file) : Exif
5775
'width' => $data_width,
5876
'height' => $data_height
5977
];
78+
$profiles = $im->getImageProfiles('iptc');
79+
$data_iptc = [];
80+
if (array_key_exists('iptc', $profiles)) {
81+
$data_iptc = $this->getIptcData($profiles['iptc']);
82+
}
6083

61-
$data = array_merge($data_exif, $additional_data);
84+
$data = array_merge($data_exif, $data_iptc, $additional_data);
6285

6386
// map the data:
6487
$mapper = $this->getMapper();
@@ -72,4 +95,30 @@ public function getExifFromFile(string $file) : Exif
7295

7396
return $exif;
7497
}
98+
99+
/**
100+
* Returns an array of IPTC data
101+
*
102+
* @param string $profile Raw IPTC data
103+
* @return array
104+
*/
105+
public function getIptcData(string $profile) : array
106+
{
107+
$arrData = [];
108+
$iptc = iptcparse($profile);
109+
110+
foreach ($this->iptcMapping as $name => $field) {
111+
if (!isset($iptc[$field])) {
112+
continue;
113+
}
114+
115+
if (count($iptc[$field]) === 1) {
116+
$arrData[$name] = reset($iptc[$field]);
117+
} else {
118+
$arrData[$name] = $iptc[$field];
119+
}
120+
}
121+
122+
return $arrData;
123+
}
75124
}

lib/PHPExif/Adapter/Native.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,17 @@ public function getExifFromFile(string $file) : Exif
195195
$sections = implode(',', $sections);
196196
$sections = $sections === '' ? null : $sections;
197197

198-
try {
199-
$data = exif_read_data(
200-
$file,
201-
$sections,
202-
$this->getSectionsAsArrays(),
203-
$this->getIncludeThumbnail()
204-
);
205-
} catch (\Throwable) {
206-
$data = false;
207-
}
198+
// exif_read_data raises E_WARNING/E_NOTICE errors for unsupported
199+
// tags, which could result in exceptions being thrown, even though
200+
// the function would otherwise succeed to return valid tags.
201+
// We explicitly disable this undesirable behavior.
202+
// @phpstan-ignore-next-line
203+
$data = @exif_read_data(
204+
$file,
205+
$sections,
206+
$this->getSectionsAsArrays(),
207+
$this->getIncludeThumbnail()
208+
);
208209

209210
// exif_read_data failed to read exif data (i.e. not a jpg/tiff)
210211
if (false === $data) {

lib/PHPExif/Mapper/ImageMagick.php

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ class ImageMagick implements MapperInterface
5757
const SOFTWARE = 'exif:Software';
5858
const XRESOLUTION = 'exif:XResolution';
5959
const YRESOLUTION = 'exif:YResolution';
60+
const TITLE = 'iptc:title';
61+
const KEYWORDS = 'iptc:keywords';
62+
const COPYRIGHT = 'iptc:copyright';
63+
const CAPTION = 'iptc:caption';
64+
const HEADLINE = 'iptc:headline';
65+
const CREDIT = 'iptc:credit';
66+
const SOURCE = 'iptc:source';
67+
const JOBTITLE = 'iptc:jobtitle';
68+
const CITY = 'iptc:city';
69+
const SUBLOCATION = 'iptc:sublocation';
70+
const STATE = 'iptc:state';
71+
const COUNTRY = 'iptc:country';
72+
6073

6174
/**
6275
* Maps the ExifTool fields to the fields of
@@ -91,8 +104,20 @@ class ImageMagick implements MapperInterface
91104
self::MODEL => Exif::CAMERA,
92105
self::ORIENTATION => Exif::ORIENTATION,
93106
self::SOFTWARE => Exif::SOFTWARE,
107+
self::XRESOLUTION => Exif::HORIZONTAL_RESOLUTION,
94108
self::YRESOLUTION => Exif::VERTICAL_RESOLUTION,
95-
109+
self::TITLE => Exif::TITLE,
110+
self::KEYWORDS => Exif::KEYWORDS,
111+
self::COPYRIGHT => Exif::COPYRIGHT,
112+
self::CAPTION => Exif::CAPTION,
113+
self::HEADLINE => Exif::HEADLINE,
114+
self::CREDIT => Exif::CREDIT,
115+
self::SOURCE => Exif::SOURCE,
116+
self::JOBTITLE => EXIF::JOB_TITLE,
117+
self::CITY => Exif::CITY,
118+
self::SUBLOCATION => Exif::SUBLOCATION,
119+
self::STATE => Exif::STATE,
120+
self::COUNTRY => Exif::COUNTRY
96121

97122
);
98123

@@ -183,6 +208,11 @@ public function mapRawData(array $data) : array
183208
case self::ISO:
184209
$value = preg_split('/([\s,]+)/', $value)[0];
185210
break;
211+
case self::XRESOLUTION:
212+
case self::YRESOLUTION:
213+
$resolutionParts = explode('/', $value);
214+
$value = (int) reset($resolutionParts);
215+
break;
186216
case self::GPSLATITUDE:
187217
$value = $this->extractGPSCoordinates($value);
188218
if ($value === false) {
@@ -227,6 +257,11 @@ public function mapRawData(array $data) : array
227257
continue 2;
228258
}
229259
break;
260+
case self::KEYWORDS:
261+
if (!is_array($value)) {
262+
$value = [$value];
263+
}
264+
break;
230265
}
231266
// set end result
232267
$mappedData[$key] = $value;
@@ -250,13 +285,13 @@ protected function extractGPSCoordinates(string $coordinates) : float|false
250285
if (is_numeric($coordinates) === true) {
251286
return ((float) $coordinates);
252287
} else {
253-
$m = '!^([0-9]+\/[1-9][0-9]*), ([0-9]+\/[1-9][0-9]*), ([0-9]+\/[1-9][0-9]*)!';
288+
$m = '!^([0-9]+\/[1-9][0-9]*)(?:, ([0-9]+\/[1-9][0-9]*))?(?:, ([0-9]+\/[1-9][0-9]*))?$!';
254289
if (preg_match($m, $coordinates, $matches) === 0) {
255290
return false;
256291
}
257292
$degrees = $this->normalizeComponent($matches[1]);
258-
$minutes = $this->normalizeComponent($matches[2]);
259-
$seconds = $this->normalizeComponent($matches[3]);
293+
$minutes = $this->normalizeComponent($matches[2] ?? 0);
294+
$seconds = $this->normalizeComponent($matches[3] ?? 0);
260295
if ($degrees === false || $minutes === false || $seconds === false) {
261296
return false;
262297
}

lib/PHPExif/Mapper/Native.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,18 @@ public function mapRawData(array $data) : array
276276
continue 2;
277277
}
278278
break;
279+
// Merge sources of keywords
280+
case self::KEYWORDS:
281+
case self::SUBJECT:
282+
$xval = is_array($value) ? $value : [$value];
283+
if (!array_key_exists(Exif::KEYWORDS, $mappedData)) {
284+
$mappedData[Exif::KEYWORDS] = $xval;
285+
} else {
286+
$tmp = array_values(array_unique(array_merge($mappedData[Exif::KEYWORDS], $xval)));
287+
$mappedData[Exif::KEYWORDS] = $tmp;
288+
}
289+
290+
continue 2;
279291
case self::LENS_LR:
280292
if (!array_key_exists(Exif::LENS, $mappedData)) {
281293
$mappedData[Exif::LENS] = $value;

tests/PHPExif/ExifTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,10 +871,12 @@ public function testAdapterConsistency()
871871
);
872872

873873
$adapter_exiftool = new \PHPExif\Adapter\Exiftool();
874+
$adapter_imagemagick = new \PHPExif\Adapter\ImageMagick();
874875
$adapter_native = new \PHPExif\Adapter\Native();
875876

876877
foreach ($testfiles as $file) {
877878
$result_exiftool = $adapter_exiftool->getExifFromFile($file);
879+
$result_imagemagick = $adapter_imagemagick->getExifFromFile($file);
878880
$result_native = $adapter_native->getExifFromFile($file);
879881

880882
// find all Getter methods on the results and compare its output
@@ -888,6 +890,11 @@ public function testAdapterConsistency()
888890
call_user_func(array($result_exiftool, $name)),
889891
'Adapter difference detected in method "' . $name . '" on image "' . basename($file) . '"'
890892
);
893+
$this->assertEquals(
894+
call_user_func(array($result_native, $name)),
895+
call_user_func(array($result_imagemagick, $name)),
896+
'Adapter difference detected in method "' . $name . '" on image "' . basename($file) . '"'
897+
);
891898
}
892899
}
893900
}

tests/PHPExif/Mapper/ImageMagickMapperTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,5 +605,69 @@ public function testNormalizeComponentCorrectly()
605605
}
606606
}
607607

608+
/**
609+
* @group mapper
610+
* @covers \PHPExif\Mapper\ImageMagick::mapRawData
611+
*/
612+
public function testMapRawDataCorrectlyKeywords()
613+
{
614+
$rawData = array(
615+
\PHPExif\Mapper\ImageMagick::KEYWORDS => 'Keyword_1 Keyword_2',
616+
);
617+
618+
$mapped = $this->mapper->mapRawData($rawData);
619+
620+
$this->assertEquals(
621+
['Keyword_1 Keyword_2'],
622+
reset($mapped)
623+
);
624+
}
625+
626+
/**
627+
* @group mapper
628+
* @covers \PHPExif\Mapper\ImageMagick::mapRawData
629+
*/
630+
public function testMapRawDataCorrectlyKeywordsAndSubject()
631+
{
632+
$rawData = array(
633+
\PHPExif\Mapper\ImageMagick::KEYWORDS => array('Keyword_1', 'Keyword_2'),
634+
);
635+
636+
$mapped = $this->mapper->mapRawData($rawData);
637+
638+
$this->assertEquals(
639+
array('Keyword_1' ,'Keyword_2'),
640+
reset($mapped)
641+
);
642+
}
608643

644+
/**
645+
* @group mapper
646+
* @covers \PHPExif\Mapper\ImageMagick::mapRawData
647+
*/
648+
public function testMapRawDataCorrectlyFormatsXResolution()
649+
{
650+
$rawData = array(
651+
\PHPExif\Mapper\ImageMagick::XRESOLUTION => '1500/300',
652+
);
653+
654+
$mapped = $this->mapper->mapRawData($rawData);
655+
656+
$this->assertEquals(1500, reset($mapped));
657+
}
658+
659+
/**
660+
* @group mapper
661+
* @covers \PHPExif\Mapper\ImageMagick::mapRawData
662+
*/
663+
public function testMapRawDataCorrectlyFormatsYResolution()
664+
{
665+
$rawData = array(
666+
\PHPExif\Mapper\ImageMagick::YRESOLUTION => '1500/300',
667+
);
668+
669+
$mapped = $this->mapper->mapRawData($rawData);
670+
671+
$this->assertEquals(1500, reset($mapped));
672+
}
609673
}

tests/PHPExif/Mapper/NativeMapperTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,41 @@ public function testMapRawDataCorrectlyLensData2()
546546
);
547547
}
548548
}
549+
550+
/**
551+
* @group mapper
552+
* @covers \PHPExif\Mapper\Native::mapRawData
553+
*/
554+
public function testMapRawDataCorrectlyKeywords()
555+
{
556+
$rawData = array(
557+
\PHPExif\Mapper\Native::KEYWORDS => 'Keyword_1 Keyword_2',
558+
);
559+
560+
$mapped = $this->mapper->mapRawData($rawData);
561+
562+
$this->assertEquals(
563+
['Keyword_1 Keyword_2'],
564+
reset($mapped)
565+
);
566+
}
567+
568+
/**
569+
* @group mapper
570+
* @covers \PHPExif\Mapper\Native::mapRawData
571+
*/
572+
public function testMapRawDataCorrectlyKeywordsAndSubject()
573+
{
574+
$rawData = array(
575+
\PHPExif\Mapper\Native::KEYWORDS => array('Keyword_1', 'Keyword_2'),
576+
\PHPExif\Mapper\Native::SUBJECT => array('Keyword_1', 'Keyword_3'),
577+
);
578+
579+
$mapped = $this->mapper->mapRawData($rawData);
580+
581+
$this->assertEquals(
582+
array('Keyword_1' ,'Keyword_2', 'Keyword_3'),
583+
reset($mapped)
584+
);
585+
}
549586
}

0 commit comments

Comments
 (0)