Skip to content

Commit a6d58bb

Browse files
committed
GPS detection
1 parent cc31d78 commit a6d58bb

File tree

6 files changed

+333
-12
lines changed

6 files changed

+333
-12
lines changed

lib/PHPExif/Adapter/Exiftool.php

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,15 @@ public function getToolPath()
9696
*/
9797
public function getExifFromFile($file)
9898
{
99+
$gpsFormat = '%d deg %d\' %.4f\"';
100+
99101
$result = $this->getCliOutput(
100102
sprintf(
101-
'%1$s%3$s -j %2$s',
103+
'%1$s%3$s -j -c "%4$s" %2$s',
102104
$this->getToolPath(),
103105
$file,
104-
$this->numeric ? ' -n' : ''
106+
$this->numeric ? ' -n' : '',
107+
$gpsFormat
105108
)
106109
);
107110

@@ -172,6 +175,33 @@ public function mapData(array $source)
172175
$caption = $source['Caption-Abstract'];
173176
}
174177

178+
$gpsLocation = false;
179+
if (isset($source['GPSLatitudeRef']) && isset($source['GPSLongitudeRef'])) {
180+
$latitude = $this->extractGPSCoordinates($source['GPSLatitude']);
181+
$longitude = $this->extractGPSCoordinates($source['GPSLongitude']);
182+
183+
if ($latitude !== false && $longitude !== false) {
184+
$gpsLocation = array();
185+
186+
$gpsLocation['latitude'] = array_merge(
187+
$latitude,
188+
array(strtoupper($source['GPSLatitudeRef'][0]))
189+
);
190+
$gpsLocation['longitude'] = array_merge(
191+
$longitude,
192+
array(strtoupper($source['GPSLongitudeRef'][0]))
193+
);
194+
195+
if (isset($source['GPSAltitudeRef'])
196+
&& preg_match('!^([0-9]+) m!', $source['GPSAltitude'], $matches)) {
197+
$gpsLocation['altitude'] = array(
198+
$matches[1],
199+
preg_match('!^Above!', $source['GPSAltitudeRef']) ? 0 : -1,
200+
);
201+
}
202+
}
203+
}
204+
175205
return array(
176206
Exif::APERTURE => (!isset($source['Aperture'])) ?
177207
false : sprintf('f/%01.1f', $source['Aperture']),
@@ -201,6 +231,37 @@ public function mapData(array $source)
201231
Exif::TITLE => (!isset($source['Title'])) ? false : $source['Title'],
202232
Exif::VERTICAL_RESOLUTION => (!isset($source['YResolution'])) ? false : $source['YResolution'],
203233
Exif::WIDTH => (!isset($source['ImageWidth'])) ? false : $source['ImageWidth'],
234+
Exif::GPS => $gpsLocation,
204235
);
205236
}
237+
238+
/**
239+
* Extract GPS coordinates from formattedstring
240+
*
241+
* @param string $coordinates
242+
* @return array
243+
*/
244+
protected function extractGPSCoordinates($coordinates)
245+
{
246+
if ($this->numeric === true) {
247+
$coordinates *= (int) $coordinates < 0 ? -1 : 1;
248+
249+
$degrees = (int) $coordinates;
250+
$decimalMinutes = ($coordinates - $degrees) * 60;
251+
$minutes = (int) $decimalMinutes;
252+
$seconds = round(($decimalMinutes - $minutes) * 60, 6);
253+
254+
return array(
255+
$degrees,
256+
$minutes,
257+
$seconds,
258+
);
259+
} else {
260+
if (!preg_match('!^([0-9.]+) deg ([0-9.]+)\' ([0-9.]+)"!', $coordinates, $matches)) {
261+
return false;
262+
}
263+
264+
return array_slice($matches, 1);
265+
}
266+
}
206267
}

lib/PHPExif/Adapter/Native.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,26 @@ public function mapData(array $source)
262262
$exposureTime = '1/' . round($denominator);
263263
}
264264

265+
$gpsLocation = false;
266+
if (isset($source['GPSLatitudeRef']) && isset($source['GPSLongitudeRef'])) {
267+
$gpsLocation = array();
268+
269+
$gpsLocation['latitude'] = array_merge(
270+
$this->normalizeGPSCoordinate($source['GPSLatitude']),
271+
array(strtoupper($source['GPSLatitudeRef']))
272+
);
273+
$gpsLocation['longitude'] = array_merge(
274+
$this->normalizeGPSCoordinate($source['GPSLongitude']),
275+
array(strtoupper($source['GPSLongitudeRef']))
276+
);
277+
278+
if (isset($source['GPSAltitudeRef'])) {
279+
$altitude = $this->normalizeGPSCoordinate(array($source['GPSAltitude']));
280+
281+
$gpsLocation['altitude'] = array($altitude[0], (int) $source['GPSAltitudeRef']);
282+
}
283+
}
284+
265285
return array(
266286
Exif::APERTURE => (!isset($source[self::SECTION_COMPUTED]['ApertureFNumber'])) ?
267287
false : $source[self::SECTION_COMPUTED]['ApertureFNumber'],
@@ -301,6 +321,7 @@ public function mapData(array $source)
301321
Exif::VERTICAL_RESOLUTION => $vertResolution,
302322
Exif::WIDTH => (!isset($source[self::SECTION_COMPUTED]['Width'])) ?
303323
false : $source[self::SECTION_COMPUTED]['Width'],
324+
Exif::GPS => $gpsLocation,
304325
);
305326

306327
$arrMapping = array(
@@ -345,4 +366,21 @@ public function mapData(array $source)
345366

346367
return $mappedData;
347368
}
369+
370+
/**
371+
* Normalize array GPS coordinates
372+
*
373+
* @param array $coordinates
374+
* @return array
375+
*/
376+
protected function normalizeGPSCoordinate(array $coordinates)
377+
{
378+
return array_map(
379+
function ($component) {
380+
$parts = explode('/', $component);
381+
return count($parts) === 1 ? $parts[0] : (int) reset($parts) / (int) end($parts);
382+
},
383+
$coordinates
384+
);
385+
}
348386
}

lib/PHPExif/Exif.php

Lines changed: 124 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Exif
4747
const TITLE = 'title';
4848
const VERTICAL_RESOLUTION = 'verticalResolution';
4949
const WIDTH = 'width';
50+
const GPS = 'gps';
5051

5152
/**
5253
* The mapped EXIF data
@@ -427,7 +428,7 @@ public function getCreationDate()
427428

428429
return $this->data[self::CREATION_DATE];
429430
}
430-
431+
431432
/**
432433
* Returns the colorspace, if it exists
433434
*
@@ -438,10 +439,10 @@ public function getColorSpace()
438439
if (!isset($this->data[self::COLORSPACE])) {
439440
return false;
440441
}
441-
442+
442443
return $this->data[self::COLORSPACE];
443444
}
444-
445+
445446
/**
446447
* Returns the mimetype, if it exists
447448
*
@@ -452,21 +453,21 @@ public function getMimeType()
452453
if (!isset($this->data[self::MIMETYPE])) {
453454
return false;
454455
}
455-
456+
456457
return $this->data[self::MIMETYPE];
457458
}
458-
459+
459460
/**
460461
* Returns the filesize, if it exists
461-
*
462+
*
462463
* @return integer
463464
*/
464465
public function getFileSize()
465466
{
466467
if (!isset($this->data[self::FILESIZE])) {
467468
return false;
468469
}
469-
470+
470471
return $this->data[self::FILESIZE];
471472
}
472473

@@ -483,4 +484,120 @@ public function getOrientation()
483484

484485
return $this->data[self::ORIENTATION];
485486
}
487+
488+
/**
489+
* Returns raw GPS coordinates, if it exists
490+
*
491+
* @return array|boolean
492+
*/
493+
public function getGPS()
494+
{
495+
if (!isset($this->data[self::GPS])) {
496+
return false;
497+
}
498+
499+
return $this->data[self::GPS];
500+
}
501+
502+
/**
503+
* Returns GPS in degrees, minutes and seconds, if it exists
504+
*
505+
* @return string|boolean
506+
*/
507+
public function getGPSDegMinSec()
508+
{
509+
return $this->getFormattedGPS('degrees_minutes_seconds');
510+
}
511+
512+
/**
513+
* Returns GPS in degrees, and decimal minutes, if it exists
514+
*
515+
* @return string|boolean
516+
*/
517+
public function getGPSDecMinutes()
518+
{
519+
return $this->getFormattedGPS('decimal_minutes');
520+
}
521+
522+
/**
523+
* Returns GPS in decimal degrees, if it exists
524+
*
525+
* @return string|boolean
526+
*/
527+
public function getGPSDecDegrees()
528+
{
529+
return $this->getFormattedGPS('decimal_degrees');
530+
}
531+
532+
/**
533+
* Returns formatted GPS coordinates, if it exists
534+
*
535+
* @param string $format
536+
* @return string|boolean
537+
*/
538+
public function getFormattedGPS($format = 'decimal_minutes')
539+
{
540+
if (!isset($this->data[self::GPS]) || $this->data[self::GPS] === false) {
541+
return false;
542+
}
543+
544+
if ($format === 'degrees_minutes_seconds') {
545+
$gps = $this->data[self::GPS];
546+
547+
return sprintf(
548+
'%d° %d\' %s" %s, %d° %d\' %s" %s',
549+
$gps['latitude'][0],
550+
$gps['latitude'][1],
551+
$gps['latitude'][2],
552+
$gps['latitude'][3],
553+
$gps['longitude'][0],
554+
$gps['longitude'][1],
555+
$gps['longitude'][2],
556+
$gps['longitude'][3]
557+
);
558+
}
559+
560+
return $this->getGPSDecimal($format);
561+
}
562+
563+
/**
564+
* Returns decimal formatted GPS coordinates, if it exists
565+
*
566+
* @param string $format
567+
* @return string
568+
* @throws \InvalidArgumentException If the the format is not valid
569+
*/
570+
protected function getGPSDecimal($format)
571+
{
572+
$gps = $this->data[self::GPS];
573+
574+
$latMinutes = $gps['latitude'][1] / 60 + $gps['latitude'][2] / 3600;
575+
$lonMinutes = $gps['longitude'][1] / 60 + $gps['longitude'][2] / 3600;
576+
577+
switch ($format) {
578+
case 'decimal_minutes':
579+
return sprintf(
580+
'%d° %f\' %s, %d° %f\' %s',
581+
$gps['latitude'][0],
582+
$latMinutes,
583+
$gps['latitude'][3],
584+
$gps['longitude'][0],
585+
$lonMinutes,
586+
$gps['longitude'][3]
587+
);
588+
break;
589+
590+
case 'decimal_degrees':
591+
return sprintf(
592+
'%f, %f',
593+
($gps['latitude'][3] === 'S' ? -1 : 1) * ($gps['latitude'][0] + $latMinutes),
594+
($gps['longitude'][3] === 'W' ? -1 : 1) * ($gps['longitude'][0] + $lonMinutes)
595+
);
596+
break;
597+
598+
default:
599+
throw new \InvalidArgumentException(sprintf('GPS format "%s" is not valid', $format));
600+
break;
601+
}
602+
}
486603
}

tests/PHPExif/Adapter/ExiftoolTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,59 @@ public function testMapDataFocalLengthIsCalculated()
128128
$this->assertEquals(18, $result[\PHPExif\Exif::FOCAL_LENGTH]);
129129
}
130130

131+
/**
132+
* @group exiftool
133+
* @covers \PHPExif\Adapter\Exiftool::mapData
134+
*/
135+
public function testMapDataCreationDegGPSIsCalculated()
136+
{
137+
$this->adapter->setNumeric(false);
138+
$result = $this->adapter->mapData(
139+
array(
140+
'GPSLatitudeRef' => 'North',
141+
'GPSLatitude' => '40 deg 20\' 0.42857" N',
142+
'GPSLongitudeRef' => 'West',
143+
'GPSLongitude' => '20 deg 10\' 2.33333" W',
144+
'GPSAltitudeRef' => 'Above Sea Level',
145+
'GPSAltitude' => '1 m Above Sea Level'
146+
)
147+
);
148+
149+
$expected = array(
150+
'latitude' => array(40, 20, 0.42857, 'N'),
151+
'longitude' => array(20, 10, 2.33333, 'W'),
152+
'altitude' => array(1, 0),
153+
);
154+
155+
$this->assertEquals($expected, $result[\PHPExif\Exif::GPS]);
156+
}
157+
158+
/**
159+
* @group exiftool
160+
* @covers \PHPExif\Adapter\Exiftool::mapData
161+
*/
162+
public function testMapDataCreationNumericGPSIsCalculated()
163+
{
164+
$result = $this->adapter->mapData(
165+
array(
166+
'GPSLatitudeRef' => 'North',
167+
'GPSLatitude' => '40.333452381',
168+
'GPSLongitudeRef' => 'West',
169+
'GPSLongitude' => '20.167314814',
170+
'GPSAltitudeRef' => 'Below Sea Level',
171+
'GPSAltitude' => '1 m Above Sea Level'
172+
)
173+
);
174+
175+
$expected = array(
176+
'latitude' => array(40, 20, 0.428572, 'N'),
177+
'longitude' => array(20, 10, 2.33333, 'W'),
178+
'altitude' => array(1, -1),
179+
);
180+
181+
$this->assertEquals($expected, $result[\PHPExif\Exif::GPS]);
182+
}
183+
131184
/**
132185
* @group exiftool
133186
* @covers \PHPExif\Adapter\Exiftool::getCliOutput

0 commit comments

Comments
 (0)