Skip to content

Commit

Permalink
Distinguish UTC vs local video takestamps (#680)
Browse files Browse the repository at this point in the history
Distinguish UTC vs local video takestamps.

Handle MPG videos correctly.

Handle WMV videos with Exiftool extractor correctly.

Fix the broken PhotoFunctions::file_type method which would not distinguish between images and videos.

Don't extract a video frame if FFmpeg is disabled in the config.
  • Loading branch information
kamil4 authored Aug 17, 2020
1 parent eb5bcd7 commit 3203891
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 41 deletions.
61 changes: 52 additions & 9 deletions app/Metadata/Extractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ public function size(array &$metadata, string $filename)
* Extracts metadata from an image file.
*
* @param string $filename
* @param string mime type
* @param string file kind
*
* @return array
*/
public function extract(string $filename, string $type): array
public function extract(string $filename, string $kind): array
{
$reader = null;

Expand All @@ -82,7 +82,7 @@ public function extract(string $filename, string $type): array
$is_raw = true;
}

if (strpos($type, 'video') !== 0) {
if ($kind !== 'video') {
// It's a photo
if (Configs::hasExiftool()) {
// reader with Exiftool adapter
Expand Down Expand Up @@ -164,7 +164,6 @@ public function extract(string $filename, string $type): array
$metadata['make'] = ($exif->getMake() !== false) ? $exif->getMake() : '';
$metadata['model'] = ($exif->getCamera() !== false) ? $exif->getCamera() : '';
$metadata['shutter'] = ($exif->getExposure() !== false) ? $exif->getExposure() : '';
$metadata['takestamp'] = ($exif->getCreationDate() !== false) ? $exif->getCreationDate()->format('Y-m-d H:i:s') : null;
$metadata['lens'] = ($exif->getLens() !== false) ? $exif->getLens() : '';
$metadata['tags'] = ($exif->getKeywords() !== false) ? (is_array($exif->getKeywords()) ? implode(',', $exif->getKeywords()) : $exif->getKeywords()) : '';
$metadata['latitude'] = ($exif->getLatitude() !== false) ? $exif->getLatitude() : null;
Expand All @@ -175,16 +174,60 @@ public function extract(string $filename, string $type): array
$metadata['livePhotoContentID'] = ($exif->getContentIdentifier() !== false) ? $exif->getContentIdentifier() : null;
$metadata['MicroVideoOffset'] = ($exif->getMicroVideoOffset() !== false) ? $exif->getMicroVideoOffset() : null;

// We need to make sure, takestamp is between '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC.
// We set value to null in case we're out of bounds
if ($metadata['takestamp'] !== null) {
$takestamp = $exif->getCreationDate();
if ($takestamp !== false) {
// Some videos store creation time in local time while others use
// UTC and it's often impossible to tell, especially since the
// metadata extractors are not consistent either. We rely here on
// a simple filetype-based heuristics and, for a timestamp we
// suspect to be in UTC, we covert it to the local time of the
// Lychee server so that it's displayed to users as local time.
//
// Other possible approaches would include deriving local time from
// the file name or from other objects in the same album, as well
// as extracting the time zone from the location data if present.
if ($kind === 'video') {
$locals = strtolower(Configs::get_value('local_takestamp_video_formats', ''));
if (!in_array(strtolower($extension), explode('|', $locals), true)) {
// This is a video format where we expect the takestamp
// to be provided in UTC.
if ($takestamp->getTimezone()->getName() === date_default_timezone_get()) {
// Most likely the time zone info was missing so the
// system default was used instead, which is wrong,
// because the takestamp is actually in UTC. This will
// trigger, e.g., for mp4 files with the Exiftool
// extractor. We recreate the takestamp as a UTC
// timestamp and _then_ change the time zone to local.
$takestamp = new \DateTime($takestamp->format('Y-m-d H:i:s'), new \DateTimeZone('UTC'));
$takestamp->setTimezone(new \DateTimeZone(date_default_timezone_get()));
} elseif ($takestamp->getTimezone()->getName() === 'Z') {
// This one is correctly in Zulu (UTC). We just need
// to change the time zone to local.
$takestamp->setTimezone(new \DateTimeZone(date_default_timezone_get()));
} else {
// In the remaining case the time zone information was
// extracted and the takestamp is assumed to be in local
// time, so we don't need to do anything.
//
// The only known example are the mov files from Apple
// devices; the time zone will be formatted as "+01:00"
// so neither of the two conditions above should trigger.
}
}
}

// We need to make sure that the takestamp is between '1970-01-01 00:00:01' UTC and '2038-01-19 03:14:07' UTC.
// We set the value to null in case we're out of bounds
$min_date = new \DateTime('1970-01-01 00:00:01', new \DateTimeZone('UTC'));
$max_date = new \DateTime('2038-01-19 03:14:07', new \DateTimeZone('UTC'));
$takestamp = new \DateTime($metadata['takestamp']);
if ($takestamp < $min_date || $takestamp > $max_date) {
$metadata['takestamp'] = null;
Logs::notice(__METHOD__, __LINE__, 'Takestamp (' . $takestamp->format('Y-m-d H:i:s') . ') out of bounds (needs to be between 1970-01-01 00:00:01 and 2038-01-19 03:14:07)');
} else {
$metadata['takestamp'] = $takestamp->format('Y-m-d H:i:s');
}
} else {
$metadata['takestamp'] = null;
}

// We need to make sure, latitude is between -90/90 and longitude is between -180/180
Expand Down Expand Up @@ -237,7 +280,7 @@ public function extract(string $filename, string $type): array
$metadata['position'] = implode(', ', $fields);
}

if (strpos($type, 'video') !== 0) {
if ($kind !== 'video') {
$metadata['aperture'] = ($exif->getAperture() !== false) ? $exif->getAperture() : '';
$metadata['focal'] = ($exif->getFocalLength() !== false) ? $exif->getFocalLength() : '';
if ($metadata['focal'] !== '') {
Expand Down
63 changes: 31 additions & 32 deletions app/ModelFunctions/PhotoFunctions.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ class PhotoFunctions
public $validVideoTypes = [
'video/mp4',
'video/mpeg',
'image/x-tga', // mpg
'video/ogg',
'video/webm',
'video/quicktime',
'video/x-ms-asf', // wmv file
'video/x-ms-wmv', // wmv file
'video/x-msvideo', // Avi
'video/x-m4v', // Avi
];
Expand Down Expand Up @@ -111,37 +113,35 @@ public function file_type($file, string $extension)
return 'raw';
}

if (!in_array(strtolower($extension), $this->validExtensions, true)) {
if (in_array(strtolower($extension), $this->validExtensions, true)) {
$mimeType = $file['type'];
if (!in_array($mimeType, $this->validVideoTypes, true)) {
// let's check for the mimetype
// maybe we don't have a photo
if (!function_exists('exif_imagetype')) {
Logs::error(
__METHOD__,
__LINE__,
'EXIF library not loaded. Make sure exif is enabled in php.ini'
);

return 'EXIF library not loaded on the server!';
}
if (in_array($mimeType, $this->validVideoTypes, true)) {
return 'video';
}

$type = @exif_imagetype($file['tmp_name']);
if (!in_array($type, $this->validTypes, true)) {
Logs::error(__METHOD__, __LINE__, 'Photo type not supported: ' . $file['name']);
return 'photo';
}

return 'Photo type not supported!';
}
// we have maybe a raw file
Logs::error(__METHOD__, __LINE__, 'Photo format not supported: ' . $file['name']);
// let's check for the mimetype
// maybe we don't have a photo
if (!function_exists('exif_imagetype')) {
Logs::error(
__METHOD__,
__LINE__,
'EXIF library not loaded. Make sure exif is enabled in php.ini'
);

return 'Photo format not supported!';
}
// we have a video
return 'video';
return 'EXIF library not loaded on the server!';
}

$type = @exif_imagetype($file['tmp_name']);
if (in_array($type, $this->validTypes, true)) {
return 'photo';
}
// we have a normal photo
return 'photo';

Logs::error(__METHOD__, __LINE__, 'Photo type not supported: ' . $file['name']);

return 'Photo type not supported!';
}

/**
Expand Down Expand Up @@ -285,7 +285,7 @@ public function add(array $file, $albumID_in = 0, bool $delete_imported = false,

// Before we skip entirely, check if there is a sidecar file and if the metadata needs to be updated (from a sidecar)
if ($resync_metadata === true) {
$info = $this->getFileMetadata($file, $path, $kind, $mimeType, $extension);
$info = $this->getFileMetadata($file, $path, $kind, $extension);
foreach ($info as $key => $value) {
if ($existing->$key !== null && $value !== $existing->$key) {
$metadataChanged = true;
Expand All @@ -307,7 +307,7 @@ public function add(array $file, $albumID_in = 0, bool $delete_imported = false,
}
}

$info = $this->getFileMetadata($file, $path, $kind, $mimeType, $extension);
$info = $this->getFileMetadata($file, $path, $kind, $extension);

// TODO: move this elsewhere
$photo->title = $info['title'];
Expand Down Expand Up @@ -540,7 +540,7 @@ public function createJpgFromRaw(Photo $photo): string
*/
public function extractVideoFrame(Photo $photo): string
{
if ($photo->aperture === '') {
if ($photo->aperture === '' || !Configs::hasFFmpeg()) {
return '';
}

Expand Down Expand Up @@ -890,14 +890,13 @@ public function getValidExtensions(): array
* @param array $file
* @param string $path
* @param string $kind
* @param string $mimeType
* @param string $extension
*
* @return array
*/
private function getFileMetadata($file, $path, $kind, $mimeType, $extension): array
private function getFileMetadata($file, $path, $kind, $extension): array
{
$info = $this->metadataExtractor->extract($path, $mimeType);
$info = $this->metadataExtractor->extract($path, $kind);
if ($kind == 'raw') {
$info['type'] = 'raw';
}
Expand Down
37 changes: 37 additions & 0 deletions database/migrations/2020_07_29_132731_config_local_takestamp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

use App\Configs;
use Illuminate\Database\Migrations\Migration;

class ConfigLocalTakestamp extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
defined('DISABLED') or define('DISABLED', '');

DB::table('configs')->insert([
[
'key' => 'local_takestamp_video_formats',
'value' => '.avi|.mov',
'confidentiality' => '2',
'cat' => 'Image Processing',
'type_range' => DISABLED,
],
]);
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Configs::where('key', '=', 'local_takestamp_video_formats')->delete();
}
}

0 comments on commit 3203891

Please sign in to comment.