12
12
namespace PHPExif \Adapter ;
13
13
14
14
use PHPExif \Exif ;
15
+ use FFMpeg ;
15
16
16
17
/**
17
18
* PHP Exif Native Reader Adapter
@@ -173,23 +174,40 @@ public function getSectionsAsArrays()
173
174
*/
174
175
public function getExifFromFile ($ file )
175
176
{
176
- $ sections = $ this ->getRequiredSections ();
177
- $ sections = implode (', ' , $ sections );
178
- $ sections = (empty ($ sections )) ? null : $ sections ;
179
-
180
- $ data = @exif_read_data (
181
- $ file ,
182
- $ sections ,
183
- $ this ->getSectionsAsArrays (),
184
- $ this ->getIncludeThumbnail ()
185
- );
186
-
187
- if (false === $ data ) {
188
- return false ;
189
- }
177
+ $ mimeType = mime_content_type ($ file );
178
+
179
+ if (strpos ($ mimeType , 'video ' ) !== 0 ) {
180
+
181
+ // Photo
182
+ $ sections = $ this ->getRequiredSections ();
183
+ $ sections = implode (', ' , $ sections );
184
+ $ sections = (empty ($ sections )) ? null : $ sections ;
185
+
186
+ $ data = @exif_read_data (
187
+ $ file ,
188
+ $ sections ,
189
+ $ this ->getSectionsAsArrays (),
190
+ $ this ->getIncludeThumbnail ()
191
+ );
192
+
193
+ if (false === $ data ) {
194
+ return false ;
195
+ }
196
+
197
+ $ xmpData = $ this ->getIptcData ($ file );
198
+ $ data = array_merge ($ data , array (self ::SECTION_IPTC => $ xmpData ));
190
199
191
- $ xmpData = $ this ->getIptcData ($ file );
192
- $ data = array_merge ($ data , array (self ::SECTION_IPTC => $ xmpData ));
200
+ } else {
201
+ // Video
202
+ try {
203
+
204
+ $ data = $ this ->getVideoData ($ file );
205
+ $ data ['MimeType ' ] = $ mimeType ;
206
+
207
+ } catch (Exception $ exception ) {
208
+ Logs::error (__METHOD__ , __LINE__ , $ exception ->getMessage ());
209
+ }
210
+ }
193
211
194
212
// map the data:
195
213
$ mapper = $ this ->getMapper ();
@@ -204,6 +222,164 @@ public function getExifFromFile($file)
204
222
return $ exif ;
205
223
}
206
224
225
+ /**
226
+ * Returns an array of video data
227
+ *
228
+ * @param string $file The file to read the video data from
229
+ * @return array
230
+ */
231
+ public function getVideoData ($ filename )
232
+ {
233
+
234
+ $ metadata ['FileSize ' ] = filesize ($ filename );
235
+
236
+ $ path_ffmpeg = exec ('which ffmpeg ' );
237
+ $ path_ffprobe = exec ('which ffprobe ' );
238
+ $ ffprobe = FFMpeg \FFProbe::create (array (
239
+ 'ffmpeg.binaries ' => $ path_ffmpeg ,
240
+ 'ffprobe.binaries ' => $ path_ffprobe ,
241
+ ));
242
+
243
+ $ stream = $ ffprobe ->streams ($ filename )->videos ()->first ()->all ();
244
+ $ format = $ ffprobe ->format ($ filename )->all ();
245
+ if (isset ($ stream ['width ' ])) {
246
+ $ metadata ['Width ' ] = $ stream ['width ' ];
247
+ }
248
+ if (isset ($ stream ['height ' ])) {
249
+ $ metadata ['Height ' ] = $ stream ['height ' ];
250
+ }
251
+ if (isset ($ stream ['tags ' ]) && isset ($ stream ['tags ' ]['rotate ' ]) && ($ stream ['tags ' ]['rotate ' ] === '90 ' || $ stream ['tags ' ]['rotate ' ] === '270 ' )) {
252
+ $ tmp = $ metadata ['Width ' ];
253
+ $ metadata ['Width ' ] = $ metadata ['Height ' ];
254
+ $ metadata ['Height ' ] = $ tmp ;
255
+ }
256
+ if (isset ($ stream ['avg_frame_rate ' ])) {
257
+ $ framerate = explode ('/ ' , $ stream ['avg_frame_rate ' ]);
258
+ if (count ($ framerate ) == 1 ) {
259
+ $ framerate = $ framerate [0 ];
260
+ } elseif (count ($ framerate ) == 2 && $ framerate [1 ] != 0 ) {
261
+ $ framerate = number_format ($ framerate [0 ] / $ framerate [1 ], 3 );
262
+ } else {
263
+ $ framerate = '' ;
264
+ }
265
+ if ($ framerate !== '' ) {
266
+ $ metadata ['framerate ' ] = $ framerate ;
267
+ }
268
+ }
269
+ if (isset ($ format ['duration ' ])) {
270
+ $ metadata ['duration ' ] = number_format ($ format ['duration ' ], 3 );
271
+ }
272
+ if (isset ($ format ['tags ' ])) {
273
+ if (isset ($ format ['tags ' ]['creation_time ' ]) && strtotime ($ format ['tags ' ]['creation_time ' ]) !== 0 ) {
274
+ $ metadata ['DateTimeOriginal ' ] = date ('Y-m-d H:i:s ' , strtotime ($ format ['tags ' ]['creation_time ' ]));
275
+ }
276
+ if (isset ($ format ['tags ' ]['location ' ])) {
277
+ $ matches = [];
278
+ preg_match ('/^([+-][0-9\.]+)([+-][0-9\.]+)\/$/ ' , $ format ['tags ' ]['location ' ], $ matches );
279
+ if (count ($ matches ) == 3 &&
280
+ !preg_match ('/^\+0+\.0+$/ ' , $ matches [1 ]) &&
281
+ !preg_match ('/^\+0+\.0+$/ ' , $ matches [2 ])) {
282
+ $ metadata ['GPSLatitude ' ] = $ matches [1 ];
283
+ $ metadata ['GPSLongitude ' ] = $ matches [2 ];
284
+ }
285
+ }
286
+ // QuickTime File Format defines several additional metadata
287
+ // Source: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html
288
+ // Special case: iPhones write into tags->creation_time the creation time of the file
289
+ // -> When converting the video from HEVC (iOS Video format) to MOV, the creation_time
290
+ // is the time when the mov file was created, not when the video was shot (fixed in iOS12)
291
+ // (see e.g. https://michaelkummer.com/tech/apple/photos-videos-wrong-date/ (for the symptom)
292
+ // Solution: Use com.apple.quicktime.creationdate which is the true creation date of the video
293
+ if (isset ($ format ['tags ' ]['com.apple.quicktime.creationdate ' ])) {
294
+ $ metadata ['DateTimeOriginal ' ] = date ('Y-m-d H:i:s ' , strtotime ($ format ['tags ' ]['com.apple.quicktime.creationdate ' ]));
295
+ }
296
+ if (isset ($ format ['tags ' ]['com.apple.quicktime.description ' ])) {
297
+ $ metadata ['description ' ] = $ format ['tags ' ]['com.apple.quicktime.description ' ];
298
+ }
299
+ if (isset ($ format ['tags ' ]['com.apple.quicktime.title ' ])) {
300
+ $ metadata ['title ' ] = $ format ['tags ' ]['com.apple.quicktime.title ' ];
301
+ }
302
+ if (isset ($ format ['tags ' ]['com.apple.quicktime.keywords ' ])) {
303
+ $ metadata ['keywords ' ] = $ format ['tags ' ]['com.apple.quicktime.keywords ' ];
304
+ }
305
+ if (isset ($ format ['tags ' ]['com.apple.quicktime.location.ISO6709 ' ])) {
306
+ $ location_data = $ this ->readISO6709 ($ format ['tags ' ]['com.apple.quicktime.location.ISO6709 ' ]);
307
+ $ metadata ['GPSLatitude ' ] = $ location_data ['latitude ' ];
308
+ $ metadata ['GPSLongitude ' ] = $ location_data ['longitude ' ];
309
+ $ metadata ['GPSAltitude ' ] = $ location_data ['altitude ' ];
310
+ }
311
+ // Not documented, but available on iPhone videos
312
+ if (isset ($ format ['tags ' ]['com.apple.quicktime.make ' ])) {
313
+ $ metadata ['Make ' ] = $ format ['tags ' ]['com.apple.quicktime.make ' ];
314
+ }
315
+ // Not documented, but available on iPhone videos
316
+ if (isset ($ format ['tags ' ]['com.apple.quicktime.model ' ])) {
317
+ $ metadata ['Model ' ] = $ format ['tags ' ]['com.apple.quicktime.model ' ];
318
+ }
319
+ }
320
+
321
+ return $ metadata ;
322
+ }
323
+
324
+ /**
325
+ * Converts results of ISO6709 parsing
326
+ * to decimal format for latitude and longitude
327
+ * See https://github.com/seanson/python-iso6709.git.
328
+ *
329
+ * @param string sign
330
+ * @param string degrees
331
+ * @param string minutes
332
+ * @param string seconds
333
+ * @param string fraction
334
+ *
335
+ * @return float
336
+ */
337
+ private function convertDMStoDecimal (string $ sign , string $ degrees , string $ minutes , string $ seconds , string $ fraction ): float
338
+ {
339
+ if ($ fraction !== '' ) {
340
+ if ($ seconds !== '' ) {
341
+ $ seconds = $ seconds . $ fraction ;
342
+ } elseif ($ minutes !== '' ) {
343
+ $ minutes = $ minutes . $ fraction ;
344
+ } else {
345
+ $ degrees = $ degrees . $ fraction ;
346
+ }
347
+ }
348
+ $ decimal = floatval ($ degrees ) + floatval ($ minutes ) / 60.0 + floatval ($ seconds ) / 3600.0 ;
349
+ if ($ sign == '- ' ) {
350
+ $ decimal = -1.0 * $ decimal ;
351
+ }
352
+ return $ decimal ;
353
+ }
354
+
355
+ /**
356
+ * Returns the latitude, longitude and altitude
357
+ * of a GPS coordiante formattet with ISO6709
358
+ * See https://github.com/seanson/python-iso6709.git.
359
+ *
360
+ * @param string val_ISO6709
361
+ *
362
+ * @return array
363
+ */
364
+ private function readISO6709 (string $ val_ISO6709 ): array
365
+ {
366
+ $ return = [
367
+ 'latitude ' => null ,
368
+ 'longitude ' => null ,
369
+ 'altitude ' => null ,
370
+ ];
371
+ $ matches = [];
372
+ // Adjustment compared to https://github.com/seanson/python-iso6709.git
373
+ // Altitude have format +XX.XXXX -> Adjustment for decimal
374
+ preg_match ('/^(?<lat_sign>\+|-)(?<lat_degrees>[0,1]?\d{2})(?<lat_minutes>\d{2}?)?(?<lat_seconds>\d{2}?)?(?<lat_fraction>\.\d+)?(?<lng_sign>\+|-)(?<lng_degrees>[0,1]?\d{2})(?<lng_minutes>\d{2}?)?(?<lng_seconds>\d{2}?)?(?<lng_fraction>\.\d+)?(?<alt>[\+\-][0-9]\d*(\.\d+)?)?\/$/ ' , $ val_ISO6709 , $ matches );
375
+ $ return ['latitude ' ] = $ this ->convertDMStoDecimal ($ matches ['lat_sign ' ], $ matches ['lat_degrees ' ], $ matches ['lat_minutes ' ], $ matches ['lat_seconds ' ], $ matches ['lat_fraction ' ]);
376
+ $ return ['longitude ' ] = $ this ->convertDMStoDecimal ($ matches ['lng_sign ' ], $ matches ['lng_degrees ' ], $ matches ['lng_minutes ' ], $ matches ['lng_seconds ' ], $ matches ['lng_fraction ' ]);
377
+ if (isset ($ matches ['alt ' ])) {
378
+ $ return ['altitude ' ] = doubleval ($ matches ['alt ' ]);
379
+ }
380
+ return $ return ;
381
+ }
382
+
207
383
/**
208
384
* Returns an array of IPTC data
209
385
*
0 commit comments