From 56cfade23cb9620f98c454854f19ae2a5fe7a446 Mon Sep 17 00:00:00 2001 From: Andy Heathershaw Date: Thu, 8 Sep 2016 23:22:29 +0100 Subject: [PATCH] Massive refactoring of the image processing, so it's now driven by the front-end and we can completely remove the command-line tasks - which will allow the app to work completely encoded using SourceGuardian and domain-locking. --- app/Album.php | 13 +- app/AlbumSources/IAlbumSource.php | 12 +- app/AlbumSources/LocalFilesystemSource.php | 31 ++- app/Console/Commands/ProcessUploadCommand.php | 234 ------------------ .../Commands/RegenerateThumbnailsCommand.php | 120 --------- app/Console/Kernel.php | 11 - app/Facade/Image.php | 18 ++ app/Helpers/ImageHelper.php | 23 +- .../Controllers/Admin/AlbumController.php | 167 ++++++------- .../Controllers/Admin/PhotoController.php | 143 +++++++---- .../Controllers/Gallery/AlbumController.php | 5 +- .../Controllers/Gallery/PhotoController.php | 2 +- app/Photo.php | 8 +- app/Services/PhotoService.php | 206 +++++++++++++++ app/Upload.php | 38 --- app/UploadPhoto.php | 33 --- config/app.php | 5 +- ...016_09_08_143227_remove_photo_rotation.php | 32 +++ ...2016_09_08_215424_remove_upload_tables.php | 58 +++++ ...08_220232_add_photo_is_analysed_column.php | 32 +++ .../admin/album_analyse_progress.blade.php | 126 ++++++++++ .../admin/album_upload_progress.blade.php | 84 ------- .../themes/base/admin/show_album.blade.php | 31 +++ resources/views/themes/base/layout.blade.php | 9 + .../partials/single_photo_admin.blade.php | 17 +- routes/web.php | 5 +- 26 files changed, 759 insertions(+), 704 deletions(-) delete mode 100644 app/Console/Commands/ProcessUploadCommand.php delete mode 100644 app/Console/Commands/RegenerateThumbnailsCommand.php create mode 100644 app/Facade/Image.php create mode 100644 app/Services/PhotoService.php delete mode 100644 app/Upload.php delete mode 100644 app/UploadPhoto.php create mode 100644 database/migrations/2016_09_08_143227_remove_photo_rotation.php create mode 100644 database/migrations/2016_09_08_215424_remove_upload_tables.php create mode 100644 database/migrations/2016_09_08_220232_add_photo_is_analysed_column.php create mode 100644 resources/views/themes/base/admin/album_analyse_progress.blade.php delete mode 100644 resources/views/themes/base/admin/album_upload_progress.blade.php diff --git a/app/Album.php b/app/Album.php index de4e0e2..dd5c1e6 100644 --- a/app/Album.php +++ b/app/Album.php @@ -30,15 +30,6 @@ class Album extends Model protected $hidden = [ ]; - public function fromRequest(Request $request) - { - $this->name = $request->get('name'); - $this->description = $request->get('description'); - $this->generateAlias(); - - return $this; - } - public function generateAlias() { $this->url_alias = ucfirst(preg_replace('/[^a-z0-9\-]/', '-', strtolower($this->name))); @@ -50,7 +41,7 @@ class Album extends Model public function getAlbumSource() { // TODO allow albums to specify different storage locations - e.g. Amazon S3, SFTP/FTP, OpenStack - return new LocalFilesystemSource(dirname(__DIR__) . '/storage/app/albums'); + return new LocalFilesystemSource($this, dirname(__DIR__) . '/storage/app/albums'); } public function photos() @@ -66,7 +57,7 @@ class Album extends Model if (!is_null($photo)) { - return $this->getAlbumSource()->getUrlToPhoto($this, $photo, $thumbnailName); + return $this->getAlbumSource()->getUrlToPhoto($photo, $thumbnailName); } return ''; diff --git a/app/AlbumSources/IAlbumSource.php b/app/AlbumSources/IAlbumSource.php index b8a4e4b..e24c142 100644 --- a/app/AlbumSources/IAlbumSource.php +++ b/app/AlbumSources/IAlbumSource.php @@ -12,37 +12,33 @@ interface IAlbumSource /** * Gets the absolute path to the given photo file. - * @param Album $album Album containing the photo. * @param Photo $photo Photo to get the path to. * @param string $thumbnail Thumbnail to get the image to. * @return string */ - function getPathToPhoto(Album $album, Photo $photo, $thumbnail = null); + function getPathToPhoto(Photo $photo, $thumbnail = null); /** * Gets the absolute URL to the given photo file. - * @param Album $album Album containing the photo. * @param Photo $photo Photo to get the URL to. * @param string $thumbnail Thumbnail to get the image to. * @return string */ - function getUrlToPhoto(Album $album, Photo $photo, $thumbnail = null); + function getUrlToPhoto(Photo $photo, $thumbnail = null); /** * Saves a generated thumbnail to its permanent location. - * @param Album $album Album containing the photo. * @param Photo $photo Photo the thumbnail relates to. * @param string $thumbnailInfo Information about the thumbnail. * @param string $tempFilename Filename containing the thumbnail. * @return mixed */ - function saveThumbnail(Album $album, Photo $photo, $thumbnailInfo, $tempFilename); + function saveThumbnail(Photo $photo, $thumbnailInfo, $tempFilename); /** * Saves an uploaded file to the container and returns the filename. - * @param Album $album The album containing the photo * @param File $uploadedFile The photo uploaded * @return File */ - function saveUploadedPhoto(Album $album, File $uploadedFile); + function saveUploadedPhoto(File $uploadedFile); } \ No newline at end of file diff --git a/app/AlbumSources/LocalFilesystemSource.php b/app/AlbumSources/LocalFilesystemSource.php index 5fa266e..d9b10cb 100644 --- a/app/AlbumSources/LocalFilesystemSource.php +++ b/app/AlbumSources/LocalFilesystemSource.php @@ -13,10 +13,19 @@ use Symfony\Component\HttpFoundation\File\File; */ class LocalFilesystemSource implements IAlbumSource { + /** + * @var Album + */ + private $album; + + /** + * @var string + */ private $parentFolder; - public function __construct($parentFolder) + public function __construct(Album $album, $parentFolder) { + $this->album = $album; $this->parentFolder = $parentFolder; } @@ -25,20 +34,20 @@ class LocalFilesystemSource implements IAlbumSource return '_originals'; } - public function getPathToPhoto(Album $album, Photo $photo, $thumbnail = null) + public function getPathToPhoto(Photo $photo, $thumbnail = null) { if (is_null($thumbnail)) { $thumbnail = $this->getOriginalsFolder(); } - return sprintf('%s/%s/%s', $this->getPathToAlbum($album), $thumbnail, $photo->storage_file_name); + return sprintf('%s/%s/%s', $this->getPathToAlbum(), $thumbnail, $photo->storage_file_name); } - public function getUrlToPhoto(Album $album, Photo $photo, $thumbnail = null) + public function getUrlToPhoto(Photo $photo, $thumbnail = null) { $photoUrl = route('downloadPhoto', [ - 'albumUrlAlias' => $album->url_alias, + 'albumUrlAlias' => $this->album->url_alias, 'photoFilename' => $photo->storage_file_name ]); @@ -50,15 +59,15 @@ class LocalFilesystemSource implements IAlbumSource return $photoUrl; } - public function saveThumbnail(Album $album, Photo $photo, $thumbnailInfo, $tempFilename) + public function saveThumbnail(Photo $photo, $thumbnailInfo, $tempFilename) { $fileInfo = new File($tempFilename); - $fileInfo->move(sprintf('%s/%s', $this->getPathToAlbum($album), $thumbnailInfo['name']), $photo->storage_file_name); + $fileInfo->move(sprintf('%s/%s', $this->getPathToAlbum(), $thumbnailInfo['name']), $photo->storage_file_name); } - public function saveUploadedPhoto(Album $album, File $uploadedFile) + public function saveUploadedPhoto(File $uploadedFile) { - $tempFilename = sprintf('%s/%s/%s', $this->getPathToAlbum($album), $this->getOriginalsFolder(), MiscHelper::randomString(20)); + $tempFilename = sprintf('%s/%s/%s', $this->getPathToAlbum(), $this->getOriginalsFolder(), MiscHelper::randomString(20)); $extension = $uploadedFile->guessExtension(); if (!is_null($extension)) @@ -70,8 +79,8 @@ class LocalFilesystemSource implements IAlbumSource return new File($tempFilename); } - private function getPathToAlbum(Album $album) + private function getPathToAlbum() { - return sprintf('%s/%s', $this->parentFolder, $album->url_alias); + return sprintf('%s/%s', $this->parentFolder, $this->album->url_alias); } } \ No newline at end of file diff --git a/app/Console/Commands/ProcessUploadCommand.php b/app/Console/Commands/ProcessUploadCommand.php deleted file mode 100644 index da9eed2..0000000 --- a/app/Console/Commands/ProcessUploadCommand.php +++ /dev/null @@ -1,234 +0,0 @@ -imageHelper = $imageHelper; - $this->themeHelper = $themeHelper; - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $uploadsToProcess = Upload::where([ - ['is_completed', false], - ['is_processing', false], - ['is_ready', true] - ]) - ->orderBy('created_at') - ->get(); - - /** @var Upload $upload */ - foreach ($uploadsToProcess as $upload) - { - $upload->is_processing = 1; - $upload->save(); - - $this->output->writeln(sprintf('Processing upload #%d', $upload->id)); - $this->handleUpload($upload); - - $upload->is_completed = 1; - $upload->is_processing = 0; - $upload->save(); - } - } - - private function handleUpload(Upload $upload) - { - $photos = $upload->uploadPhotos; - - /** @var UploadPhoto $photo */ - foreach ($photos as $photo) - { - try - { - $this->handlePhoto($photo); - $upload->number_successful++; - } - catch (\Exception $ex) - { - $upload->number_failed++; - $photo->photo->delete(); - $photo->delete(); - } - - $upload->save(); - } - } - - private function handlePhoto(UploadPhoto $uploadPhoto) - { - /** @var Photo $photo */ - $photo = $uploadPhoto->photo; - - /** @var Album $album */ - $album = $photo->album; - $albumSource = $album->getAlbumSource(); - - $photoFile = $albumSource->getPathToPhoto($album, $photo); - - // Read and analyse Exif data - $this->output->writeln(sprintf('Analysing photo #%d: %s', $photo->id, $photo->name)); - - // Open the photo - $imageInfo = null; - $originalPhotoResource = $this->imageHelper->openImage($photoFile, $imageInfo); - $photo->width = $imageInfo[0]; - $photo->height = $imageInfo[1]; - $photo->mime_type = $imageInfo['mime']; - - // Read the Exif data - $exifData = @exif_read_data($photoFile); - $isExifDataFound = ($exifData !== false && is_array($exifData)); - $angleToRotate = 0; - - if ($isExifDataFound && isset($exifData['Orientation'])) - { - 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) - { - $photo->width = $imageInfo[1]; - $photo->height = $imageInfo[0]; - } - } - } - - if ($isExifDataFound) - { - $photo->metadata_version = ProcessUploadCommand::METADATA_VERSION; - $photo->taken_at = $this->metadataDateTime($exifData); - $photo->camera_make = $this->metadataCameraMake($exifData); - $photo->camera_model = $this->metadataCameraModel($exifData); - $photo->camera_software = $this->metadataCameraSoftware($exifData); - $photo->rotation = $angleToRotate; - } - - $photo->save(); - - // Generate and save thumbnails - $this->output->writeln('Generating thumbnails'); - $themeInfo = $this->themeHelper->info(); - $thumbnailsRequired = $themeInfo['thumbnails']; - - /** @var mixed $thumbnail */ - foreach ($thumbnailsRequired as $thumbnail) - { - $generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $photo, $thumbnail); - $albumSource->saveThumbnail($album, $photo, $thumbnail, $generatedThumbnailPath); - - $this->output->writeln(sprintf('Thumbnail \'%s\' (%dx%d) created', $thumbnail['name'], $thumbnail['width'], $thumbnail['height'])); - } - } - - private function metadataCameraMake(array $exifData) - { - if (isset($exifData['Make'])) - { - return $exifData['Make']; - } - - return null; - } - - private function metadataCameraModel(array $exifData) - { - if (isset($exifData['Model'])) - { - return $exifData['Model']; - } - - return null; - } - - private function metadataCameraSoftware(array $exifData) - { - if (isset($exifData['Software'])) - { - return $exifData['Software']; - } - - return null; - } - - private function metadataDateTime(array $exifData) - { - $dateTime = null; - if (isset($exifData['DateTime'])) - { - $dateTime = $exifData['DateTime']; - } - - if (!is_null($dateTime)) - { - $dateTime = preg_replace('/^([\d]{4}):([\d]{2}):([\d]{2})/', '$1-$2-$3', $dateTime); - } - - return $dateTime; - } -} diff --git a/app/Console/Commands/RegenerateThumbnailsCommand.php b/app/Console/Commands/RegenerateThumbnailsCommand.php deleted file mode 100644 index 4eec209..0000000 --- a/app/Console/Commands/RegenerateThumbnailsCommand.php +++ /dev/null @@ -1,120 +0,0 @@ -imageHelper = $imageHelper; - $this->themeHelper = $themeHelper; - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $id = intval($this->argument('id')); - if ($this->option('album')) - { - $this->handleAlbum($id); - } - else - { - $this->handlePhoto($id); - } - } - - private function handleAlbum($id) - { - $album = Album::where('id', $id)->first(); - if (is_null($album)) - { - throw new \Exception(sprintf('The album with ID %d could not be found', $id)); - } - - /** @var Photo $photo */ - foreach ($album->photos as $photo) - { - $this->regenerateThumbnailsForPhoto($album, $photo); - } - } - - private function handlePhoto($id) - { - $photo = Photo::where('id', $id)->first(); - if (is_null($photo)) - { - throw new \Exception(sprintf('The photo with ID %d could not be found', $id)); - } - - /** @var Album $album */ - $album = $photo->album; - $this->regenerateThumbnailsForPhoto($album, $photo); - } - - private function regenerateThumbnailsForPhoto(Album $album, Photo $photo) - { - $albumSource = $album->getAlbumSource(); - - $originalPhotoResource = $this->imageHelper->openImage($albumSource->getPathToPhoto($album, $photo), $imageInfo); - if (!is_resource($originalPhotoResource)) - { - throw new \Exception(sprintf('The original image for photo ID %d could not be found', $id)); - } - - $this->output->writeln(sprintf('Generating thumbnails for "%s"...', $photo->file_name)); - - $themeInfo = $this->themeHelper->info(); - $thumbnailsRequired = $themeInfo['thumbnails']; - - /** @var mixed $thumbnail */ - foreach ($thumbnailsRequired as $thumbnail) - { - $generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $photo, $thumbnail); - $albumSource->saveThumbnail($album, $photo, $thumbnail, $generatedThumbnailPath); - - $this->output->writeln(sprintf('Thumbnail \'%s\' (%dx%d) created', $thumbnail['name'], $thumbnail['width'], $thumbnail['height'])); - } - } -} \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 6ece851..c28f2af 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -16,8 +16,6 @@ class Kernel extends ConsoleKernel * @var array */ protected $commands = [ - ProcessUploadCommand::class, - RegenerateThumbnailsCommand::class ]; /** @@ -28,15 +26,6 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - $schedule->command('twilight:process-uploads') - ->everyMinute() - ->when(function () { - return (Upload::where([ - 'is_completed' => 0, - 'is_processing' => 0, - 'is_ready' => 1 - ])->count() > 0); - }); } /** diff --git a/app/Facade/Image.php b/app/Facade/Image.php new file mode 100644 index 0000000..6012826 --- /dev/null +++ b/app/Facade/Image.php @@ -0,0 +1,18 @@ +authorize('admin-access'); + + $album = $this->loadAlbum($id); + $photos = $album->photos() + ->where('is_analysed', false) + ->orderBy('created_at') + ->get(); + + return Theme::render('admin.album_analyse_progress', ['album' => $album, 'photos' => $photos]); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $this->authorize('admin-access'); + + return Theme::render('admin.create_album'); + } + + public function delete($id) + { + $this->authorize('admin-access'); + + $album = $this->loadAlbum($id); + + return Theme::render('admin.delete_album', ['album' => $album]); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function destroy($id) + { + $this->authorize('admin-access'); + + $album = $this->loadAlbum($id); + $album->delete(); + + return redirect(route('albums.index')); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function edit($id) + { + $this->authorize('admin-access'); + + $album = $this->loadAlbum($id); + + return Theme::render('admin.edit_album', ['album' => $album]); + } + /** * Display a listing of the resource. * @@ -32,27 +97,6 @@ class AlbumController extends Controller ]); } - /** - * Show the form for creating a new resource. - * - * @return \Illuminate\Http\Response - */ - public function create() - { - $this->authorize('admin-access'); - - return Theme::render('admin.create_album'); - } - - public function delete($id) - { - $this->authorize('admin-access'); - - $album = AlbumController::loadAlbum($id); - - return Theme::render('admin.delete_album', ['album' => $album]); - } - /** * Display the specified resource. * @@ -63,7 +107,7 @@ class AlbumController extends Controller { $this->authorize('admin-access'); - $album = AlbumController::loadAlbum($id); + $album = $this->loadAlbum($id); $photos = $album->photos() ->orderBy(DB::raw('COALESCE(taken_at, created_at)')) ->paginate(UserConfig::get('items_per_page_admin')); @@ -86,42 +130,13 @@ class AlbumController extends Controller $this->authorize('admin-access'); $album = new Album(); - $album->fromRequest($request)->save(); + $album->fill($request->only(['name', 'description'])); + $album->generateAlias(); + $album->save(); return redirect(route('albums.index')); } - /** - * Show the form for editing the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response - */ - public function edit($id) - { - $this->authorize('admin-access'); - - $album = AlbumController::loadAlbum($id); - - return Theme::render('admin.edit_album', ['album' => $album]); - } - - public function monitorUpload($id, $uploadId) - { - $this->authorize('admin-access'); - - $upload = AlbumController::loadUpload($uploadId, $id); - - return Theme::render('admin.album_upload_progress', ['upload' => $upload, 'album' => $upload->album]); - } - - public function monitorUploadJson($id, $uploadId) - { - $this->authorize('admin-access'); - - return response()->json(AlbumController::loadUpload($uploadId, $id)->toArray()); - } - /** * Update the specified resource in storage. * @@ -133,33 +148,18 @@ class AlbumController extends Controller { $this->authorize('admin-access'); - $album = AlbumController::loadAlbum($id); - $album->fromRequest($request)->save(); + $album = $this->loadAlbum($id); + $album->fill($request->only(['name', 'description'])); + $album->save(); return Theme::render('admin.show_album', ['album' => $album]); } - /** - * Remove the specified resource from storage. - * - * @param int $id - * @return \Illuminate\Http\Response - */ - public function destroy($id) - { - $this->authorize('admin-access'); - - $album = $this->loadAlbum($id); - $album->delete(); - - return redirect(route('albums.index')); - } - /** * @param $id * @return Album */ - public static function loadAlbum($id) + private function loadAlbum($id) { $album = Album::where('id', intval($id))->first(); if (is_null($album)) @@ -170,25 +170,4 @@ class AlbumController extends Controller return $album; } - - /** - * @param $id - * @param $albumId - * @return Upload|null - */ - private static function loadUpload($id, $albumId) - { - $upload = Upload::where([ - 'id' => intval($id), - 'album_id' => intval($albumId) - ])->first(); - - if (is_null($upload)) - { - App::abort(404); - return null; - } - - return $upload; - } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/PhotoController.php b/app/Http/Controllers/Admin/PhotoController.php index 75edd8e..951e526 100644 --- a/app/Http/Controllers/Admin/PhotoController.php +++ b/app/Http/Controllers/Admin/PhotoController.php @@ -3,8 +3,13 @@ namespace App\Http\Controllers\Admin; use App\Album; +use App\AlbumSources\IAlbumSource; +use App\Facade\Image; +use App\Facade\Theme; +use App\Helpers\ImageHelper; use App\Helpers\MiscHelper; use App\Photo; +use App\Services\PhotoService; use App\Upload; use App\UploadPhoto; use Illuminate\Http\Request; @@ -16,6 +21,38 @@ use Symfony\Component\HttpFoundation\File\File; class PhotoController extends Controller { + public function analyse($photoId) + { + $this->authorize('admin-access'); + + /** @var Photo $photo */ + $photo = Photo::where('id', intval($photoId))->first(); + if (is_null($photo)) + { + App::abort(404); + return null; + } + + $result = ['is_successful' => false, 'message' => '']; + + try + { + $photoService = new PhotoService($photo); + $photoService->analyse(); + + $result['is_successful'] = true; + } + catch (\Exception $ex) + { + $result['is_successful'] = false; + $result['message'] = $ex->getMessage(); + + $photo->delete(); + } + + return response()->json($result); + } + /** * Display a listing of the resource. * @@ -36,6 +73,27 @@ class PhotoController extends Controller // } + public function rotate($photoId, $angle) + { + $this->authorize('admin-access'); + + $photo = Photo::where('id', intval($photoId))->first(); + if (is_null($photo)) + { + App::abort(404); + return null; + } + + if ($angle != 90 && $angle != 180 && $angle != 270) + { + App::aport(400); + return null; + } + + $photoService = new PhotoService($photo); + $photoService->rotate($angle); + } + /** * Store a newly created resource in storage. * @@ -49,22 +107,14 @@ class PhotoController extends Controller $photoFiles = $request->files->get('photo'); // Load the linked album - $album = AlbumController::loadAlbum($request->get('album_id')); - - $upload = new Upload(); - $upload->album_id = $album->id; - $upload->is_completed = false; - $upload->is_processing = false; - $upload->is_ready = false; - $upload->number_photos = 0; - $upload->save(); + $album = $this->loadAlbum($request->get('album_id')); foreach ($photoFiles as $photoFile) { $photoFile = UploadedFile::createFromBase($photoFile); /** @var File $savedFile */ - $savedFile = $album->getAlbumSource()->saveUploadedPhoto($album, $photoFile); + $savedFile = $album->getAlbumSource()->saveUploadedPhoto($photoFile); $photo = new Photo(); $photo->album_id = $album->id; @@ -73,22 +123,12 @@ class PhotoController extends Controller $photo->storage_file_name = $savedFile->getFilename(); $photo->mime_type = $savedFile->getMimeType(); $photo->file_size = $savedFile->getSize(); + $photo->is_analysed = false; $photo->save(); - - $upload->number_photos++; - - $uploadPhoto = new UploadPhoto(); - $uploadPhoto->upload_id = $upload->id; - $uploadPhoto->photo_id = $photo->id; - $uploadPhoto->save(); } - $upload->is_ready = true; - $upload->save(); - - return redirect(route('albums.monitorUpload', [ - 'id' => $album->id, - 'upload_id' => $upload->id + return redirect(route('albums.analyse', [ + 'id' => $album->id ])); } @@ -99,7 +139,7 @@ class PhotoController extends Controller $archiveFile = UploadedFile::createFromBase($request->files->get('archive')); // Load the linked album - $album = AlbumController::loadAlbum($request->get('album_id')); + $album = $this->loadAlbum($request->get('album_id')); // Create a temporary folder to hold the extracted files $tempFolder = sprintf('%s/btw_upload_%s', env('TEMP_FOLDER', '/tmp'), MiscHelper::randomString()); @@ -120,14 +160,6 @@ class PhotoController extends Controller return redirect(route('albums.show', ['id' => $album->id])); } - $upload = new Upload(); - $upload->album_id = $album->id; - $upload->is_completed = false; - $upload->is_processing = false; - $upload->is_ready = false; - $upload->number_photos = 0; - $upload->save(); - $di = new \RecursiveDirectoryIterator($tempFolder, \RecursiveDirectoryIterator::SKIP_DOTS); $recursive = new \RecursiveIteratorIterator($di); @@ -136,13 +168,26 @@ class PhotoController extends Controller { if ($fileInfo->isDir()) { + if ($fileInfo->getFilename() == '__MACOSX') + { + @rmdir($fileInfo->getPathname()); + } + + continue; + } + + $result = getimagesize($fileInfo->getPathname()); + if ($result === false) + { + // Not an image file - skip + @unlink($fileInfo->getPathname()); continue; } $photoFile = new File($fileInfo->getPathname()); /** @var File $savedFile */ - $savedFile = $album->getAlbumSource()->saveUploadedPhoto($album, $photoFile); + $savedFile = $album->getAlbumSource()->saveUploadedPhoto($photoFile); $photo = new Photo(); $photo->album_id = $album->id; @@ -151,22 +196,14 @@ class PhotoController extends Controller $photo->storage_file_name = $savedFile->getFilename(); $photo->mime_type = $savedFile->getMimeType(); $photo->file_size = $savedFile->getSize(); + $photo->is_analysed = false; $photo->save(); - - $upload->number_photos++; - - $uploadPhoto = new UploadPhoto(); - $uploadPhoto->upload_id = $upload->id; - $uploadPhoto->photo_id = $photo->id; - $uploadPhoto->save(); } - $upload->is_ready = true; - $upload->save(); + @rmdir($tempFolder); - return redirect(route('albums.monitorUpload', [ - 'id' => $album->id, - 'upload_id' => $upload->id + return redirect(route('albums.analyse', [ + 'id' => $album->id ])); } @@ -243,4 +280,20 @@ class PhotoController extends Controller { // } + + /** + * @param $id + * @return Album + */ + private function loadAlbum($id) + { + $album = Album::where('id', intval($id))->first(); + if (is_null($album)) + { + App::abort(404); + return null; + } + + return $album; + } } diff --git a/app/Http/Controllers/Gallery/AlbumController.php b/app/Http/Controllers/Gallery/AlbumController.php index d7f16a2..3f3fc48 100644 --- a/app/Http/Controllers/Gallery/AlbumController.php +++ b/app/Http/Controllers/Gallery/AlbumController.php @@ -8,13 +8,16 @@ use App\Facade\UserConfig; use App\Http\Controllers\Controller; use App\Http\Requests; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; class AlbumController extends Controller { public function index($albumUrlAlias) { $album = AlbumController::loadAlbum($albumUrlAlias); - $photos = $album->photos()->paginate(UserConfig::get('items_per_page')); + $photos = $album->photos() + ->orderBy(DB::raw('COALESCE(taken_at, created_at)')) + ->paginate(UserConfig::get('items_per_page_admin')); return Theme::render('gallery.album', [ 'album' => $album, diff --git a/app/Http/Controllers/Gallery/PhotoController.php b/app/Http/Controllers/Gallery/PhotoController.php index 3498b2a..abb9593 100644 --- a/app/Http/Controllers/Gallery/PhotoController.php +++ b/app/Http/Controllers/Gallery/PhotoController.php @@ -20,7 +20,7 @@ class PhotoController extends Controller $thumbnail = $request->get('t', $albumSource->getOriginalsFolder()); $photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename); - return response()->file($albumSource->getPathToPhoto($album, $photo, $thumbnail)); + return response()->file($albumSource->getPathToPhoto($photo, $thumbnail)); } public function show($albumUrlAlias, $photoFilename) diff --git a/app/Photo.php b/app/Photo.php index b0bbfec..ef876bf 100644 --- a/app/Photo.php +++ b/app/Photo.php @@ -28,7 +28,8 @@ class Photo extends Model 'camera_model', 'camera_software', 'width', - 'height' + 'height', + 'is_analysed' ]; /** @@ -46,10 +47,7 @@ class Photo extends Model public function thumbnailUrl($thumbnailName = null) { - /** @var Album $album */ - $album = $this->album; - - return $album->getAlbumSource()->getUrlToPhoto($album, $this, $thumbnailName); + return $this->album->getAlbumSource()->getUrlToPhoto($this, $thumbnailName); } public function url() diff --git a/app/Services/PhotoService.php b/app/Services/PhotoService.php new file mode 100644 index 0000000..bcafae0 --- /dev/null +++ b/app/Services/PhotoService.php @@ -0,0 +1,206 @@ +photo = $photo; + $this->album = $photo->album; + $this->albumSource = $this->album->getAlbumSource(); + + $this->imageHelper = new ImageHelper(); + $this->themeHelper = new ThemeHelper(); + } + + public function analyse() + { + /** @var Album $album */ + $album = $this->photo->album; + $albumSource = $album->getAlbumSource(); + + $photoFile = $albumSource->getPathToPhoto($this->photo); + + $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 + $exifData = @exif_read_data($photoFile); + $isExifDataFound = ($exifData !== false && is_array($exifData)); + $angleToRotate = 0; + + // If Exif data contains an Orientation, ensure we rotate the original image as such + if ($isExifDataFound && isset($exifData['Orientation'])) + { + 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->camera_make = $this->metadataCameraMake($exifData); + $this->photo->camera_model = $this->metadataCameraModel($exifData); + $this->photo->camera_software = $this->metadataCameraSoftware($exifData); + } + + $this->photo->is_analysed = true; + $this->photo->save(); + + $this->regenerateThumbnails($originalPhotoResource); + } + + public function regenerateThumbnails($originalPhotoResource = null) + { + if (is_null($originalPhotoResource)) + { + $imageInfo = null; + $originalPhotoResource = $this->imageHelper->openImage($this->albumSource->getPathToPhoto($this->photo), $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, $thumbnail, $generatedThumbnailPath); + } + } + + public function rotate($angle) + { + $imageInfo = array(); + $photoPath = $this->albumSource->getPathToPhoto($this->photo); + $originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo); + $originalPhotoImage = $this->imageHelper->rotateImage($originalPhotoImage, intval($angle)); + $this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo); + + 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(); + } + + private function metadataCameraMake(array $exifData) + { + if (isset($exifData['Make'])) + { + return $exifData['Make']; + } + + return null; + } + + private function metadataCameraModel(array $exifData) + { + if (isset($exifData['Model'])) + { + return $exifData['Model']; + } + + return null; + } + + private function metadataCameraSoftware(array $exifData) + { + if (isset($exifData['Software'])) + { + return $exifData['Software']; + } + + return null; + } + + private function metadataDateTime(array $exifData) + { + $dateTime = null; + if (isset($exifData['DateTime'])) + { + $dateTime = $exifData['DateTime']; + } + + if (!is_null($dateTime)) + { + $dateTime = preg_replace('/^([\d]{4}):([\d]{2}):([\d]{2})/', '$1-$2-$3', $dateTime); + } + + return $dateTime; + } +} \ No newline at end of file diff --git a/app/Upload.php b/app/Upload.php deleted file mode 100644 index 2219ea9..0000000 --- a/app/Upload.php +++ /dev/null @@ -1,38 +0,0 @@ -belongsTo(Album::class); - } - - public function uploadPhotos() - { - return $this->hasMany(UploadPhoto::class); - } -} diff --git a/app/UploadPhoto.php b/app/UploadPhoto.php deleted file mode 100644 index fd56101..0000000 --- a/app/UploadPhoto.php +++ /dev/null @@ -1,33 +0,0 @@ -belongsTo(Photo::class); - } -} diff --git a/config/app.php b/config/app.php index a9707ad..bf8c89a 100644 --- a/config/app.php +++ b/config/app.php @@ -229,8 +229,9 @@ return [ // Additional aliases added by AH 'Form' => Collective\Html\FormFacade::class, 'Html' => Collective\Html\HtmlFacade::class, - 'Theme' => App\Facade\Theme::class, - 'UserConfig' => App\Facade\UserConfig::class + 'Image' => \App\Facade\Image::class, + 'Theme' => \App\Facade\Theme::class, + 'UserConfig' => \App\Facade\UserConfig::class ], ]; diff --git a/database/migrations/2016_09_08_143227_remove_photo_rotation.php b/database/migrations/2016_09_08_143227_remove_photo_rotation.php new file mode 100644 index 0000000..56685f3 --- /dev/null +++ b/database/migrations/2016_09_08_143227_remove_photo_rotation.php @@ -0,0 +1,32 @@ +dropColumn('rotation'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('photos', function (Blueprint $table) { + $table->integer('rotation')->nullable(); + }); + } +} diff --git a/database/migrations/2016_09_08_215424_remove_upload_tables.php b/database/migrations/2016_09_08_215424_remove_upload_tables.php new file mode 100644 index 0000000..1806cbc --- /dev/null +++ b/database/migrations/2016_09_08_215424_remove_upload_tables.php @@ -0,0 +1,58 @@ +increments('id'); + $table->unsignedInteger('album_id'); + $table->boolean('is_completed'); + $table->boolean('is_processing'); + $table->integer('number_photos')->default(0); + $table->integer('number_successful')->default(0); + $table->integer('number_failed')->default(0); + $table->boolean('is_ready'); + $table->timestamps(); + + $table->foreign('album_id') + ->references('id')->on('albums') + ->onDelete('cascade'); + }); + + Schema::create('upload_photos', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->unsignedInteger('upload_id'); + $table->unsignedBigInteger('photo_id'); + $table->boolean('is_ready'); + $table->timestamps(); + + $table->foreign('upload_id') + ->references('id')->on('uploads') + ->onDelete('cascade'); + $table->foreign('photo_id') + ->references('id')->on('photos') + ->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2016_09_08_220232_add_photo_is_analysed_column.php b/database/migrations/2016_09_08_220232_add_photo_is_analysed_column.php new file mode 100644 index 0000000..e40756e --- /dev/null +++ b/database/migrations/2016_09_08_220232_add_photo_is_analysed_column.php @@ -0,0 +1,32 @@ +boolean('is_analysed'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('photos', function (Blueprint $table) { + $table->dropColumn('is_analysed'); + }); + } +} diff --git a/resources/views/themes/base/admin/album_analyse_progress.blade.php b/resources/views/themes/base/admin/album_analyse_progress.blade.php new file mode 100644 index 0000000..92ebfec --- /dev/null +++ b/resources/views/themes/base/admin/album_analyse_progress.blade.php @@ -0,0 +1,126 @@ +@extends('themes.base.layout') +@section('title', 'Analysing...') + +@section('content') +
+
+
+
+
Analysing...
+
+

Your uploaded photos are now being analysed.

+
+
+
+ +
+ @foreach ($photos as $photo) +

{{ $photo->name }} ...

+ @endforeach +
+
+
+ + +
+
+
+@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/themes/base/admin/album_upload_progress.blade.php b/resources/views/themes/base/admin/album_upload_progress.blade.php deleted file mode 100644 index cfe5279..0000000 --- a/resources/views/themes/base/admin/album_upload_progress.blade.php +++ /dev/null @@ -1,84 +0,0 @@ -@extends('themes.base.layout') -@section('title', 'Processing...') - -@section('content') -
-
-
-
-
Processing...
-
-

Your upload is now being processed.

-
-
-
-
-
- - -
-
-
-@endsection - -@push('scripts') - -@endpush \ No newline at end of file diff --git a/resources/views/themes/base/admin/show_album.blade.php b/resources/views/themes/base/admin/show_album.blade.php index 468d055..e78e210 100644 --- a/resources/views/themes/base/admin/show_album.blade.php +++ b/resources/views/themes/base/admin/show_album.blade.php @@ -93,10 +93,41 @@ @push('scripts') @endpush \ No newline at end of file diff --git a/resources/views/themes/base/layout.blade.php b/resources/views/themes/base/layout.blade.php index 5614951..0c3cf1e 100644 --- a/resources/views/themes/base/layout.blade.php +++ b/resources/views/themes/base/layout.blade.php @@ -9,6 +9,8 @@ @yield('title') | {{ UserConfig::get('app_name') }} + + {{-- Cannot use $theme_url here: if a theme uses the base layout, it would also have to provide all these dependencies! --}} {{-- As these files are shipped with core (not a theme) use the main app.version instead of the current theme's version --}} @@ -59,6 +61,13 @@ + @stack('scripts') diff --git a/resources/views/themes/base/partials/single_photo_admin.blade.php b/resources/views/themes/base/partials/single_photo_admin.blade.php index ddc7121..263df8a 100644 --- a/resources/views/themes/base/partials/single_photo_admin.blade.php +++ b/resources/views/themes/base/partials/single_photo_admin.blade.php @@ -1,10 +1,21 @@ @php ($field_prefix = sprintf('photo[%d]', $photo->id))
-
+
- - + +
+
diff --git a/routes/web.php b/routes/web.php index 28ca5db..1d5cbb7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,12 +21,13 @@ Route::group(['prefix' => 'admin'], function () { Route::get('settings', 'Admin\DefaultController@settings')->name('admin.settings'); // Album management + Route::get('albums/{id}/analyse', 'Admin\AlbumController@analyse')->name('albums.analyse'); Route::get('albums/{id}/delete', 'Admin\AlbumController@delete')->name('albums.delete'); - Route::get('albums/{id}/monitor/{uploadId}.json', 'Admin\AlbumController@monitorUploadJson')->name('albums.monitorUploadJson'); - Route::get('albums/{id}/monitor/{uploadId}', 'Admin\AlbumController@monitorUpload')->name('albums.monitorUpload'); Route::resource('albums', 'Admin\AlbumController'); // Photo management + Route::post('photos/analyse/{id}', 'Admin\PhotoController@analyse')->name('photos.analyse'); + Route::post('photos/rotate/{photoId}/{angle}', 'Admin\PhotoController@rotate')->name('photos.rotate'); Route::post('photos/store-bulk', 'Admin\PhotoController@storeBulk')->name('photos.storeBulk'); Route::put('photos/update-bulk/{albumId}', 'Admin\PhotoController@updateBulk')->name('photos.updateBulk'); Route::resource('photos', 'Admin\PhotoController');