diff --git a/.gitignore b/.gitignore index 846b879..a2741e5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /formwidgets/responsiveimage/assets/css/responsiveimage.build.css.map /formwidgets/responsiveimage/assets/js/responsiveimage.build.js.map /formwidgets/responsiveimage/assets/js/responsiveimage.build.js.LICENSE.txt +/.phpunit.result.cache diff --git a/Plugin.php b/Plugin.php index 1f26c32..743e082 100644 --- a/Plugin.php +++ b/Plugin.php @@ -1,16 +1,21 @@ bind(ImageResizer::class, function() { + $disk = (new File())->getDisk(); + $dir = (new File(['is_public' => true]))->getStorageDirectory().'c/'; + $media = MediaLibrary::instance(); - } + return new ImageResizer($disk, $dir, $media); + }); - /** - * Registers any front-end components implemented in this plugin. - * - * @return array - */ - public function registerComponents() - { - return []; // Remove this line to activate + Event::listen('media.file.upload', function($widget, $filePath, $uploadedFile) { + if (app(FileObserver::class)->shouldProcessFile($filePath)) { + Queue::push(ResizeJob::class, [$filePath]); + } + }); + Event::listen('media.file.delete', function($widget, $filePath) { + app(FileObserver::class)->delete($filePath); + }); - return [ - 'Aweos\Resizer\Components\MyComponent' => 'myComponent', - ]; - } - - /** - * Registers any back-end permissions used by this plugin. - * - * @return array - */ - public function registerPermissions() - { - return []; // Remove this line to activate - - return [ - 'aweos.resizer.some_permission' => [ - 'tab' => 'resizer', - 'label' => 'Some permission' - ], - ]; - } - - /** - * Registers back-end navigation items for this plugin. - * - * @return array - */ - public function registerNavigation() - { - return []; // Remove this line to activate - - return [ - 'resizer' => [ - 'label' => 'resizer', - 'url' => Backend::url('aweos/resizer/mycontroller'), - 'icon' => 'icon-leaf', - 'permissions' => ['aweos.resizer.*'], - 'order' => 500, - ], - ]; + Event::listen('media.file.move', function($widget, $old, $new) { + app(FileObserver::class)->rename($old, $new); + }); + Event::listen('media.file.rename', function($widget, $old, $new) { + app(FileObserver::class)->rename($old, $new); + }); } public function registerSettings() { @@ -123,75 +99,9 @@ class Plugin extends PluginBase public function registerMarkupTags() { return [ 'filters' => [ - 'resize' => function($media, $breakpoints = [], $name = '') { - return Cache::remember('resized_image.'.$name.$media, 3600, function() use ($media, $breakpoints) { - ksort($breakpoints); - $l = MediaLibrary::instance(); - $folders = Setting::get('folders'); - $sizes = Setting::get('srcx'); - - $media = '/'.ltrim($media, '/'); - $filename = basename($media); - $dirname = dirname($media); - $extension = pathinfo($media, PATHINFO_EXTENSION); - - if (! $l->folderExists($dirname.'/c')) { - return 'src="'.$l->getPathUrl($media).'"'; - } - - $sizes = collect($l->listFolderContents($dirname.'/c')) - ->filter(function($f) use ($sizes, $filename) { - foreach ($sizes as $size) { - if (pathinfo($f->path, PATHINFO_FILENAME).'.'.pathinfo($filename, PATHINFO_EXTENSION) - == pathinfo($filename, PATHINFO_FILENAME).'-'.$size.'t.'.pathinfo($filename, PATHINFO_EXTENSION)) { - return true; - } - - if (pathinfo($f->path, PATHINFO_FILENAME).'.'.pathinfo($filename, PATHINFO_EXTENSION) - == pathinfo($filename, PATHINFO_FILENAME).'-'.$size.'.'.pathinfo($filename, PATHINFO_EXTENSION)) { - return true; - } - } - - return false; - }) - ->map(function($fileVersion) use ($l) { - $sizes = getimagesize(Storage::path('media/'.$fileVersion->path)); - - return [$l->getPathUrl($fileVersion->path), $sizes[0], $sizes[1]]; - })->sortBy(function($size) { - return $size[1]; - }) - ; - - if ($sizes->isEmpty()) { - return 'src="'.$l->getPathUrl($media).'"'; - } - - $srcset = 'srcset="'; - $s = ''; - - foreach($sizes as $size) { - $srcset .= $size[0].' '.$size[1].'w, '; - $s .= '(max-width: '.$size[1].'px) '.$size[1].'px, '; - } - - $s = 'sizes="'.substr($s, 0, -2).', 1920px"'; - - if (!empty($breakpoints)) { - $s = $this->breakpointsToSizes($breakpoints); - } - - $srcset = substr($srcset, 0, -2).'"'; - - $normalSize = 'width="'.$sizes->last()[1].'" height="'.$sizes->last()[2].'"'; - - return 'src="'.$l->getPathUrl($media).'" '.$srcset.' '.$s.' '.$normalSize; - }); + 'resize' => function($media, $size = 'original') { + return app(TagGenerator::class)->generate($media, $size); }, - 'responsiveimage' => function($id, $data = []) { - return (new TagPresenter($id, $data))->cacheOutput(); - } ] ]; } diff --git a/classes/FileObserver.php b/classes/FileObserver.php new file mode 100644 index 0000000..025623d --- /dev/null +++ b/classes/FileObserver.php @@ -0,0 +1,70 @@ +normalizePath($mediaPath)}"; + } + + public function normalizePath(string $path): string + { + return preg_replace('|^/*|', '', $path); + } + + public function fullMediaPath(string $path): string + { + return "media/{$this->normalizePath($path)}"; + } + + public function shouldProcessFile($file): bool + { + return collect(Setting::get('folders'))->pluck('folder')->first( + fn ($folder) => starts_with($file, $folder.'/') + ) !== null; + } + + public function delete($path): void + { + $dir = pathinfo($path, PATHINFO_DIRNAME); + $base = $dir.'/'.pathinfo($path, PATHINFO_FILENAME); + + Storage::delete("uploads/public/c{$path}"); + + collect(Storage::files("uploads/public/c{$dir}"))->filter( + fn ($file) => preg_match('|'.preg_quote("uploads/public/c{$base}", '|').'-[0-9]+x[0-9]+\.[a-zA-Z]+$|', $file) + )->each(function ($path) { + Storage::delete($path); + }); + + if (empty(Storage::allFiles("uploads/public/c{$dir}"))) { + Storage::deleteDirectory("uploads/public/c{$dir}"); + } + } + + public function rename(string $old, string $new): void + { + $dir = pathinfo($old, PATHINFO_DIRNAME); + $base = pathinfo($old, PATHINFO_FILENAME); + $newBase = pathinfo($new, PATHINFO_FILENAME); + $newDir = pathinfo($new, PATHINFO_DIRNAME); + + foreach (Storage::files("uploads/public/c{$dir}") as $file) { + if (!preg_match_all('|'.preg_quote("uploads/public/c{$dir}/{$base}", '|').'(-[0-9]+x[0-9]+)?(\.[a-zA-Z]+)$|', $file, $matches)) { + continue; + } + + $size = $matches[1][0]; + $ext = $matches[2][0]; + + Storage::move($file, "uploads/public/c{$newDir}/{$newBase}{$size}{$ext}"); + } + } + +} diff --git a/classes/FirstLetterStrategy.php b/classes/FirstLetterStrategy.php deleted file mode 100644 index 58911d1..0000000 --- a/classes/FirstLetterStrategy.php +++ /dev/null @@ -1,38 +0,0 @@ -disk = $disk; + $this->uploadDir = $uploadDir; + $this->media = $media; + } + + public function generate(string $file): void + { + $this->source = app(FileObserver::class)->normalizePath($file); + if ($this->media->findFiles($this->source)[0]->getFileType() !== 'image') { + return; + } + $this->copyOriginalImage(); + $this->generateVersions(); + } + + public function copyOriginalImage() + { + $this->disk->put($this->destinationFilename(), $this->media->get($this->source)); + } + + private function destinationFilename(): string + { + return $this->uploadDir.$this->source; + } + + private function dimensions(): Collection + { + [$width, $height] = getimagesize(Storage::path($this->destinationFilename())); + + return collect(compact('width', 'height')); + } + + private function sizes(): Collection + { + $sizes = Setting::get('sizes'); + + return collect(array_column($sizes, 'aspect_ratio'))->map( + fn ($ratio) => collect(array_combine(['width', 'height'], explode('x', $ratio))), + ); + } + + private function possibleSizes(): Collection + { + $return = collect([]); + $ratios = collect(); + $ratios->push($this->dimensions()); + $ratios = $ratios->merge($this->sizes()); + + foreach (collect(Setting::get('breakpoints'))->push($this->dimensions()->get('width'))->unique() as $size) { + foreach ($ratios as $ratio) { + $height = $size * $ratio->get('height') / $ratio->get('width'); + + $return->push(collect([ + 'width' => round($size), + 'height' => round($height), + ])); + } + } + + return $return; + } + + private function generateVersions(): void + { + foreach ($this->possibleSizes() as $size) { + $temp = microtime().'.jpg'; + $this->disk->copy($this->destinationFilename(), $temp); + $fullOriginalPath = $this->disk->path($this->destinationFilename()); + $pathinfo = collect(pathinfo($this->destinationFilename())); + + $r = app(ImageManager::class)->make($this->disk->path($temp)) + ->fit($size->get('width'), $size->get('height'), fn ($constraint) => $constraint->upsize()) + ->save($this->disk->path($temp)); + list($destWidth, $destHeight) = getimagesize($this->disk->path($temp)); + + $versionFilename = $pathinfo->get('dirname'). + '/'. + $pathinfo->get('filename'). + '-'. + $destWidth. + 'x'. + $destHeight. + '.'. + $pathinfo->get('extension'); + + if ($this->disk->exists($versionFilename)) { + $this->disk->delete($versionFilename); + } + + $this->disk->move($temp, $versionFilename); + } + } + +} diff --git a/classes/ResizeJob.php b/classes/ResizeJob.php new file mode 100644 index 0000000..e19dd5f --- /dev/null +++ b/classes/ResizeJob.php @@ -0,0 +1,13 @@ +generate($file); + } +} diff --git a/classes/TagGenerator.php b/classes/TagGenerator.php new file mode 100644 index 0000000..77afd96 --- /dev/null +++ b/classes/TagGenerator.php @@ -0,0 +1,106 @@ +media = MediaLibrary::instance(); + $this->breakpoints = Setting::get('breakpoints'); + ksort($this->breakpoints); + } + + public function cacheOutput() { + return Cache::rememberForever('responsive-image-'.$this->attachment->id, function() { + return $this->output(); + }).' '.$this->attrs; + } + + public function size(string $name): array + { + $size = collect(Setting::get('sizes'))->filter(fn ($size) => $size['name'] === $name) + ->first(); + + return explode('x', $size['aspect_ratio']); + } + + public function possibleFiles(string $ratio): Collection + { + if (Storage::mimeType(app(FileObserver::class)->fullMediaPath($this->path)) !== 'image/jpeg') { + return collect([]); + } + + $filename = pathinfo($this->path, PATHINFO_FILENAME); + $basePath = app(FileObserver::class)->versionsPath(pathinfo($this->path, PATHINFO_DIRNAME)); + [$originalWidth, $originalHeight] = getimagesize(Storage::path(app(FileObserver::class)->fullMediaPath($this->path))); + $aspectRatio = $ratio === 'original' + ? $originalWidth / $originalHeight + : $this->size($ratio)[0] / $this->size($ratio)[1]; + + $result = collect([]); + + foreach (Storage::files($basePath) as $file) { + if (!preg_match('|'.preg_quote($basePath.'/'.$filename, '|').'-([0-9]+x[0-9]+)\.([a-zA-Z]+)$|', $file, $matches)) { + continue; + } + + $width = explode('x', $matches[1])[0]; + $height = explode('x', $matches[1])[1]; + if ($width / ($height+1) > $aspectRatio || $width / ($height-1) < $aspectRatio) { + continue; + } + + $result->push(collect([ + 'url' => Storage::url($file), + 'width' => $width, + 'height' => $height, + ])); + } + + return $result->sortBy('width'); + } + + public function generate(string $path, ?string $ratio = 'original'): string + { + $this->path = $path; + $files = $this->possibleFiles($ratio); + + if ($files->isEmpty()) { + return 'src="/storage/app/media/'.$path.'"'; + } + + $sizes = $files->map(function($file) { + return "(max-width: {$file->get('width')}px) {$file->get('width')}px"; + }); + + $srcset = $files->map(function($file) { + return "{$file->get('url')} {$file->get('width')}w"; + }); + + return $this->htmlAttributes(collect([ + 'width' => $files->last()->get('width'), + 'height' => $files->last()->get('height'), + 'sizes' => $sizes->implode(', '), + 'srcset' => $srcset->implode(', '), + 'src' => $files->last()->get('url'), + ])); + } + + private function htmlAttributes($attr) { + return $attr->map(function($value, $key) { + return "{$key}=\"{$value}\""; + })->implode(' '); + } +} diff --git a/classes/TagPresenter.php b/classes/TagPresenter.php deleted file mode 100644 index aa78373..0000000 --- a/classes/TagPresenter.php +++ /dev/null @@ -1,72 +0,0 @@ -attachment = Attachment::find($id); - $this->attrs = data_get($data, 'attrs'); - $this->breakpoints = data_get($data, 'breakpoints', []); - $this->sizes = Setting::get('srcx'); - } - - public function cacheOutput() { - return Cache::rememberForever('responsive-image-'.$this->attachment->id, function() { - return $this->output(); - }).' '.$this->attrs; - } - - public function output() { - ksort($this->breakpoints); - - $files = collect(Storage::disk($this->disk)->files($this->attachment->path))->filter(function($file) { - return preg_match($this->attachment->versionRegex, $file); - })->map(function($file) { - $sizes = getimagesize(Storage::disk($this->disk)->path($file)); - - return collect([ - 'url' => Storage::disk($this->disk)->url($file), - 'width' => $sizes[0], - 'height' => $sizes[1] - ]); - })->sortBy('width'); - - if ($files->isEmpty()) { - return 'src="'.$this->attachment->url.'"'; - } - - $sizes = $files->map(function($file) { - return "(max-width: {$file->get('width')}px) {$file->get('width')}px"; - }); - - $srcset = $files->map(function($file) { - return "{$file->get('url')} {$file->get('width')}w"; - }); - - return $this->htmlAttributes(collect([ - 'width' => $files->last()->get('width'), - 'height' => $files->last()->get('height'), - 'sizes' => $sizes->implode(', '), - 'srcset' => $srcset->implode(', '), - 'alt' => $this->attachment->title - ])); - } - - private function htmlAttributes($attr) { - return $attr->map(function($value, $key) { - return "{$key}=\"{$value}\""; - })->implode(' '); - } -} diff --git a/composer.json b/composer.json index 7d0e834..6cc81b0 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,8 @@ "name": "aweos/oc-resizer-plugin", "type": "october-plugin", "require": { - "guzzlehttp/guzzle": "^6.3" + "guzzlehttp/guzzle": "^6.3", + "intervention/image": "^2.6" }, "authors": [ { diff --git a/composer.lock b/composer.lock index bf6fd2d..28c4e0c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "82331c61df9e7f95117f9b82b35b6ce2", + "content-hash": "d44b27fd707830b38897f254dc4f9040", "packages": [ { "name": "guzzlehttp/guzzle", @@ -193,6 +193,90 @@ ], "time": "2019-07-01T23:21:34+00:00" }, + { + "name": "intervention/image", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "0925f10b259679b5d8ca58f3a2add9255ffcda45" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/0925f10b259679b5d8ca58f3a2add9255ffcda45", + "reference": "0925f10b259679b5d8ca58f3a2add9255ffcda45", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "guzzlehttp/psr7": "~1.1 || ^2.0", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.2", + "phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15" + }, + "suggest": { + "ext-gd": "to use GD library based image processing.", + "ext-imagick": "to use Imagick based image processing.", + "intervention/imagecache": "Caching extension for the Intervention Image library" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + }, + "laravel": { + "providers": [ + "Intervention\\Image\\ImageServiceProvider" + ], + "aliases": { + "Image": "Intervention\\Image\\Facades\\Image" + } + } + }, + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src/Intervention/Image" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@olivervogel.com", + "homepage": "http://olivervogel.com/" + } + ], + "description": "Image handling and manipulation library with support for Laravel integration", + "homepage": "http://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "laravel", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/2.6.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/interventionphp", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2021-07-22T14:31:53+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -291,5 +375,6 @@ "prefer-stable": false, "prefer-lowest": false, "platform": [], - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.0.0" } diff --git a/console/ResizeMake.php b/console/ResizeMake.php index bad1b1e..7413381 100644 --- a/console/ResizeMake.php +++ b/console/ResizeMake.php @@ -1,20 +1,21 @@ start($folder['folder'], $sizes, $callback); + public function resize(string $folder): void + { + foreach ($this->media->listFolderContents($folder) as $item) { + if ($item->type === 'folder') { + $this->resize($item->path); + } else { + Queue::push(ResizeJob::class, [$item->path]); + } } } @@ -43,117 +44,12 @@ class ResizeMake extends Command */ public function handle() { - $this->http = new Client(['base_uri' => 'https://api.tinify.com']); $this->media = MediaLibrary::instance(); ProgressBar::setFormatDefinition('custom', '%current%/%max% %bar% -- %message% (%size%)'); - $tinifyKey = Setting::get('tinify') ? Setting::get('tinypngkey') : null; + Storage::deleteDirectory('uploads/public/c'); - $i = 0; - $this->forAllResizables(function($file, $w) use (&$i) { - $i++; - }); - - $bar = $this->output->createProgressBar($i); - $bar->setFormat('custom'); - - /** - * @param File $file The current filename - * @param int $w Current width - * @param string $compressed compressed file - * @param string $uncompressed uncompressed file - */ - $this->forAllResizables(function($file, $w, $compressed, $uncompressed) use ($bar, $tinifyKey) { - $bar->setMessage($file->path); - $bar->setMessage($w, 'size'); - $bar->advance(); - - if (Storage::exists($compressed)) { - return; - } - - if (!Storage::exists($uncompressed)) { - $r = Resizer::open(Storage::disk('local')->path('media'.$file->path)); - $r->resize($w, 0); - $r->save(Storage::disk('local')->path($uncompressed)); - $this->info(Storage::url($uncompressed)); - } - - if (!is_null($tinifyKey)) { - $newUrl = url($this->media->getPathUrl(preg_replace('/^media/', '', $uncompressed))); - - try { - $response = $this->http->post('/shrink', [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - 'Authorization' => 'Basic '.base64_encode('api:'.$tinifyKey) - ], - 'json' => [ - 'source' => ['url' => $newUrl] - ] - ]); - - $output = json_decode((string) $response->getBody()); - $url = $output->output->url; - - $response = $this->http->get($output->output->url, [ - 'headers' => [ - 'Authorization' => 'Basic '.base64_encode('api:'.$tinifyKey) - ] - ]); - $image = (string) $response->getBody(); - - Storage::put($compressed, $image); - Storage::delete($uncompressed); - } catch(ClientException $e) { - return; - } - } - - $bar->advance(); - }); - - $bar->finish(); - } - - public function getMediaOfType($f, $type) { - return array_filter($this->media->listFolderContents($f, 'title', 'image'), function($item) use ($f, $type) { - return $item->type == $type && $item->path != $f.'/c'; - }); - } - - public function start($f, $sizes, $callback) { - $f = '/'.ltrim(rtrim($f, '/'), '/'); - - $folders = $this->getMediaOfType($f, 'folder'); - - foreach ($folders as $folder) { - $this->start($folder->path, $sizes, $callback); - } - - $files = $this->getMediaOfType($f, 'file'); - - // Create C-Folder as current subfolder if it doesnt exist yet - if(count($files) && !$this->media->folderExists($f.'/c')) { - $this->media->makeFolder($f.'/c'); - } - - foreach ($files as $file) { - foreach ($sizes as $w) { - $imagesize = getimagesize(Storage::path('media'.$file->path)); - - $uncompressedFilename = pathinfo($file->path, PATHINFO_FILENAME).'-'.$w.'.'.pathinfo($file->path, PATHINFO_EXTENSION); - $compressedFilename = pathinfo($file->path, PATHINFO_FILENAME).'-'.$w.'t.'.pathinfo($file->path, PATHINFO_EXTENSION); - - $compressed = 'media'.dirname($file->path).'/c/'.$compressedFilename; - $uncompressed = 'media'.dirname($file->path).'/c/'.$uncompressedFilename; - - if ($imagesize[0] < $w) { - continue; - } - - call_user_func($callback, $file, $w, $compressed, $uncompressed); - } + foreach (Setting::get('folders') as $folder) { + $this->resize($folder['folder']); } } diff --git a/models/setting/fields.yaml b/models/setting/fields.yaml index 3303979..bdce25d 100644 --- a/models/setting/fields.yaml +++ b/models/setting/fields.yaml @@ -5,27 +5,29 @@ fields: folders: type: repeater + label: Überwachte Ordner form: fields: folder: type: mediafinder label: Folder - srcx: + breakpoints: type: taglist mode: array - label: Source X - srcy: - type: taglist - mode: array - label: Source Y + label: Breakpoints + comment: Von diesen Werten werden Bilder generiert mit entsprechender Breite. Bitte nur die Breite in Pixel angeben. Die Höhe bestimmt sich nach der Zielgröße + sizes: + type: repeater + label: Seitenverhältnisse + form: + fields: + name: + label: Name + span: left + comment: Bitte im Slug Format eingeben - z.B. "big-box" + aspect_ratio: + label: Seitenverhältnis + comment: Bitte im Format [x]x[y] eingeben - z.B. 1280x700 + span: right + comment: Ein Bild mit den selben Seitenverhältnissen des Originalbildes wird immer generiert. Gebe hier die Seitenverhältnisse in "x/y" an, die sonst noch generiert werden sollen, z.B. 16/9 - tinify: - type: checkbox - label: Tinyfy with tinypng - tinypngkey: - type: text - label: Tinypng API Key - trigger: - action: show - field: tinify - condition: checked diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..311802b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,29 @@ + + + + + ./tests + + + + + + + + + + + + + + + diff --git a/tests/DeleteTest.php b/tests/DeleteTest.php new file mode 100644 index 0000000..7056064 --- /dev/null +++ b/tests/DeleteTest.php @@ -0,0 +1,60 @@ +image(100, 100)->storeAs('uploads/public/c/pages', 'test.jpg', 'local'); + Event::fire('media.file.delete', [null, '/pages/test.jpg', null]); + + $this->assertFileCount(0, 'pages'); + Storage::assertMissing('uploads/public/c/pages'); + } + + public function testItPreservesDirectoryWhenThereAreOtherFiles() + { + Setting::set('folders', ['pages']); + Setting::set('sizes', []); + Setting::set('breakpoints', []); + + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'test.jpg', 'local'); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'test2.jpg', 'local'); + Event::fire('media.file.delete', [null, '/pages/test.jpg', null]); + + $this->assertHasFile('pages/test2.jpg'); + $this->assertDoesntHaveFile('pages/test.jpg'); + } + + public function testItDeletesFileVersions() + { + Setting::set('folders', ['pages']); + Setting::set('sizes', []); + Setting::set('breakpoints', []); + + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'test.jpg', 'local'); + UploadedFile::fake()->image(300, 500)->storeAs('uploads/public/c/pages', 'test-300x500.jpg', 'local'); + Event::fire('media.file.delete', [null, '/pages/test.jpg', null]); + + $this->assertDoesntHaveFile('pages/test-300x500.jpg'); + } + +} diff --git a/tests/ImageTagTest.php b/tests/ImageTagTest.php new file mode 100644 index 0000000..4d16338 --- /dev/null +++ b/tests/ImageTagTest.php @@ -0,0 +1,88 @@ +media->put('/pages/test.jpg', UploadedFile::fake()->image('test.jpg', 500, 500)->get()); + UploadedFile::fake()->image('test.jpg', 500, 500)->storeAs('uploads/public/c/pages', 'test-500x500.jpg', 'local'); + UploadedFile::fake()->image('test.jpg', 200, 200)->storeAs('uploads/public/c/pages', 'test-200x200.jpg', 'local'); + UploadedFile::fake()->image('test.jpg', 100, 100)->storeAs('uploads/public/c/pages', 'test-100x100.jpg', 'local'); + + $this->assertEquals( + 'width="500" height="500" sizes="(max-width: 100px) 100px, (max-width: 200px) 200px, (max-width: 500px) 500px" srcset="/storage/uploads/public/c/pages/test-100x100.jpg 100w, /storage/uploads/public/c/pages/test-200x200.jpg 200w, /storage/uploads/public/c/pages/test-500x500.jpg 500w" src="/storage/uploads/public/c/pages/test-500x500.jpg"', + app(TagGenerator::class)->generate('pages/test.jpg'), + ); + } + + public function testItSkipsImagesWithWrongAspectRatio() + { + Setting::set('folders', ['pages']); + Setting::set('sizes', []); + Setting::set('breakpoints', [100,200]); + $this->media->put('/pages/test.jpg', UploadedFile::fake()->image('test.jpg', 500, 500)->get()); + UploadedFile::fake()->image('test.jpg', 100, 100)->storeAs('uploads/public/c/pages', 'test-100x100.jpg', 'local'); + UploadedFile::fake()->image('test.jpg', 150, 100)->storeAs('uploads/public/c/pages', 'test-150x100.jpg', 'local'); + + $this->assertFalse(str_contains( + app(TagGenerator::class)->generate('pages/test.jpg'), + '150x100', + )); + } + + public function testItCanGenerateAAspectRatio() + { + Setting::set('folders', ['pages']); + Setting::set('sizes', [['name' => 'size', 'aspect_ratio' => '1x2']]); + Setting::set('breakpoints', [100,200]); + $this->media->put('/pages/test.jpg', UploadedFile::fake()->image('test.jpg', 500, 500)->get()); + UploadedFile::fake()->image('test.jpg', 100, 100)->storeAs('uploads/public/c/pages', 'test-100x100.jpg', 'local'); + UploadedFile::fake()->image('test.jpg', 150, 100)->storeAs('uploads/public/c/pages', 'test-150x100.jpg', 'local'); + UploadedFile::fake()->image('test.jpg', 250, 500)->storeAs('uploads/public/c/pages', 'test-250x500.jpg', 'local'); + UploadedFile::fake()->image('test.jpg', 200, 400)->storeAs('uploads/public/c/pages', 'test-200x400.jpg', 'local'); + UploadedFile::fake()->image('test.jpg', 100, 200)->storeAs('uploads/public/c/pages', 'test-100x200.jpg', 'local'); + + $this->assertFalse(str_contains( + app(TagGenerator::class)->generate('pages/test.jpg', 'size'), + '100x100', + )); + $this->assertTrue(str_contains( + app(TagGenerator::class)->generate('pages/test.jpg', 'size'), + '200x400', + )); + } + + public function testItServesAspectRatiosThatCanBeOnePixelLarger() + { + Setting::set('folders', ['pages']); + Setting::set('sizes', [['name' => 'size', 'aspect_ratio' => '1200x280']]); + Setting::set('breakpoints', [640]); + $this->media->put('/pages/test.jpg', UploadedFile::fake()->image('test.jpg', 1920, 899)->get()); + UploadedFile::fake()->image('test.jpg', 640, 149)->storeAs('uploads/public/c/pages', 'test-640x149.jpg', 'local'); + + $this->assertTrue(str_contains( + app(TagGenerator::class)->generate('pages/test.jpg', 'size'), + '640x149', + )); + } + +} diff --git a/tests/MoveTest.php b/tests/MoveTest.php new file mode 100644 index 0000000..f2b3005 --- /dev/null +++ b/tests/MoveTest.php @@ -0,0 +1,69 @@ +media->put('/pages/alt/test.jpg', UploadedFile::fake()->image(100, 100)->get()); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages/alt', 'test.jpg', 'local'); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages/alt', 'test-200x300.jpg', 'local'); + + Event::fire('media.file.move', [null, '/pages/alt/test.jpg', '/pages/neu/test.jpg']); + + $this->assertFileCount(0, 'pages/alt'); + $this->assertHasFile('pages/neu/test.jpg'); + $this->assertHasFile('pages/neu/test-200x300.jpg'); + } + + public function testItMovesFilesOnRename() + { + Setting::set('folders', ['pages']); + Setting::set('sizes', []); + Setting::set('breakpoints', []); + $this->media->put('/pages/test.jpg', UploadedFile::fake()->image(100, 100)->get()); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'test.jpg', 'local'); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'test-200x300.jpg', 'local'); + + Event::fire('media.file.rename', [null, '/pages/test.jpg', '/pages/testneu.jpg']); + + $this->assertFileCount(2, 'pages'); + $this->assertHasFile('pages/testneu.jpg'); + $this->assertHasFile('pages/testneu-200x300.jpg'); + } + + public function testItDoesntMoveOtherFilesInTheSameDirectory() + { + Setting::set('folders', ['pages']); + Setting::set('sizes', []); + Setting::set('breakpoints', []); + $this->media->put('/pages/test.jpg', UploadedFile::fake()->image(100, 100)->get()); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'test.jpg', 'local'); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'test-200x300.jpg', 'local'); + UploadedFile::fake()->image(100, 100)->storeAs('uploads/public/c/pages', 'testother.jpg', 'local'); + + Event::fire('media.file.rename', [null, '/pages/test.jpg', '/pages/testneu.jpg']); + + $this->assertFileCount(3, 'pages'); + $this->assertHasFile('pages/testneu.jpg'); + $this->assertHasFile('pages/testneu-200x300.jpg'); + } + +} diff --git a/tests/ResizerTest.php b/tests/ResizerTest.php new file mode 100644 index 0000000..11793f4 --- /dev/null +++ b/tests/ResizerTest.php @@ -0,0 +1,158 @@ +image('test.jpg', 500, 600); + $media = MediaLibrary::instance(); + $media->put('/pages/test.jpg', $file); + Event::fire('media.file.upload', [null, '/pages/test.jpg', null]); + + $this->assertFileCount(0, ''); + } + + public function testCopyOriginalFileWithoutSizesWhenNoSizesAreSet(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', []); + Setting::set('breakpoints', []); + + $file = UploadedFile::fake()->image('test.jpg', 500, 600); + $media = MediaLibrary::instance(); + $media->put('/pages/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages/test.jpg', null]); + + $this->assertFileCount(2, 'pages'); + $this->assertHasFile('pages/test.jpg'); + $this->assertHasFile('pages/test-500x600.jpg'); + } + + public function testCopyTwoDirectoriesDeepButNotAnotherDirectory(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', []); + Setting::set('breakpoints', []); + + $file = UploadedFile::fake()->image('test.jpg', 500, 600); + $media = MediaLibrary::instance(); + $media->put('/pages-neu/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages-neu/test.jpg', null]); + + $this->assertFileCount(0, 'pages'); + $this->assertFileCount(0, 'pages-neu'); + } + + public function testCopySubdirectoriesOfSelectedPath(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', []); + Setting::set('breakpoints', []); + + $file = UploadedFile::fake()->image('test.jpg', 500, 600); + $media = MediaLibrary::instance(); + $media->put('/pages/neu/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages/neu/test.jpg', null]); + + $this->assertFileCount(2, 'pages/neu'); + } + + public function testGenerateSizeIfSizeIsSmallerWithSameAspectRatio(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', []); + Setting::set('breakpoints', ['250']); + + $file = UploadedFile::fake()->image('test.jpg', 500, 600); + $media = MediaLibrary::instance(); + $media->put('/pages/neu/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages/neu/test.jpg', null]); + + $this->assertFileCount(3, 'pages/neu'); + $this->assertHasFile('pages/neu/test-250x300.jpg'); + $this->assertHasFile('pages/neu/test-500x600.jpg'); + $this->assertHasFile('pages/neu/test.jpg'); + } + + public function testGenerateSizeIfSizeIsSmallerWithDifferentAspectRatio(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', []); + Setting::set('breakpoints', ['250']); + + $file = UploadedFile::fake()->image('test.jpg', 500, 100); + $media = MediaLibrary::instance(); + $media->put('/pages/neu/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages/neu/test.jpg', null]); + + $this->assertHasFile('pages/neu/test-250x50.jpg'); + } + + public function testDontGenerateSizeIfOriginalFileIsSmaller(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', []); + Setting::set('breakpoints', ['250']); + + $file = UploadedFile::fake()->image('test.jpg', 250, 1000); + $media = MediaLibrary::instance(); + $media->put('/pages/neu/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages/neu/test.jpg', null]); + + $this->assertFileCount(2, 'pages/neu'); + } + + public function testDontGenerateSizeIfImageWouldBeLarger(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', []); + Setting::set('breakpoints', ['250']); + + $file = UploadedFile::fake()->image('test.jpg', 249, 1000); + $media = MediaLibrary::instance(); + $media->put('/pages/neu/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages/neu/test.jpg', null]); + + $this->assertFileCount(2, 'pages/neu'); + } + + public function testGenerateBreakpointImage(): void + { + Setting::set('folders', [['folder' => '/pages']]); + Setting::set('sizes', [['name' => 'testas', 'aspect_ratio' => '5x8']]); + Setting::set('breakpoints', ['100']); + + $file = UploadedFile::fake()->image('test.jpg', 500, 400); + $media = MediaLibrary::instance(); + $media->put('/pages/neu/test.jpg', $file->get()); + Event::fire('media.file.upload', [null, '/pages/neu/test.jpg', null]); + + $this->assertFileCount(5, 'pages/neu'); + $this->assertHasFile('pages/neu/test.jpg'); + $this->assertHasFile('pages/neu/test-500x400.jpg'); + $this->assertHasFile('pages/neu/test-250x400.jpg'); + $this->assertHasFile('pages/neu/test-100x160.jpg'); + $this->assertHasFile('pages/neu/test-100x80.jpg'); + } + +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..649a924 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,41 @@ +media = MediaLibrary::instance(); + } + + public function assertHasFile($file): void + { + Storage::disk('local')->assertExists("uploads/public/c/{$this->normalizeFilePath($file)}"); + } + + public function assertDoesntHaveFile($file): void + { + Storage::disk('local')->assertMissing("uploads/public/c/{$this->normalizeFilePath($file)}"); + } + + public function assertFileCount($count, $dir): void + { + $this->assertCount($count, Storage::disk('local')->files("uploads/public/c/{$this->normalizeFilePath($dir)}")); + } + + public function normalizeFilePath(string $path): string + { + return preg_replace('|^/*|', '', $path); + } +} diff --git a/tests/stub/img.jpg b/tests/stub/img.jpg new file mode 100644 index 0000000..c9dd7a5 Binary files /dev/null and b/tests/stub/img.jpg differ