diff --git a/composer.json b/composer.json index 902c88c..43d599e 100644 --- a/composer.json +++ b/composer.json @@ -24,9 +24,9 @@ ], "require": { "php": "^8.1", - "bkwld/croppa": "^6.0", "filament/filament": "^3.0", "illuminate/contracts": "^10.0", + "spatie/image": "^2.2", "spatie/laravel-package-tools": "^1.15.0" }, "require-dev": { diff --git a/config/gallery-json-media.php b/config/gallery-json-media.php index 07a8ae1..710b332 100644 --- a/config/gallery-json-media.php +++ b/config/gallery-json-media.php @@ -3,9 +3,20 @@ declare(strict_types=1); // config for WebplusMultimedia\GalleryJsonMedia +use Spatie\Image\Manipulations; + return [ 'disk' => 'public', 'root_directory' => 'web_attachments', + 'images' => [ + 'path' => 'storage/(.*)$', + 'signing_key' => 'app.key', + 'driver' => 'imagick', // gd or imagick + 'quality' => 80, + 'thumbnails-crop-method' => Manipulations::CROP_CENTER, + 'thumbnails-saved-format' => null, // Manipulations::FORMAT_PNG / following formats are supported: FORMAT_JPG, FORMAT_PJPG, FORMAT_PNG, FORMAT_GIF, FORMAT_WEBP and FORMAT_TIFF + + ], 'form' => [ 'default' => [ 'image_accepted_text' => '.jpg, .svg, .png, .webp, .avif', diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..6b1fff1 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,10 @@ +where('path', UrlParser::make()->routePattern()); diff --git a/src/Form/JsonMediaGallery.php b/src/Form/JsonMediaGallery.php index 922c244..6fd2f41 100644 --- a/src/Form/JsonMediaGallery.php +++ b/src/Form/JsonMediaGallery.php @@ -4,11 +4,11 @@ namespace GalleryJsonMedia\Form; -use Bkwld\Croppa\Facades\Croppa; use Closure; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\BaseFileUpload; use GalleryJsonMedia\Form\Concerns\HasCustomProperties; +use GalleryJsonMedia\JsonMedia\ImageManipulation\Croppa; use GalleryJsonMedia\Support\Concerns\HasThumbProperties; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; @@ -56,6 +56,8 @@ protected function setUp(): void $this->multiple(); + $this->diskName = config('gallery-json-media.disk'); + $this->registerActions( actions: [ static fn (JsonMediaGallery $component): ?Action => $component->getCustomPropertiesAction(), @@ -141,13 +143,15 @@ public function getUploadedFiles(): array $fileName = data_get($file, 'file'); $mimeType = data_get($file, 'mime_type'); + $url[$fileKey] = [ 'name' => $fileName, 'size' => data_get($file, 'size'), 'mime_type' => $mimeType, 'url' => ($this->isImageFile($mimeType) and ! $this->isSvgFile($mimeType)) - ? url(Croppa::url($storage->url($fileName), $this->getThumbWidth())) - : $storage->url($fileName), + ? (new Croppa(filesystem: $storage, filePath: $fileName, width: $this->getThumbWidth(), height: $this->getThumbHeight())) + ->url() + : $storage->url($fileName), ]; } @@ -170,7 +174,7 @@ public function saveUploadedFiles(): void $state = array_filter(array_map(function (array $file) use ($storage) { if (isset($file['deleted']) and $file['deleted']) { try { - Croppa::reset($storage->url($file['file'])); // remove all thumbs + (new Croppa($storage, $file['file']))->reset(); // remove all thumbs } catch (\Throwable) { //never mind if file doesn't exist } diff --git a/src/JsonMedia/Controllers/JsonImageController.php b/src/JsonMedia/Controllers/JsonImageController.php new file mode 100644 index 0000000..dec5dbd --- /dev/null +++ b/src/JsonMedia/Controllers/JsonImageController.php @@ -0,0 +1,61 @@ +urlParser->signingToken($requestPath); + + if ($token !== request('_token')) { + throw new NotFoundHttpException('Token mismatch'); + } + + $url = $this->urlParser->parse($requestPath); + + /** @var FilesystemAdapter $storage */ + $storage = Storage::disk(config('gallery-json-media.disk')); + + /**@todo : for non local file Soon */ + // Create the image file + $croppa = (new Croppa(filesystem: $storage, filePath: $url['path'], width: $url['width'], height: $url['height'])); + $croppa->render(); + if ($storage->getAdapter() instanceof LocalFilesystemAdapter) { + return redirect($requestPath, 301); + } + + $absolutePath = $storage->path($requestPath); + + return new BinaryFileResponse($absolutePath, 200, [ + 'Content-Type' => $storage->mimeType($requestPath), + ]); + + } +} diff --git a/src/JsonMedia/ImageManipulation/Croppa.php b/src/JsonMedia/ImageManipulation/Croppa.php new file mode 100644 index 0000000..5835603 --- /dev/null +++ b/src/JsonMedia/ImageManipulation/Croppa.php @@ -0,0 +1,107 @@ +filesystem->exists($this->getPathNameForThumbs())) { + $this->save(); + }*/ + $url = $this->filesystem->url($this->getPathNameForThumbs()); + $url .= '?_token=' . UrlParser::make()->signingToken($url); + + return $url; + } + + /** + * @throws InvalidManipulation + * @throws InvalidImageDriver + */ + public function render(): void + { + $image = Image::load($this->filesystem->path($this->filePath)) + ->useImageDriver(config('gallery-json-media.images.driver')); + + $manipulations = new Manipulations(); + $manipulations->quality(config('gallery-json-media.images.quality')); + + if ($this->width and $this->height) { + $manipulations->crop( + cropMethod: config('gallery-json-media.images.thumbnails-crop-method'), + width: $this->width, + height: $this->height + ); + } else { + if ($this->width) { + $manipulations->width($this->width); + } + if ($this->height) { + $manipulations->height($this->height); + } + } + $image->manipulate($manipulations) + ->save($this->filesystem->path($this->getPathNameForThumbs())); + } + + protected function getPathNameForThumbs(): string + { + return $this->getBaseNameForTumbs() . $this->getSuffix() . '.' . $this->getFileInfo()['extension']; + } + + protected function getBaseNameForTumbs() + { + $basePath = str($this->filePath)->beforeLast('/'); + + return $basePath . '/' . $this->getFileInfo()['filename']; + } + + protected function getSuffix(): string + { + $suffix = '-'; + if ($this->width === null && $this->height === null) { + return ''; + } + $width = $this->width ?? '_'; + $height = $this->height ?? '_'; + $suffix .= $width . 'x' . $height; + + return $suffix; + } + + /** + * @return array{dirname : string,basename:string,extension : string,filename : string} + */ + protected function getFileInfo(): array + { + return pathinfo($this->filesystem->path($this->filePath)); + } + + public function reset(): void + { + $search = $this->filesystem->path($this->getBaseNameForTumbs() . '-*.*'); + foreach (glob($search) as $file) { + unlink($file); + } + } + + public function delete(): void + { + $this->reset(); + $this->filesystem->delete($this->filePath); + } +} diff --git a/src/JsonMedia/Media.php b/src/JsonMedia/Media.php index 0afd358..f792683 100644 --- a/src/JsonMedia/Media.php +++ b/src/JsonMedia/Media.php @@ -4,10 +4,9 @@ namespace GalleryJsonMedia\JsonMedia; -use Bkwld\Croppa\Facades\Croppa; -use Exception; use GalleryJsonMedia\JsonMedia\Concerns\HasFile; use GalleryJsonMedia\JsonMedia\Contracts\CanDeleteMedia; +use GalleryJsonMedia\JsonMedia\ImageManipulation\Croppa; use Illuminate\Contracts\Support\Htmlable; use Illuminate\View\View; use Stringable; @@ -37,6 +36,18 @@ public static function isImage(string $mimeType): bool return str($mimeType)->startsWith('image'); } + protected function getPath(): ?string + { + if ($fileName = $this->getContentKeyValue('file')) { + $storage = $this->getDisk(); + if ($storage->exists($fileName)) { + return $storage->path($fileName); + } + } + + return null; + } + public function getUrl(): ?string { if ($fileName = $this->getContentKeyValue('file')) { @@ -44,7 +55,6 @@ public function getUrl(): ?string if ($storage->exists($fileName)) { return $storage->url($fileName); } - //throw new Exception("File not Found [\$imagesDirectory] {$fileName}"); } return null; @@ -55,8 +65,18 @@ public function getCropUrl(?int $width = null, ?int $height = null, ?array $opti if ($this->isSvgFile()) { return $this->getUrl(); } + if ($path = $this->getPath()) { + $croppa = new Croppa( + $this->getDisk(), + $this->getContentKeyValue('file'), + $width, + $height + ); + + return $croppa->url(); + } - return url(Croppa::url($this->getUrl(), $width, $height, $options)); + return ''; } public function isSvgFile(): bool @@ -79,8 +99,8 @@ public function withImageProperties(int $width, int $height): Media public function delete(): void { - if ($this->getUrl()) { - Croppa::delete($this->getUrl()); + if ($this->getFileName()) { + (new Croppa($this->getDisk(), $this->getFileName()))->delete(); } } diff --git a/src/JsonMedia/UrlParser.php b/src/JsonMedia/UrlParser.php new file mode 100644 index 0000000..94fd5c2 --- /dev/null +++ b/src/JsonMedia/UrlParser.php @@ -0,0 +1,95 @@ + $this->relativePath($matches[1] . '.' . $matches[5]), // Path + 'width' => $matches[2] === '_' ? null : (int) $matches[2], // Width + 'height' => $matches[3] === '_' ? null : (int) $matches[3], // Height + 'options' => $matches[4], // Options + ]; + } + + /** + * Extract the path from a URL and remove it's leading slash. + */ + public function toPath(string $url): string + { + return ltrim(parse_url($url, PHP_URL_PATH), '/'); + } + + /** + * Take a URL or path to an image and get the path relative to the src and + * crops dirs by using the `path` config regex. + */ + public function relativePath(string $url): string + { + $path = $this->toPath($url); + $configPath = config('gallery-json-media.images.path'); + if (! preg_match('#' . $configPath . '#', $path, $matches)) { + throw new Exception("{$url} doesn't match `{$configPath}`"); + } + + return $matches[1]; + } +} diff --git a/src/JsonMediaServiceProvider.php b/src/JsonMediaServiceProvider.php index e17d35d..c98d23d 100644 --- a/src/JsonMediaServiceProvider.php +++ b/src/JsonMediaServiceProvider.php @@ -12,7 +12,6 @@ use Filament\Support\Facades\FilamentIcon; use GalleryJsonMedia\Commands\FilamentJsonMediaCommand; use GalleryJsonMedia\Testing\TestsFilamentJsonMedia; -use Illuminate\Filesystem\Filesystem; use Livewire\Features\SupportTesting\Testable; use Spatie\LaravelPackageTools\Commands\InstallCommand; use Spatie\LaravelPackageTools\Package; @@ -34,6 +33,7 @@ public function configurePackage(Package $package): void $package->name(static::$name) ->hasCommands($this->getCommands()) ->hasTranslations() + ->hasRoute('web') ->hasInstallCommand(function (InstallCommand $command) { $command ->publishConfigFile()