2022-08-20 02:53:21 +00:00
|
|
|
<?php
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2022-08-20 02:53:21 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace OCA\Memories;
|
|
|
|
|
|
|
|
use OCA\Memories\AppInfo\Application;
|
|
|
|
use OCP\Files\File;
|
|
|
|
use OCP\IConfig;
|
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
class Exif
|
|
|
|
{
|
2023-03-17 07:12:06 +00:00
|
|
|
private const FORBIDDEN_EDIT_MIMES = ['image/bmp', 'image/x-dcraw', 'video/MP2T'];
|
2023-03-21 18:05:10 +00:00
|
|
|
private const EXIFTOOL_VER = '12.58';
|
2022-11-24 11:18:35 +00:00
|
|
|
private const EXIFTOOL_TIMEOUT = 30000;
|
2023-02-24 17:28:57 +00:00
|
|
|
private const EXIFTOOL_ARGS = ['-api', 'QuickTimeUTC=1', '-n', '-U', '-json', '--b'];
|
2022-10-20 21:05:01 +00:00
|
|
|
|
2022-08-23 09:19:19 +00:00
|
|
|
/** Opened instance of exiftool when running in command mode */
|
2022-10-19 17:10:36 +00:00
|
|
|
private static $staticProc;
|
|
|
|
private static $staticPipes;
|
2022-08-23 09:19:19 +00:00
|
|
|
private static $noStaticProc = false;
|
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function closeStaticExiftoolProc()
|
|
|
|
{
|
2022-08-23 09:19:19 +00:00
|
|
|
try {
|
|
|
|
if (self::$staticProc) {
|
|
|
|
fclose(self::$staticPipes[0]);
|
|
|
|
fclose(self::$staticPipes[1]);
|
|
|
|
fclose(self::$staticPipes[2]);
|
|
|
|
proc_terminate(self::$staticProc);
|
2022-09-15 01:14:06 +00:00
|
|
|
self::$staticProc = null;
|
|
|
|
self::$staticPipes = null;
|
2022-08-23 09:19:19 +00:00
|
|
|
}
|
2022-10-19 17:10:36 +00:00
|
|
|
} catch (\Exception $ex) {
|
|
|
|
}
|
2022-08-23 09:19:19 +00:00
|
|
|
}
|
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function restartStaticExiftoolProc()
|
|
|
|
{
|
2022-09-15 01:14:06 +00:00
|
|
|
self::closeStaticExiftoolProc();
|
|
|
|
self::ensureStaticExiftoolProc();
|
|
|
|
}
|
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function ensureStaticExiftoolProc()
|
|
|
|
{
|
2022-08-23 09:19:19 +00:00
|
|
|
if (self::$noStaticProc) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!self::$staticProc) {
|
|
|
|
self::initializeStaticExiftoolProc();
|
2022-09-09 15:18:55 +00:00
|
|
|
usleep(500000); // wait if error
|
2022-10-19 17:10:36 +00:00
|
|
|
if (!proc_get_status(self::$staticProc)['running']) {
|
|
|
|
error_log('WARN: Failed to create stay_open exiftool process');
|
2022-08-23 09:19:19 +00:00
|
|
|
self::$noStaticProc = true;
|
|
|
|
self::$staticProc = null;
|
|
|
|
}
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2022-08-23 09:19:19 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
if (!proc_get_status(self::$staticProc)['running']) {
|
2022-08-23 09:19:19 +00:00
|
|
|
self::$staticProc = null;
|
|
|
|
self::ensureStaticExiftoolProc();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-20 02:53:21 +00:00
|
|
|
/**
|
|
|
|
* Get the path to the user's configured photos directory.
|
|
|
|
*/
|
2023-03-23 21:45:56 +00:00
|
|
|
public static function getPhotosPath(IConfig $config, string &$userId)
|
2022-10-19 17:10:36 +00:00
|
|
|
{
|
2022-08-20 02:53:21 +00:00
|
|
|
$p = $config->getUserValue($userId, Application::APPNAME, 'timelinePath', '');
|
|
|
|
if (empty($p)) {
|
2022-09-13 17:39:38 +00:00
|
|
|
return 'Photos/';
|
2022-08-20 02:53:21 +00:00
|
|
|
}
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2022-09-13 17:39:38 +00:00
|
|
|
return self::sanitizePath($p);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sanitize a path to keep only ASCII characters and special characters.
|
|
|
|
*/
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function sanitizePath(string $path)
|
|
|
|
{
|
2022-10-24 22:50:22 +00:00
|
|
|
return mb_ereg_replace('([^\\w\\s\\d\\-_~,;:!@#$&*{}\[\]\'\\[\\]\\(\\).\\\/])', '', $path);
|
2022-09-13 17:39:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-10-19 17:10:36 +00:00
|
|
|
* Keep only one slash if multiple repeating.
|
2022-09-13 17:39:38 +00:00
|
|
|
*/
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function removeExtraSlash(string $path)
|
|
|
|
{
|
2022-09-15 18:06:19 +00:00
|
|
|
return mb_ereg_replace('\/\/+', '/', $path);
|
2022-09-13 17:39:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-10-19 17:10:36 +00:00
|
|
|
* Remove any leading slash present on the path.
|
2022-09-13 17:39:38 +00:00
|
|
|
*/
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function removeLeadingSlash(string $path)
|
|
|
|
{
|
2022-09-13 23:30:01 +00:00
|
|
|
return mb_ereg_replace('~^/+~', '', $path);
|
2022-08-20 02:53:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get exif data as a JSON object from a Nextcloud file.
|
|
|
|
*/
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function getExifFromFile(File &$file)
|
|
|
|
{
|
2022-10-31 04:18:39 +00:00
|
|
|
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
|
|
|
|
if (!\is_string($path)) {
|
|
|
|
throw new \Exception('Failed to get local file path');
|
2022-08-20 02:53:21 +00:00
|
|
|
}
|
|
|
|
|
2022-11-10 05:39:13 +00:00
|
|
|
$exif = self::getExifFromLocalPath($path);
|
|
|
|
|
|
|
|
// We need to remove blacklisted fields to prevent leaking info
|
|
|
|
unset($exif['SourceFile'], $exif['FileName'], $exif['ExifToolVersion'], $exif['Directory'], $exif['FileSize'], $exif['FileModifyDate'], $exif['FileAccessDate'], $exif['FileInodeChangeDate'], $exif['FilePermissions'], $exif['ThumbnailImage']);
|
|
|
|
|
2023-03-27 19:46:12 +00:00
|
|
|
// Ignore zero dates
|
|
|
|
$dateFields = [
|
|
|
|
'DateTimeOriginal',
|
2023-03-27 22:26:08 +00:00
|
|
|
'SubSecDateTimeOriginal',
|
2023-03-27 19:46:12 +00:00
|
|
|
'CreateDate',
|
|
|
|
'ModifyDate',
|
|
|
|
'TrackCreateDate',
|
|
|
|
'TrackModifyDate',
|
|
|
|
'MediaCreateDate',
|
|
|
|
'MediaModifyDate',
|
|
|
|
];
|
|
|
|
foreach ($dateFields as $field) {
|
|
|
|
if (\array_key_exists($field, $exif) && \is_string($exif[$field]) && str_starts_with($exif[$field], '0000:00:00')) {
|
|
|
|
unset($exif[$field]);
|
|
|
|
}
|
2023-02-24 05:19:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ignore zero lat lng
|
|
|
|
if (\array_key_exists('GPSLatitude', $exif) && abs((float) $exif['GPSLatitude']) < 0.0001
|
|
|
|
&& \array_key_exists('GPSLongitude', $exif) && abs((float) $exif['GPSLongitude']) < 0.0001) {
|
|
|
|
unset($exif['GPSLatitude'], $exif['GPSLongitude']);
|
|
|
|
}
|
|
|
|
|
2022-11-10 05:39:13 +00:00
|
|
|
return $exif;
|
2022-08-20 02:53:21 +00:00
|
|
|
}
|
|
|
|
|
2022-08-23 07:54:39 +00:00
|
|
|
/** Get exif data as a JSON object from a local file path */
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function getExifFromLocalPath(string &$path)
|
|
|
|
{
|
|
|
|
if (null !== self::$staticProc) {
|
2022-08-26 00:37:40 +00:00
|
|
|
self::ensureStaticExiftoolProc();
|
2022-09-15 01:14:06 +00:00
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
return self::getExifFromLocalPathWithStaticProc($path);
|
2022-09-15 01:14:06 +00:00
|
|
|
}
|
2022-08-23 09:19:19 +00:00
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
return self::getExifFromLocalPathWithSeparateProc($path);
|
2022-08-23 07:54:39 +00:00
|
|
|
}
|
|
|
|
|
2022-09-25 13:21:40 +00:00
|
|
|
/**
|
2022-10-19 17:10:36 +00:00
|
|
|
* Parse date from exif format and throw error if invalid.
|
2022-09-25 13:21:40 +00:00
|
|
|
*/
|
2023-03-28 00:50:10 +00:00
|
|
|
public static function parseExifDate(array $exif): \DateTime
|
2022-10-19 17:10:36 +00:00
|
|
|
{
|
2023-03-27 22:26:08 +00:00
|
|
|
// Get date from exif
|
|
|
|
$exifDate = $exif['SubSecDateTimeOriginal'] ?? $exif['DateTimeOriginal'] ?? $exif['CreateDate'] ?? null;
|
|
|
|
if (null === $exifDate || empty($exifDate) || !\is_string($exifDate)) {
|
|
|
|
throw new \Exception('No date found in exif');
|
|
|
|
}
|
2023-02-11 15:41:01 +00:00
|
|
|
|
2023-03-27 22:26:08 +00:00
|
|
|
// Get timezone from exif
|
|
|
|
try {
|
2023-03-28 00:50:10 +00:00
|
|
|
$exifTz = $exif['OffsetTimeOriginal'] ?? $exif['OffsetTime'] ?? $exif['LocationTZID'] ?? null;
|
|
|
|
$exifTz = new \DateTimeZone($exifTz);
|
2023-03-27 22:26:08 +00:00
|
|
|
} catch (\Error $e) {
|
2023-03-28 00:50:10 +00:00
|
|
|
$exifTz = null;
|
2023-03-27 22:26:08 +00:00
|
|
|
}
|
|
|
|
|
2023-03-28 00:50:10 +00:00
|
|
|
// Force UTC if no timezone found
|
|
|
|
$parseTz = $exifTz ?? new \DateTimeZone('UTC');
|
|
|
|
|
2023-03-27 22:26:08 +00:00
|
|
|
// https://github.com/pulsejet/memories/pull/397
|
|
|
|
// https://github.com/pulsejet/memories/issues/485
|
|
|
|
|
|
|
|
$formats = [
|
|
|
|
'Y:m:d H:i', // 2023:03:05 18:58
|
|
|
|
'Y:m:d H:iO', // 2023:03:05 18:58+05:00
|
|
|
|
'Y:m:d H:i:s', // 2023:03:05 18:58:17
|
|
|
|
'Y:m:d H:i:sO', // 2023:03:05 10:58:17+05:00
|
|
|
|
'Y:m:d H:i:s.u', // 2023:03:05 10:58:17.000
|
|
|
|
'Y:m:d H:i:s.uO', // 2023:03:05 10:58:17.000Z
|
|
|
|
];
|
|
|
|
|
|
|
|
/** @var \DateTime $dt */
|
|
|
|
$parsedDate = null;
|
|
|
|
|
|
|
|
foreach ($formats as $format) {
|
|
|
|
if ($parsedDate = \DateTime::createFromFormat($format, $exifDate, $parseTz)) {
|
|
|
|
break;
|
2022-09-25 13:21:40 +00:00
|
|
|
}
|
2023-03-27 22:26:08 +00:00
|
|
|
}
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2023-03-28 00:50:10 +00:00
|
|
|
// If we couldn't parse the date, throw an error
|
2023-03-27 22:26:08 +00:00
|
|
|
if (!$parsedDate) {
|
|
|
|
throw new \Exception("Invalid date: {$exifDate}");
|
2022-09-25 13:21:40 +00:00
|
|
|
}
|
|
|
|
|
2023-03-28 00:50:10 +00:00
|
|
|
// Filter out dates before 1800 A.D.
|
2023-03-27 22:26:08 +00:00
|
|
|
if ($parsedDate->getTimestamp() < -5364662400) { // 1800 A.D.
|
|
|
|
throw new \Exception("Date too old: {$exifDate}");
|
2022-09-27 21:05:26 +00:00
|
|
|
}
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2023-03-28 00:50:10 +00:00
|
|
|
// Force the timezone to be the same as parseTz
|
|
|
|
if ($exifTz) {
|
|
|
|
$parsedDate->setTimezone($exifTz);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $parsedDate;
|
2022-09-27 21:05:26 +00:00
|
|
|
}
|
|
|
|
|
2022-08-20 02:53:21 +00:00
|
|
|
/**
|
|
|
|
* Get the date taken from either the file or exif data if available.
|
|
|
|
*/
|
2023-03-28 00:50:10 +00:00
|
|
|
public static function getDateTaken(File $file, array $exif): \DateTime
|
2022-10-19 17:10:36 +00:00
|
|
|
{
|
2022-09-25 13:21:40 +00:00
|
|
|
try {
|
2023-03-27 22:26:08 +00:00
|
|
|
return self::parseExifDate($exif);
|
2022-10-11 19:57:55 +00:00
|
|
|
} catch (\Exception $ex) {
|
|
|
|
} catch (\ValueError $ex) {
|
|
|
|
}
|
2022-08-20 02:53:21 +00:00
|
|
|
|
|
|
|
// Fall back to modification time
|
2023-03-28 00:50:10 +00:00
|
|
|
try {
|
|
|
|
$parseTz = new \DateTimeZone(getenv('TZ')); // debian
|
|
|
|
} catch (\Error $e) {
|
|
|
|
$parseTz = new \DateTimeZone('UTC');
|
|
|
|
}
|
|
|
|
|
|
|
|
$dt = new \DateTime('@'.$file->getMtime(), $parseTz);
|
|
|
|
$dt->setTimezone($parseTz);
|
|
|
|
|
|
|
|
return self::forgetTimezone($dt);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert time to local date in UTC.
|
|
|
|
*/
|
|
|
|
public static function forgetTimezone(\DateTime $date): \DateTime
|
|
|
|
{
|
|
|
|
return new \DateTime($date->format('Y-m-d H:i:s'), new \DateTimeZone('UTC'));
|
2022-08-20 02:53:21 +00:00
|
|
|
}
|
2022-09-25 13:21:40 +00:00
|
|
|
|
2022-10-15 19:15:07 +00:00
|
|
|
/**
|
2022-10-19 17:10:36 +00:00
|
|
|
* Get image dimensions from Exif data.
|
|
|
|
*
|
2022-10-15 19:15:07 +00:00
|
|
|
* @return array [width, height]
|
|
|
|
*/
|
2022-10-19 17:10:36 +00:00
|
|
|
public static function getDimensions(array &$exif)
|
|
|
|
{
|
2022-10-15 19:15:07 +00:00
|
|
|
$width = $exif['ImageWidth'] ?? 0;
|
|
|
|
$height = $exif['ImageHeight'] ?? 0;
|
2022-10-16 05:23:07 +00:00
|
|
|
|
|
|
|
// Check if image is rotated and we need to swap width and height
|
2022-10-18 02:45:44 +00:00
|
|
|
$rotation = $exif['Rotation'] ?? 0;
|
|
|
|
$orientation = $exif['Orientation'] ?? 0;
|
2022-10-19 17:10:36 +00:00
|
|
|
if (\in_array($orientation, [5, 6, 7, 8], true) || \in_array($rotation, [90, 270], true)) {
|
2022-10-16 05:23:07 +00:00
|
|
|
return [$height, $width];
|
|
|
|
}
|
|
|
|
|
2022-12-03 07:50:33 +00:00
|
|
|
if ($width <= 0 || $height <= 0 || $width > 100000 || $height > 100000) {
|
2022-10-28 16:48:08 +00:00
|
|
|
return [0, 0];
|
|
|
|
}
|
|
|
|
|
2022-10-15 19:15:07 +00:00
|
|
|
return [$width, $height];
|
|
|
|
}
|
|
|
|
|
2023-03-17 07:12:06 +00:00
|
|
|
/**
|
|
|
|
* Get the list of MIME Types that are allowed to be edited.
|
|
|
|
*/
|
|
|
|
public static function allowedEditMimetypes(): array
|
|
|
|
{
|
|
|
|
return array_diff(array_merge(Application::IMAGE_MIMES, Application::VIDEO_MIMES), self::FORBIDDEN_EDIT_MIMES);
|
|
|
|
}
|
|
|
|
|
2022-09-25 13:21:40 +00:00
|
|
|
/**
|
2022-11-10 06:19:44 +00:00
|
|
|
* Set exif data using raw json.
|
|
|
|
*
|
|
|
|
* @param string $path to local file
|
|
|
|
* @param array $data exif data
|
2022-09-25 13:21:40 +00:00
|
|
|
*
|
2022-11-10 06:19:44 +00:00
|
|
|
* @throws \Exception on failure
|
2022-09-25 13:21:40 +00:00
|
|
|
*/
|
2023-03-17 08:11:13 +00:00
|
|
|
public static function setExif(string $path, array $data)
|
2022-10-19 17:10:36 +00:00
|
|
|
{
|
2022-11-10 06:19:44 +00:00
|
|
|
$data['SourceFile'] = $path;
|
|
|
|
$raw = json_encode([$data]);
|
|
|
|
$cmd = array_merge(self::getExiftool(), ['-json=-', $path]);
|
|
|
|
$proc = proc_open($cmd, [
|
|
|
|
0 => ['pipe', 'r'],
|
|
|
|
1 => ['pipe', 'w'],
|
|
|
|
2 => ['pipe', 'w'],
|
|
|
|
], $pipes);
|
2022-09-25 13:21:40 +00:00
|
|
|
|
2022-11-10 06:19:44 +00:00
|
|
|
fwrite($pipes[0], $raw);
|
|
|
|
fclose($pipes[0]);
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2022-11-24 11:18:35 +00:00
|
|
|
$stdout = self::readOrTimeout($pipes[1], self::EXIFTOOL_TIMEOUT);
|
2022-11-10 06:19:44 +00:00
|
|
|
fclose($pipes[1]);
|
|
|
|
fclose($pipes[2]);
|
|
|
|
proc_terminate($proc);
|
|
|
|
if (false !== strpos($stdout, 'error')) {
|
|
|
|
error_log("Exiftool error: {$stdout}");
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2022-11-10 06:19:44 +00:00
|
|
|
throw new \Exception('Could not set exif data: '.$stdout);
|
2022-09-25 13:21:40 +00:00
|
|
|
}
|
|
|
|
}
|
2022-10-19 17:10:36 +00:00
|
|
|
|
2023-03-17 08:11:13 +00:00
|
|
|
public static function setFileExif(File $file, array $data)
|
|
|
|
{
|
|
|
|
// Get path to local file so we can skip reading
|
|
|
|
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
|
|
|
|
|
|
|
|
// Set exif data
|
|
|
|
self::setExif($path, $data);
|
|
|
|
|
|
|
|
// Update remote file if not local
|
|
|
|
if (!$file->getStorage()->isLocal()) {
|
|
|
|
$file->putContent(fopen($path, 'r')); // closes the handler
|
|
|
|
}
|
|
|
|
|
|
|
|
// Touch the file, triggering a reprocess through the hook
|
|
|
|
$file->touch();
|
|
|
|
}
|
|
|
|
|
2022-11-22 16:54:19 +00:00
|
|
|
public static function getBinaryExifProp(string $path, string $prop)
|
|
|
|
{
|
|
|
|
$pipes = [];
|
|
|
|
$proc = proc_open(array_merge(self::getExiftool(), [$prop, '-n', '-b', $path]), [
|
|
|
|
1 => ['pipe', 'w'],
|
|
|
|
2 => ['pipe', 'w'],
|
|
|
|
], $pipes);
|
|
|
|
stream_set_blocking($pipes[1], false);
|
|
|
|
|
|
|
|
try {
|
2022-11-24 11:18:35 +00:00
|
|
|
return self::readOrTimeout($pipes[1], self::EXIFTOOL_TIMEOUT);
|
2022-11-22 16:54:19 +00:00
|
|
|
} catch (\Exception $ex) {
|
|
|
|
error_log("Exiftool timeout: [{$path}]");
|
|
|
|
|
|
|
|
throw new \Exception('Could not read from Exiftool');
|
|
|
|
} finally {
|
|
|
|
fclose($pipes[1]);
|
|
|
|
fclose($pipes[2]);
|
|
|
|
proc_terminate($proc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-26 18:50:41 +00:00
|
|
|
public static function getExifWithDuplicates(string $path)
|
|
|
|
{
|
|
|
|
return self::getExifFromLocalPathWithSeparateProc($path, ['-G4']);
|
|
|
|
}
|
|
|
|
|
2022-10-20 19:31:12 +00:00
|
|
|
/** Get path to exiftool binary */
|
|
|
|
private static function getExiftool()
|
|
|
|
{
|
2022-10-22 17:45:20 +00:00
|
|
|
$configKey = 'memories.exiftool';
|
2022-12-04 17:57:31 +00:00
|
|
|
$config = \OC::$server->get(IConfig::class);
|
2022-10-20 19:31:12 +00:00
|
|
|
$configPath = $config->getSystemValue($configKey);
|
|
|
|
$noLocal = $config->getSystemValue($configKey.'_no_local', false);
|
|
|
|
|
|
|
|
// We know already where it is
|
2022-10-20 21:05:01 +00:00
|
|
|
if (!empty($configPath) && file_exists($configPath)) {
|
2022-10-30 04:23:20 +00:00
|
|
|
if (!is_executable($configPath)) {
|
|
|
|
chmod($configPath, 0755);
|
|
|
|
}
|
2022-10-22 22:07:08 +00:00
|
|
|
|
2022-10-30 04:23:20 +00:00
|
|
|
return explode(' ', $configPath);
|
2022-10-20 20:41:34 +00:00
|
|
|
}
|
2022-10-20 19:31:12 +00:00
|
|
|
|
|
|
|
// Detect architecture
|
2022-11-09 09:23:12 +00:00
|
|
|
$arch = $noLocal ? null : \OCA\Memories\Util::getArch();
|
|
|
|
$libc = $noLocal ? null : \OCA\Memories\Util::getLibc();
|
2022-10-20 19:31:12 +00:00
|
|
|
|
|
|
|
// Get static binary if available
|
|
|
|
if ($arch && $libc && !$noLocal) {
|
|
|
|
// get target file path
|
2022-11-09 09:23:12 +00:00
|
|
|
$path = realpath(__DIR__."/../exiftool-bin/exiftool-{$arch}-{$libc}");
|
2022-10-20 19:31:12 +00:00
|
|
|
|
|
|
|
// check if file exists
|
|
|
|
if (file_exists($path)) {
|
2022-10-22 21:47:49 +00:00
|
|
|
// make executable before version check
|
2022-10-30 04:23:20 +00:00
|
|
|
if (!is_executable($path)) {
|
|
|
|
chmod($path, 0755);
|
|
|
|
}
|
2022-10-22 21:47:49 +00:00
|
|
|
|
2022-10-20 19:31:12 +00:00
|
|
|
// check if the version prints correctly
|
|
|
|
$ver = self::EXIFTOOL_VER;
|
2022-10-20 21:05:01 +00:00
|
|
|
$vero = shell_exec("{$path} -ver");
|
2022-10-20 19:31:12 +00:00
|
|
|
if ($vero && false !== stripos(trim($vero), $ver)) {
|
|
|
|
$out = trim($vero);
|
2022-10-20 21:05:01 +00:00
|
|
|
echo "Exiftool binary version check passed {$out} <==> {$ver}\n";
|
2022-10-20 19:31:12 +00:00
|
|
|
$config->setSystemValue($configKey, $path);
|
2022-10-20 21:05:01 +00:00
|
|
|
|
2022-10-30 04:23:20 +00:00
|
|
|
return [$path];
|
2022-10-20 19:31:12 +00:00
|
|
|
}
|
2022-10-20 21:05:01 +00:00
|
|
|
error_log("Exiftool version check failed {$vero} <==> {$ver}");
|
|
|
|
$config->setSystemValue($configKey.'_no_local', true);
|
2022-10-20 19:31:12 +00:00
|
|
|
} else {
|
2022-10-20 21:05:01 +00:00
|
|
|
error_log("Exiftool not found: {$path}");
|
2022-10-20 19:31:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-20 20:41:34 +00:00
|
|
|
// Fallback to perl script
|
2022-10-20 21:05:01 +00:00
|
|
|
$path = __DIR__.'/../exiftool-bin/exiftool/exiftool';
|
2022-10-20 20:41:34 +00:00
|
|
|
if (file_exists($path)) {
|
2022-10-30 04:23:20 +00:00
|
|
|
return ['perl', $path];
|
2022-10-20 20:41:34 +00:00
|
|
|
}
|
|
|
|
|
2022-10-20 21:05:01 +00:00
|
|
|
error_log("Exiftool not found: {$path}");
|
2022-10-20 20:41:34 +00:00
|
|
|
|
2022-10-20 19:31:12 +00:00
|
|
|
// Fallback to system binary
|
2022-10-30 04:23:20 +00:00
|
|
|
return ['exiftool'];
|
2022-10-20 19:31:12 +00:00
|
|
|
}
|
|
|
|
|
2022-10-19 17:10:36 +00:00
|
|
|
/** Initialize static exiftool process for local reads */
|
|
|
|
private static function initializeStaticExiftoolProc()
|
|
|
|
{
|
|
|
|
self::closeStaticExiftoolProc();
|
2022-10-30 04:23:20 +00:00
|
|
|
self::$staticProc = proc_open(array_merge(self::getExiftool(), ['-stay_open', 'true', '-@', '-']), [
|
2022-10-19 17:10:36 +00:00
|
|
|
0 => ['pipe', 'r'],
|
|
|
|
1 => ['pipe', 'w'],
|
|
|
|
2 => ['pipe', 'w'],
|
|
|
|
], self::$staticPipes);
|
|
|
|
stream_set_blocking(self::$staticPipes[1], false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read from non blocking handle or throw timeout.
|
|
|
|
*
|
|
|
|
* @param resource $handle
|
|
|
|
* @param int $timeout milliseconds
|
|
|
|
* @param string $delimiter null for eof
|
|
|
|
*/
|
2022-11-24 02:28:34 +00:00
|
|
|
private static function readOrTimeout($handle, int $timeout, ?string $delimiter = null)
|
2022-10-19 17:10:36 +00:00
|
|
|
{
|
|
|
|
$buf = '';
|
|
|
|
$waitedMs = 0;
|
|
|
|
|
|
|
|
while ($waitedMs < $timeout && ($delimiter ? !str_ends_with($buf, $delimiter) : !feof($handle))) {
|
|
|
|
$r = stream_get_contents($handle);
|
|
|
|
if (empty($r)) {
|
|
|
|
++$waitedMs;
|
|
|
|
usleep(1000);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$buf .= $r;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($waitedMs >= $timeout) {
|
|
|
|
throw new \Exception('Timeout');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $buf;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static function getExifFromLocalPathWithStaticProc(string &$path)
|
|
|
|
{
|
2023-02-24 17:28:57 +00:00
|
|
|
$args = implode("\n", self::EXIFTOOL_ARGS);
|
|
|
|
fwrite(self::$staticPipes[0], "{$path}\n{$args}\n-execute\n");
|
2022-10-19 17:10:36 +00:00
|
|
|
fflush(self::$staticPipes[0]);
|
|
|
|
|
|
|
|
$readyToken = "\n{ready}\n";
|
|
|
|
|
|
|
|
try {
|
2022-11-24 11:18:35 +00:00
|
|
|
$buf = self::readOrTimeout(self::$staticPipes[1], self::EXIFTOOL_TIMEOUT, $readyToken);
|
2022-10-19 17:10:36 +00:00
|
|
|
$tokPos = strrpos($buf, $readyToken);
|
|
|
|
$buf = substr($buf, 0, $tokPos);
|
|
|
|
|
|
|
|
return self::processStdout($buf);
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
error_log("ERROR: Exiftool may have crashed, restarting process [{$path}]");
|
|
|
|
self::restartStaticExiftoolProc();
|
|
|
|
|
|
|
|
throw new \Exception('Nothing to read from Exiftool');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-26 18:50:41 +00:00
|
|
|
private static function getExifFromLocalPathWithSeparateProc(string &$path, array $extraArgs = [])
|
2022-10-19 17:10:36 +00:00
|
|
|
{
|
|
|
|
$pipes = [];
|
2023-02-24 17:28:57 +00:00
|
|
|
$proc = proc_open(array_merge(self::getExiftool(), self::EXIFTOOL_ARGS, $extraArgs, [$path]), [
|
2022-10-19 17:10:36 +00:00
|
|
|
1 => ['pipe', 'w'],
|
|
|
|
2 => ['pipe', 'w'],
|
|
|
|
], $pipes);
|
|
|
|
stream_set_blocking($pipes[1], false);
|
|
|
|
|
|
|
|
try {
|
2022-11-24 11:18:35 +00:00
|
|
|
$stdout = self::readOrTimeout($pipes[1], self::EXIFTOOL_TIMEOUT);
|
2022-10-19 17:10:36 +00:00
|
|
|
|
|
|
|
return self::processStdout($stdout);
|
|
|
|
} catch (\Exception $ex) {
|
|
|
|
error_log("Exiftool timeout: [{$path}]");
|
|
|
|
|
|
|
|
throw new \Exception('Could not read from Exiftool');
|
|
|
|
} finally {
|
|
|
|
fclose($pipes[1]);
|
|
|
|
fclose($pipes[2]);
|
|
|
|
proc_terminate($proc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Get json array from stdout of exiftool */
|
|
|
|
private static function processStdout(string &$stdout)
|
|
|
|
{
|
|
|
|
$json = json_decode($stdout, true);
|
|
|
|
if (!$json) {
|
|
|
|
throw new \Exception('Could not read exif data');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $json[0];
|
|
|
|
}
|
|
|
|
}
|