photo = $photo; $this->album = $photo->album; $this->albumSource = $this->album->getAlbumSource(); $this->imageHelper = new ImageHelper(); $this->themeHelper = new ThemeHelper(); } public function analyse($queueToken, $isReanalyse = false) { /** @var IAnalysisQueueSource $analysisQueueStorage */ $analysisQueueStorage = AnalysisQueueHelper::getStorageQueueSource(); $photoFile = $analysisQueueStorage->fetchItemFromAnalysisQueue($queueToken, $this->photo->storage_file_name); try { $imageInfo = null; $originalPhotoResource = $this->imageHelper->openImage($photoFile, $imageInfo); if ($originalPhotoResource === false) { throw new \Exception(sprintf('The image "%s" does not appear to be a valid image, or cannot be read', pathinfo($photoFile, PATHINFO_FILENAME))); } $this->photo->width = $imageInfo[0]; $this->photo->height = $imageInfo[1]; $this->photo->mime_type = $imageInfo['mime']; // Read the Exif data if (empty($this->photo->raw_exif_data)) { $exifData = @exif_read_data($photoFile); $isExifDataFound = ($exifData !== false && is_array($exifData)); $this->photo->raw_exif_data = $isExifDataFound ? base64_encode(serialize($exifData)) : ''; } else { $exifData = unserialize(base64_decode($this->photo->raw_exif_data)); $isExifDataFound = ($exifData !== false && is_array($exifData)); } $angleToRotate = 0; // If Exif data contains an Orientation, ensure we rotate the original image as such (providing we don't // currently have a metadata version - i.e. it hasn't been read and rotated already before) if ($isExifDataFound && isset($exifData['Orientation']) && !$isReanalyse) { switch ($exifData['Orientation']) { case 3: $angleToRotate = 180; break; case 6: $angleToRotate = 270; break; case 8: $angleToRotate = 90; break; } if ($angleToRotate > 0) { $originalPhotoResource = $this->imageHelper->rotateImage($originalPhotoResource, $angleToRotate); if ($angleToRotate == 90 || $angleToRotate == 270) { $this->photo->width = $imageInfo[1]; $this->photo->height = $imageInfo[0]; } $this->imageHelper->saveImage($originalPhotoResource, $photoFile, $imageInfo); } } if ($isExifDataFound) { $this->photo->metadata_version = self::METADATA_VERSION; $this->photo->taken_at = $this->metadataDateTime($exifData, $this->photo->taken_at); $this->photo->camera_make = $this->metadataCameraMake($exifData, $this->photo->camera_make); $this->photo->camera_model = $this->metadataCameraModel($exifData, $this->photo->camera_model); $this->photo->camera_software = $this->metadataCameraSoftware($exifData, $this->photo->camera_software); $this->photo->aperture_fnumber = $this->metadataApertureFNumber($exifData, $this->photo->aperture_fnumber); $this->photo->iso_number = $this->metadataIsoNumber($exifData, $this->photo->iso_number); $this->photo->focal_length = $this->metadataFocalLength($exifData, $this->photo->focal_length); $this->photo->shutter_speed = $this->metadataExposureTime($exifData, $this->photo->shutter_speed); } $this->photo->is_analysed = true; $this->photo->save(); // Save the original $this->albumSource->saveThumbnail($this->photo, $photoFile); $this->regenerateThumbnails($originalPhotoResource); } catch (\Exception $ex) { throw $ex; } finally { // Remove the temporary file @unlink($photoFile); // Remove from the storage $analysisQueueStorage->deleteItemFromAnalysisQueue($queueToken, $this->photo->storage_file_name); } } public function changeAlbum(Album $newAlbum) { /** @var IAlbumSource $currentSource */ $currentSource = $this->photo->album->getAlbumSource(); $newSource = $newAlbum->getAlbumSource(); // First export the original photo from the storage provider $photoPath = $this->downloadToTemporaryFolder(); // Save to the new album $newSource->saveThumbnail($this->photo, $photoPath); // Delete the original $this->delete(); // Update the ID and new file name $this->photo->album_id = $newAlbum->id; $this->photo->save(); // Switch to the new album source $this->albumSource = $newSource; // Regenerate the thumbnails in the new album $this->regenerateThumbnails(); } public function delete() { // Remove all thumbnails first - so if any fail, we don't delete the original $themeInfo = $this->themeHelper->info(); $thumbnailsRequired = $themeInfo['thumbnails']; /** @var mixed $thumbnail */ foreach ($thumbnailsRequired as $thumbnail) { $this->albumSource->deleteThumbnail($this->photo, $thumbnail['name']); } // Next remove the original photo $this->albumSource->deleteThumbnail($this->photo); // Finally remove the database record $this->photo->delete(); } public function downloadOriginalToFolder($folderPath) { $photoPath = join(DIRECTORY_SEPARATOR, [$folderPath, $this->photo->storage_file_name]); $photoHandle = fopen($photoPath, 'w'); $stream = $this->albumSource->fetchPhotoContent($this->photo); $stream->rewind(); while (!$stream->eof()) { fwrite($photoHandle, $stream->read(4096)); } fflush($photoHandle); fclose($photoHandle); $stream->close(); return $photoPath; } public function flip($horizontal, $vertical) { // First export the original photo from the storage provider $photoPath = $this->downloadToTemporaryFolder(); $imageInfo = array(); $originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo); if ($this->imageHelper->flipImage($originalPhotoImage, boolval($horizontal), boolval($vertical))) { $this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo); // Update and save the original image back to the storage provider $this->albumSource->saveThumbnail($this->photo, $photoPath); // Re-create the thumbnails $this->regenerateThumbnails($originalPhotoImage); $this->photo->save(); } // Remove the temp file @unlink($photoPath); } public function regenerateThumbnails($originalPhotoResource = null) { $photoPath = null; if (is_null($originalPhotoResource)) { // First export the original photo from the storage provider $photoPath = $this->downloadToTemporaryFolder(); $imageInfo = null; $originalPhotoResource = $this->imageHelper->openImage($photoPath, $imageInfo); } // Generate and save thumbnails $themeInfo = $this->themeHelper->info(); $thumbnailsRequired = $themeInfo['thumbnails']; /** @var mixed $thumbnail */ foreach ($thumbnailsRequired as $thumbnail) { $generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $this->photo, $thumbnail); $this->albumSource->saveThumbnail($this->photo, $generatedThumbnailPath, $thumbnail['name']); @unlink($generatedThumbnailPath); } if (is_null($originalPhotoResource) && !is_null($photoPath)) { // Remove the temp file @unlink($photoPath); } } public function rotate($angle) { $imageInfo = array(); // First export the photo from the storage provider $photoPath = $this->downloadToTemporaryFolder(); $originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo); $originalPhotoImage = $this->imageHelper->rotateImage($originalPhotoImage, intval($angle)); $this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo); // Update and save the original image back to the storage provider $this->albumSource->saveThumbnail($this->photo, $photoPath); if ($angle == 90 || $angle == 270) { $width = $this->photo->width; $this->photo->width = $this->photo->height; $this->photo->height = $width; } $this->regenerateThumbnails($originalPhotoImage); $this->photo->save(); // Remove the temp file @unlink($photoPath); } private function calculateValueFromFraction($input) { $split = explode('/', $input); if (count($split) != 2) { return $split; } $numerator = intval($split[0]); $denominator = intval($split[1]); return $denominator == 0 ? 0 : ($numerator / $denominator); } private function downloadToTemporaryFolder() { $photoPath = tempnam(sys_get_temp_dir(), 'BlueTwilight_'); $photoHandle = fopen($photoPath, 'w'); $stream = $this->albumSource->fetchPhotoContent($this->photo); $stream->rewind(); while (!$stream->eof()) { fwrite($photoHandle, $stream->read(4096)); } fflush($photoHandle); fclose($photoHandle); $stream->close(); return $photoPath; } private function metadataApertureFNumber(array $exifData, $originalValue = null) { if (isset($exifData['FNumber'])) { $value = $this->calculateValueFromFraction($exifData['FNumber']); if (intval($value) === $value) { return sprintf('f/%d', $value); } return sprintf('f/%0.1f', $value); } return $originalValue; } private function metadataCameraMake(array $exifData, $originalValue = null) { if (isset($exifData['Make'])) { return $exifData['Make']; } return $originalValue; } private function metadataCameraModel(array $exifData, $originalValue = null) { if (isset($exifData['Model'])) { return $exifData['Model']; } return $originalValue; } private function metadataCameraSoftware(array $exifData, $originalValue = null) { if (isset($exifData['Software'])) { return $exifData['Software']; } return $originalValue; } private function metadataDateTime(array $exifData, $originalValue = null) { $dateTime = null; if (isset($exifData['DateTimeOriginal'])) { $dateTime = $exifData['DateTimeOriginal']; } else if (isset($exifData['DateTime'])) { $dateTime = $exifData['DateTime']; } if (is_null($dateTime)) { return $originalValue; } return preg_replace('/^([\d]{4}):([\d]{2}):([\d]{2})/', '$1-$2-$3', $dateTime); } private function metadataExposureTime(array $exifData, $originalValue = null) { if (isset($exifData['ExposureTime'])) { $decimal = $this->calculateValueFromFraction($exifData['ExposureTime']); $fraction = MiscHelper::decimalToFraction($decimal); return sprintf('%d/%d', $fraction[0], $fraction[1]); } return $originalValue; } private function metadataFocalLength(array $exifData, $originalValue = null) { if (isset($exifData['FocalLength'])) { return $this->calculateValueFromFraction($exifData['FocalLength']); } return $originalValue; } private function metadataIsoNumber(array $exifData, $originalValue = null) { if (isset($exifData['ISOSpeedRatings'])) { return $exifData['ISOSpeedRatings']; } return $originalValue; } }