<?php namespace App\Models; use Carbon\Carbon; use Illuminate\Support\Facades\Log; use Illuminate\Support\Traits\ForwardsCalls; use Imagick; use App\Casts\PostgresBytea; use App\Jobs\CatalogMove; class Photo extends Abstracted\Catalog { use ForwardsCalls; public const config = 'photo'; protected $casts = [ 'created'=>'datetime:Y-m-d H:i:s', 'thumbnail'=>PostgresBytea::class, ]; protected static $includeSubSecTime = TRUE; // Imagick Object private ?Imagick $_o; protected array $init = [ 'creation_date', 'gps', 'heightwidth', 'signature', 'software', 'subsectime', ]; // How should the image be rotated, based on the value of orientation private array $_rotate = [ 3 => 180, 6 => 90, 8 => -90, ]; /** * Calculate the GPS coordinates */ public static function latlon(array $coordinate,string $hemisphere): ?float { if ((! $coordinate) || (! $hemisphere)) return NULL; for ($i=0; $i<3; $i++) { $part = explode('/', $coordinate[$i]); if (count($part) == 1) $coordinate[$i] = $part[0]; elseif (count($part) == 2) $coordinate[$i] = floatval($part[0])/floatval($part[1]); else $coordinate[$i] = 0; } list($degrees,$minutes,$seconds) = $coordinate; $sign = ($hemisphere === 'W' || $hemisphere === 'S') ? -1 : 1; return round($sign*($degrees+$minutes/60+$seconds/3600),($degrees>100 ? 3 : 4)); } /** * Forward calls to Imagick * * @param $method * @param $parameters * @return mixed|null */ public function __call($method,$parameters) { if (str_starts_with($method,'Imagick_')) { $method = preg_replace('/^Imagick_/','',$method); return $this->o ? $this->forwardCallTo($this->_o,$method,$parameters) : NULL; } else return parent::__call($method,$parameters); } public function __get($key): mixed { if ($key === 'o') { if (isset($this->_o)) return $this->_o; if ((! file_exists($this->file_name(FALSE))) || (! is_readable($this->file_name(FALSE)))) return $this->_o = NULL; if (! isset($this->_o)) return $this->_o = new Imagick($this->file_name(FALSE)); } return parent::__get($key); } /* ATTRIBUTES */ public function getFileSignatureAttribute(string $val=NULL): string { return $val ?: $this->getObjectOriginal('file_signature'); } public function getHeightAttribute(string $val=NULL): ?int { return $val ?: $this->getObjectOriginal('height'); } public function getOrientationAttribute(int $val=NULL): ?int { return $val ?: $this->getObjectOriginal('orientation'); } public function getSignatureAttribute(string $val=NULL): ?string { return $val ?: $this->getObjectOriginal('signature'); } public function getWidthAttribute(string $val=NULL): ?int { return $val ?: $this->getObjectOriginal('width'); } /* METHODS */ public function custom_init(): void { $this->orientation = $this->getObjectOriginal('orientation'); try { if ($this->isReadable() && $this->o->thumbnailimage(150,150,true)) { $this->o->setImageFormat('jpg'); $this->thumbnail = $this->o->getImageBlob(); } } catch (\Exception $e) { Log::info(sprintf('Unable to create thumbnail for %s (%s)',$this->id,$e->getMessage())); } if ($this->thumbnail === FALSE) $this->thumbnail = NULL; } public function getObjectOriginal(string $property): mixed { switch ($property) { case 'creation_date': if ($this->Imagick_getImageProperty('exif:DateTimeOriginal') === '0000:00:00 00:00:00' && $this->Imagick_getImageProperty('exif:DateTime') === '0000:00:00 00:00:00') return NULL; $result = Carbon::create($x= ($this->Imagick_getImageProperty('exif:DateTimeOriginal') && ($this->Imagick_getImageProperty('exif:DateTimeOriginal') !== '0000:00:00 00:00:00')) ? $this->Imagick_getImageProperty('exif:DateTimeOriginal').$this->Imagick_getImageProperty('exif:OffsetTimeOriginal') : $this->Imagick_getImageProperty('exif:DateTime').$this->Imagick_getImageProperty('exif:OffsetTime')); return $result ?: NULL; case 'file_signature': return md5_file($this->file_name(FALSE)); case 'gps_lat': return self::latlon(preg_split('/,\s?/',$this->Imagick_getImageProperty('exif:GPSLatitude')),$this->Imagick_getImageProperty('exif:GPSLatitudeRef')); case 'gps_lon': return self::latlon(preg_split('/,\s?/',$this->Imagick_getImageProperty('exif:GPSLongitude')),$this->Imagick_getImageProperty('exif:GPSLongitudeRef')); case 'height': return $this->Imagick_getImageHeight(); case 'identifier': return NULL; case 'make': return $this->Imagick_getImageProperty('exif:Make'); case 'model': return $this->Imagick_getImageProperty('exif:Model'); case 'orientation': return $this->Imagick_getImageOrientation(); case 'signature': return $this->Imagick_getImageSignature(); case 'software': return $this->Imagick_getImageProperty('exif:Software'); case 'subsectime': $this->subsectime = (int)$this->Imagick_getImageProperty('exif:SubSecTimeOriginal'); // In case of an error. if ($this->subsectime > 32767) $this->subsectime = 32767; if ($this->subsectime === FALSE) $this->subsectime = 0; return $this->subsectime; case 'width': return $this->Imagick_getImageWidth(); default: throw new \Exception('To implement: '.$property); } } /** * Return the image, rotated */ public function image(): ?string { if (! $this->o) return NULL; $imo = clone($this->o); return $imo ? $this->rotate($imo) : NULL; } /** * Display the orientation of a photo */ public function orientation(): string { switch ($this->orientation) { case 1: return 'None!'; case 3: return 'Upside Down'; case 6: return 'Rotate Right'; case 8: return 'Rotate Left'; default: return 'unknown?'; } } /** * Rotate the image */ private function rotate(\Imagick $imo,string $format='jpg'): string { if (array_key_exists($this->orientation,$this->_rotate)) $imo->rotateImage(new \ImagickPixel('none'),$this->_rotate[$this->orientation]); $imo->setImageFormat($format); if (array_key_exists('exif',$imo->getImageProfiles())) $imo->removeImageProfile('exif'); return $imo->getImageBlob(); } /** * Return the image's thumbnail */ public function thumbnail($rotate=TRUE): ?string { if (! $this->thumbnail) { if ($this->isReadable() && $this->o->thumbnailimage(150,150,true)) { $this->o->setImageFormat('jpg'); return $this->o->getImageBlob(); } else { return NULL; } } if ((! $rotate) || (! array_key_exists($this->orientation,$this->_rotate)) || (! extension_loaded('imagick'))) return $this->thumbnail; $imo = new \Imagick(); $imo->readImageBlob($this->thumbnail); $imo->setImageFormat('jpg'); return $this->rotate($imo); } /** * Return the extension of the image */ public function type(bool $mime=FALSE): string { return strtolower($mime ? mime_content_type($this->file_name(FALSE)) : pathinfo($this->filename,PATHINFO_EXTENSION)); } }