From fdc9dceb8ae81de4dc9ada709c87e62b9cf93a18 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 10:18:29 -0700 Subject: [PATCH 01/39] Making PHP 5.4 the new minimum --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a617f14..db44427 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "authors": [{ "name": "Robert Reinhard" } ], "license": "MIT", "require": { - "php": ">=5.3.0", + "php": ">=5.4.0", "illuminate/support": "~5.0", "weotch/PHPThumb": "~1.0", "symfony/http-kernel": "~2.0" From b32b7c1a05515b06cac7129dfac130aca9aada03 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 10:19:07 -0700 Subject: [PATCH 02/39] Refactoring service providers into a single provider Also, for a simpler supporting of L5, ditching the "local" config file --- src/Bkwld/Croppa/ServiceProvider.php | 151 +++++++++++-------- src/Bkwld/Croppa/ServiceProviderLaravel4.php | 41 ----- src/Bkwld/Croppa/ServiceProviderLaravel5.php | 48 ------ src/config/config.php | 2 +- src/config/local/config.php | 6 - 5 files changed, 88 insertions(+), 160 deletions(-) delete mode 100644 src/Bkwld/Croppa/ServiceProviderLaravel4.php delete mode 100644 src/Bkwld/Croppa/ServiceProviderLaravel5.php delete mode 100755 src/config/local/config.php diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index a8e3b77..9f8a455 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -1,74 +1,97 @@ app::VERSION); + } - /** - * Create a new service provider instance. - * - * @param \Illuminate\Contracts\Foundation\Application $app - * @return void - */ - public function __construct($app) - { - parent::__construct($app); - $this->provider = $this->getProvider(); - } + /** + * Register the service provider. + * + * @return void + */ + public function register() { - /** - * Bootstrap the application events. - * - * @return void - */ - public function boot() - { - return $this->provider->boot(); - } + // Version specific registering + if ($this->version() == 5) $this->registerLaravel5(); - /** - * Register the service provider. - * - * @return void - */ - public function register() - { - return $this->provider->register(); - } + // Bind a new singleton instance of Croppa to the app + $this->app->singleton('croppa', function($app) { - /** - * Return ServiceProvider according to Laravel version - */ - private function getProvider() - { - $app = $this->app; - $version = intval($app::VERSION); - $provider = sprintf( - '\Bkwld\Croppa\ServiceProviderLaravel%d', $version - ); + // Inject dependencies + return new Croppa(array_merge($app->make('config')->get('croppa::config'), [ + 'host' => '//'.$app->make('request')->getHttpHost(), + 'public' => $app->make('path.public'), + ])); + }); + } - return new $provider($app); - } + /** + * Register specific logic for Laravel 5. Merges package config with user config + * + * @return void + */ + public function registerLaravel5() { + $this->mergeConfigFrom(__DIR__.'/../../config/config.php', 'croppa'); + } - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return array('croppa'); - } + /** + * Bootstrap the application events. + * + * @return void + */ + public function boot() { + + // Version specific booting + switch($this->version()) { + case 4: $this->bootLaravel4(); break; + case 5: $this->bootLaravel5(); break; + default: throw new Exception('Unsupported Laravel version'); + } + + // Listen for Cropa style URLs, these are how Croppa gets triggered + $croppa = $this->app['croppa']; + $this->app->make('router')->get('{path}', function($path) use ($croppa) { + $image = $croppa->generate($path); + return \Response::stream(function() use ($image) { + return $image->show(); + }); + })->where('path', $croppa->directoryPattern()); + } + + /** + * Boot specific logic for Laravel 4. Tells Laravel about the package for auto namespacing of + * config files + * + * @return void + */ + public function bootLaravel4() { + $this->package('bkwld/croppa'); + } + + /** + * Boot specific logic for Laravel 5. Registers the config file for publishing to app directory + * + * @return void + */ + public function bootLaravel5() { + $this->publishes([ + __DIR__.'/../../config/config.php' => config_path('croppa.php') + ], 'config'); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() { + return ['croppa']; + } } diff --git a/src/Bkwld/Croppa/ServiceProviderLaravel4.php b/src/Bkwld/Croppa/ServiceProviderLaravel4.php deleted file mode 100644 index 29e5dbb..0000000 --- a/src/Bkwld/Croppa/ServiceProviderLaravel4.php +++ /dev/null @@ -1,41 +0,0 @@ -app->singleton('croppa', function($app) { - - // Inject dependencies - return new Croppa(array_merge($app->make('config')->get('croppa::config'), array( - 'host' => '//'.$this->app->make('request')->getHttpHost(), - 'public' => $app->make('path.public'), - ))); - }); - } - - /** - * Bootstrap the application events. - * - * @return void - */ - public function boot() { - $this->package('bkwld/croppa'); - - // Listen for Cropa style URLs, these are how Croppa gets triggered - $croppa = $this->app['croppa']; - $this->app->make('router')->get('{path}', function($path) use ($croppa) { - $image = $croppa->generate($path); - return \Response::stream(function() use ($image) { - return $image->show(); - }); - })->where('path', $croppa->directoryPattern()); - } - -} diff --git a/src/Bkwld/Croppa/ServiceProviderLaravel5.php b/src/Bkwld/Croppa/ServiceProviderLaravel5.php deleted file mode 100644 index 6582329..0000000 --- a/src/Bkwld/Croppa/ServiceProviderLaravel5.php +++ /dev/null @@ -1,48 +0,0 @@ -environment('local')) { - $config_file = __DIR__.'/../../config/local/config.php'; - $this->mergeConfigFrom($config_file, 'croppa'); - } - $this->mergeConfigFrom(__DIR__.'/../../config/config.php', 'croppa'); - - // Bind a new singleton instance of Croppa to the app - $this->app->singleton('croppa', function($app) { - // Inject dependencies - return new Croppa(array_merge($app['config']->get('croppa'), array( - 'host' => '//'.$app->make('request')->getHttpHost(), - 'public' => $app->make('path.public'), - ))); - }); - } - - /** - * Bootstrap the application events. - * - * @return void - */ - public function boot() { - - $this->publishes(array( - __DIR__.'/../../config/config.php' => config_path('croppa.php') - )); - - // Listen for Cropa style URLs, these are how Croppa gets triggered - $croppa = $this->app['croppa']; - $this->app->make('router')->get('{path}', function($path) use ($croppa) { - $image = $croppa->generate($path); - return \Response::stream(function() use ($image) { - return $image->show(); - }); - })->where('path', $croppa->directoryPattern()); - } -} diff --git a/src/config/config.php b/src/config/config.php index e4abb3e..f67ea33 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -13,7 +13,7 @@ * up your hard drive with images. Set to false or comment * out to have no limit. */ - 'max_crops' => 12, + 'max_crops' => App::isLocal() ? false : 12, /** * The jpeg quality of generated images. The difference between diff --git a/src/config/local/config.php b/src/config/local/config.php deleted file mode 100755 index 39edf78..0000000 --- a/src/config/local/config.php +++ /dev/null @@ -1,6 +0,0 @@ - false, - -); \ No newline at end of file From a8e553a63d4711122ecd07a590768f504be27e48 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 10:56:50 -0700 Subject: [PATCH 03/39] Removing unused directories that were part of L4 package boilerplate --- src/config/.gitkeep | 0 src/lang/.gitkeep | 0 src/migrations/.gitkeep | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/config/.gitkeep delete mode 100644 src/lang/.gitkeep delete mode 100644 src/migrations/.gitkeep diff --git a/src/config/.gitkeep b/src/config/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/lang/.gitkeep b/src/lang/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/migrations/.gitkeep b/src/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 From 5c9583bb1bb7af06a3bcda57cd812eb5c09601bd Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 11:01:44 -0700 Subject: [PATCH 04/39] Adding and documenting new config options --- src/config/config.php | 77 +++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/src/config/config.php b/src/config/config.php index f67ea33..bf984d5 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -1,50 +1,71 @@ array( - App::make('path.public').'/uploads', - ), + 'src_dir' => public_path().'/uploads', + + /** + * The directory where cropped images should be saved. The route to the + * cropped versions is what should be rendered in your markup; it must be a + * web accessible directory. + * + * @var string Absolute path in local fileystem + * | string IoC binding name of League\Flysystem\Filesystem + * | string IoC binding name of League\Flysystem\Cached\CachedAdapter + */ + 'crops_dir' => public_path().'/uploads', + + /** + * A regex pattern that locates the path to the image relative to the + * crops_dir. This path will be used to find the source image in the src_dir. + * The path component of the regex must exist in the first captured + * subpattern. In other words, in the `preg_match` $matches[1]. + * + * @var string + */ + 'path' => '^https?://[^/]+/uploads/(.*)$', /** - * Maximum number of sizes to allow for a particular - * source file. This is to limit scripts from filling - * up your hard drive with images. Set to false or comment - * out to have no limit. + * Maximum number of sizes to allow for a particular source file. This is to + * limit scripts from filling up your hard drive with images. Set to false or + * comment out to have no limit. + * + * @var integer | boolean */ 'max_crops' => App::isLocal() ? false : 12, /** - * The jpeg quality of generated images. The difference between - * 100 and 95 usually cuts the file size in half. Going down to - * 70 looks ok on photos and will reduce filesize by more than another - * half but on vector files there is noticeable aliasing. + * The jpeg quality of generated images. The difference between 100 and 95 + * usually cuts the file size in half. Going down to 70 looks ok on photos + * and will reduce filesize by more than another half but on vector files + * there is noticeable aliasing. + * + * @var integer */ 'jpeg_quality' => 95, /** * Turn on interlacing to make progessive jpegs + * + * @var boolean */ 'interlace' => true, /** - * Optional. Specify the host for Croppa::url() to use when generating - * absolute paths to images. If undefined and using Laravel, - * the `Request::host()` is used by default. - */ - // 'host' => 'http://mydomain.com', - - /** - * Optional. Specify the route to the document_root of your app. If undefined - * and using Laravel, the `public_path()` is used by default. - */ - // 'public' => '/absolute/path/to/document_root', - - /** - * Optional. Ignore cropping for image URLs that match a regular - * expression. Useful for returning animated gifs. + * Specify the host for Croppa::url() to use when generating URLs. An + * altenative to the default is to use the app.url setting: + * + * preg_replace('#https?:#', '', Config::get('app.url')) + * + * @var string */ - // 'ignore' => '.+\.gif$', + 'host' => '//'.Request::getHttpHost(), ); \ No newline at end of file From b8ffb88cc0142a6a00ce0b31e80b9e2a3f38a630 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 11:01:49 -0700 Subject: [PATCH 05/39] Rewrapping lines --- src/Bkwld/Croppa/ServiceProvider.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index 9f8a455..66ec428 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -66,8 +66,8 @@ public function boot() { } /** - * Boot specific logic for Laravel 4. Tells Laravel about the package for auto namespacing of - * config files + * Boot specific logic for Laravel 4. Tells Laravel about the package for auto + * namespacing of config files * * @return void */ @@ -76,7 +76,8 @@ public function bootLaravel4() { } /** - * Boot specific logic for Laravel 5. Registers the config file for publishing to app directory + * Boot specific logic for Laravel 5. Registers the config file for publishing + * to app directory * * @return void */ From b454bf897803d281dbc98898c049b5deae3a598c Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 13:23:39 -0700 Subject: [PATCH 06/39] Removing migrations from autoload config --- composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/composer.json b/composer.json index db44427..c9458e4 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,6 @@ "mikey179/vfsStream": "~1.3" }, "autoload": { - "classmap": [ - "src/migrations" - ], "psr-0": { "Bkwld\\Croppa": "src/" } From 8c5ee294ff1f29bbc89833ec84591330720fbae5 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 15:22:04 -0700 Subject: [PATCH 07/39] Supporting L4 and L5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c9458e4..51a7af8 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=5.4.0", - "illuminate/support": "~5.0", + "illuminate/support": "4.0 - 5.0", "weotch/PHPThumb": "~1.0", "symfony/http-kernel": "~2.0" }, From 0def49f2694f8f6a5ebc4d4136f8a69b6b6cb83d Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 15:23:17 -0700 Subject: [PATCH 08/39] Removing hostname from the path match We'll use the path returned from the route closure --- src/config/config.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/config/config.php b/src/config/config.php index bf984d5..9a4b8fb 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -23,14 +23,15 @@ 'crops_dir' => public_path().'/uploads', /** - * A regex pattern that locates the path to the image relative to the - * crops_dir. This path will be used to find the source image in the src_dir. - * The path component of the regex must exist in the first captured - * subpattern. In other words, in the `preg_match` $matches[1]. + * A regex pattern that compares against the Request path (`Request::path()`) + * to find the image path to the image relative to the crops_dir. This path + * will be used to find the source image in the src_dir. The path component of + * the regex must exist in the first captured subpattern. In other words, in + * the `preg_match` $matches[1]. * * @var string */ - 'path' => '^https?://[^/]+/uploads/(.*)$', + 'path' => 'uploads/(.*)$', /** * Maximum number of sizes to allow for a particular source file. This is to From 522c16d2f08dcdff10cfeef81066e2d602b52039 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 15:23:56 -0700 Subject: [PATCH 09/39] Re-adding config that allows passingthru of crops Was "ignore" before --- src/config/config.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/config/config.php b/src/config/config.php index 9a4b8fb..f8b01c7 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -32,6 +32,16 @@ * @var string */ 'path' => 'uploads/(.*)$', + + /** + * A regex pattern that works like `path` except it is applied AFTER Croppa + * handles a request. Croppa will create a cropped file, but it will passthru + * the source image. This is designed, in particular, for animated gifs which + * lose animation when cropped. + * + * @var string + */ + 'passthru' => '\.(?:gif|GIF)$', /** * Maximum number of sizes to allow for a particular source file. This is to From 9ffd6646785519a36c8bb6af7bc62a146b7caa65 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 15:25:59 -0700 Subject: [PATCH 10/39] Re-implementing handler logic --- src/Bkwld/Croppa/Handler.php | 26 ++++++++++++ src/Bkwld/Croppa/ServiceProvider.php | 34 ++++++++------- src/Bkwld/Croppa/URL.php | 63 ++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/Bkwld/Croppa/Handler.php create mode 100644 src/Bkwld/Croppa/URL.php diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php new file mode 100644 index 0000000..88ca42d --- /dev/null +++ b/src/Bkwld/Croppa/Handler.php @@ -0,0 +1,26 @@ +generate($path); + // return \Response::stream(function() use ($image) { + // return $image->show(); + // }); + + } + +} \ No newline at end of file diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index 66ec428..da1ee56 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -8,7 +8,8 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider { * @return integer */ public function version() { - return intval($this->app::VERSION); + $app = $this->app; + return intval($app::VERSION); } /** @@ -23,12 +24,17 @@ public function register() { // Bind a new singleton instance of Croppa to the app $this->app->singleton('croppa', function($app) { + return new Croppa($app->make('config')->get('croppa::config')); + }); + + // Bind the Croppa URL generator and parser + $this->app->singleton('croppa.url', function($app) { + return new URL($app->make('config')->get('croppa::config')); + }); - // Inject dependencies - return new Croppa(array_merge($app->make('config')->get('croppa::config'), [ - 'host' => '//'.$app->make('request')->getHttpHost(), - 'public' => $app->make('path.public'), - ])); + // Bind the Croppa URL generator and parser + $this->app->singleton('croppa.handler', function($app) { + return new Handler; }); } @@ -56,13 +62,9 @@ public function boot() { } // Listen for Cropa style URLs, these are how Croppa gets triggered - $croppa = $this->app['croppa']; - $this->app->make('router')->get('{path}', function($path) use ($croppa) { - $image = $croppa->generate($path); - return \Response::stream(function() use ($image) { - return $image->show(); - }); - })->where('path', $croppa->directoryPattern()); + $this->app['router']->get('{path}', function($path) { + return $this->app['croppa.handler']->handle($path); + })->where('path', app('croppa.url')->routePattern()); } /** @@ -93,6 +95,10 @@ public function bootLaravel5() { * @return array */ public function provides() { - return ['croppa']; + return [ + 'croppa', + 'croppa.url', + 'croppa.handler', + ]; } } diff --git a/src/Bkwld/Croppa/URL.php b/src/Bkwld/Croppa/URL.php new file mode 100644 index 0000000..4fc3a62 --- /dev/null +++ b/src/Bkwld/Croppa/URL.php @@ -0,0 +1,63 @@ +config = $config; + } + + /** + * Return the Croppa URL regex + * + * @return string + */ + public function pattern() { + $pattern = ''; + + // Add rest of the path up to croppa's extension + $pattern .= '(.+)'; + + // Check for the size bounds + $pattern .= '-([0-9_]+)x([0-9_]+)'; + + // Check for options that may have been added + $pattern .= '(-[0-9a-zA-Z(),\-._]+)*'; + + // Check for possible image suffixes. + $pattern .= '\.(jpg|jpeg|png|gif|JPG|JPEG|PNG|GIF)$'; + + // Return it + return $pattern; + } + + /** + * Make the regex for the route definition. This works by wrapping both the + * basic Croppa pattern and the `path` config in positive regex lookaheads so + * they working like an AND condition. + * https://regex101.com/r/kO6kL1/1 + * + * In the Laravel router, this gets wrapped with some extra regex before the + * matching happnens and for the pattern to match correctly, the final .* needs + * to exist. Otherwise, the lookaheads have no length and the regex fails + * https://regex101.com/r/xS3nQ2/1 + * + * @return string + */ + public function routePattern() { + return sprintf("(?=%s)(?=%s).+", $this->config['path'], $this->pattern()); + } + +} \ No newline at end of file From 248996adc20a5b8d12d13e7e8370f53fc7e3c307 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 16:34:01 -0700 Subject: [PATCH 11/39] Parsing the request path and adding unit tests --- src/Bkwld/Croppa/Handler.php | 35 +++++++++++++++++-- src/Bkwld/Croppa/ServiceProvider.php | 2 +- src/Bkwld/Croppa/URL.php | 52 ++++++++++++++++++++++++++++ tests/.gitkeep | 0 tests/TestRouteRegistration.php | 27 --------------- tests/TestUrlMatching.php | 52 ++++++++++++++++++++++++++++ tests/TestUrlParsing.php | 50 ++++++++++++++++++++++++++ 7 files changed, 187 insertions(+), 31 deletions(-) delete mode 100644 tests/.gitkeep delete mode 100644 tests/TestRouteRegistration.php create mode 100644 tests/TestUrlMatching.php create mode 100644 tests/TestUrlParsing.php diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index 88ca42d..9f318f6 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -8,14 +8,43 @@ */ class Handler extends Controller { + /** + * @var Bkwld\Croppa\URL + */ + private $url; + + /** + * Dependency injection + * + * @param Bkwld\Croppa\URL $url + */ + public function __construct(URL $url) { + $this->url = $url; + } + /** * Handles a Croppa style route * - * @param string $path The `Request::path()` + * @param string $request The `Request::path()` * @return Symfony\Component\HttpFoundation\StreamedResponse */ - public function handle($path) { - dd($path); + public function handle($request) { + + // Parse the path + list($path, $width, $height, $options) = $this->url->parse($request); + + // Look for the src image + print_r($this->url->parse($request)); + + // Crop the image + + // Write the image to the crop dir + + // Render the image to the browser + + + + // $image = $croppa->generate($path); // return \Response::stream(function() use ($image) { // return $image->show(); diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index da1ee56..c93aa53 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -34,7 +34,7 @@ public function register() { // Bind the Croppa URL generator and parser $this->app->singleton('croppa.handler', function($app) { - return new Handler; + return new Handler($app['croppa.url']); }); } diff --git a/src/Bkwld/Croppa/URL.php b/src/Bkwld/Croppa/URL.php index 4fc3a62..325ed50 100644 --- a/src/Bkwld/Croppa/URL.php +++ b/src/Bkwld/Croppa/URL.php @@ -60,4 +60,56 @@ public function routePattern() { return sprintf("(?=%s)(?=%s).+", $this->config['path'], $this->pattern()); } + /** + * Parse a request path into Croppa instructions + * + * @param string $request + * @return array | boolean + */ + public function parse($request) { + if (!preg_match('#'.$this->pattern().'#', $request, $matches)) return false; + return [ + $this->parseRelativePath($matches[1].'.'.$matches[5]), // Path + $matches[2] == '_' ? null : (int) $matches[2], // Width + $matches[3] == '_' ? null : (int) $matches[3], // Height + $this->parseOptions($matches[4]), // Options + ]; + } + + /** + * Take the path with Croppa options removed and get the path relative + * to the crops_dir + * + * @param string $path + * @return string + */ + protected function parseRelativePath($path) { + preg_match('#'.$this->config['path'].'#', $path, $matches); + return $matches[1]; + } + + /** + * Create options array where each key is an option name + * and the value if an array of the passed arguments + * + * @param string $option_params Options string in the Croppa URL style + * @return array + */ + protected function parseOptions($option_params) { + $options = array(); + + // These will look like: "-quadrant(T)-resize" + $option_params = explode('-', $option_params); + + // Loop through the params and make the options key value pairs + foreach($option_params as $option) { + if (!preg_match('#(\w+)(?:\(([\w,.]+)\))?#i', $option, $matches)) continue; + if (isset($matches[2])) $options[$matches[1]] = explode(',', $matches[2]); + else $options[$matches[1]] = null; + } + + // Return new options array + return $options; + } + } \ No newline at end of file diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/TestRouteRegistration.php b/tests/TestRouteRegistration.php deleted file mode 100644 index fbd55c0..0000000 --- a/tests/TestRouteRegistration.php +++ /dev/null @@ -1,27 +0,0 @@ - '/var/www/public', - 'src_dirs' => array('/var/www/public/uploads', '/var/www/public/more'), - )); - $this->assertRegExp('#'.$croppa->directoryPattern().'#', 'uploads/photo-300x200.png'); - } - - public function testOutsideSrcDir() { - $croppa = new Croppa(array( - 'public' => '/var/www/public', - 'src_dirs' => array('/var/www/public/uploads', '/var/www/public/more'), - )); - $this->assertNotRegExp('#'.$croppa->directoryPattern().'#', 'apple-touch-icon-152x152-precomposed.png'); - } - -} \ No newline at end of file diff --git a/tests/TestUrlMatching.php b/tests/TestUrlMatching.php new file mode 100644 index 0000000..e163874 --- /dev/null +++ b/tests/TestUrlMatching.php @@ -0,0 +1,52 @@ +url = new URL([ + 'path' => 'uploads/(.*)$', + ]); + } + + /** + * This mimics the Illuminate\Routing\Matching\UriValidator compiled regex + * https://regex101.com/r/xS3nQ2/1 + */ + public function match($path) { + + // The compiled regex is wrapped like this + $pattern = '#^\/(?P'.$this->url->routePattern().')$#s'; + + // UriValidator prepends a slash + return preg_match($pattern, '/'.$path) > 0; + } + + public function testNoParams() { + $this->assertFalse($this->match('uploads/1/2/file.jpg')); + } + + public function testOursideDir() { + $this->assertFalse($this->match('assets/1/2/file.jpg')); + $this->assertFalse($this->match('apple-touch-icon-152x152-precomposed.png')); + } + + public function testWidth() { + $this->assertTrue($this->match('uploads/1/2/file-200x_.jpg')); + } + + public function testHeight() { + $this->assertTrue($this->match('uploads/1/2/file-_x100.jpg')); + } + + public function testWidthAndHeight() { + $this->assertTrue($this->match('uploads/1/2/file-200x100.jpg')); + } + + public function testWidthAndHeightAndOptions() { + $this->assertTrue($this->match('uploads/1/2/file-200x100-quadrant(T).jpg')); + } + +} \ No newline at end of file diff --git a/tests/TestUrlParsing.php b/tests/TestUrlParsing.php new file mode 100644 index 0000000..e87e3d5 --- /dev/null +++ b/tests/TestUrlParsing.php @@ -0,0 +1,50 @@ +url = new URL([ + 'src_dir' => '/uploads', + 'crops_dir' => '/uploads', + 'path' => 'uploads/(.*)$', + ]); + } + + public function testNoParams() { + $this->assertFalse($this->url->parse('uploads/1/2/file.jpg')); + } + + public function testWidth() { + $this->assertEquals([ + '1/2/file.jpg', 200, null, [] + ], $this->url->parse('uploads/1/2/file-200x_.jpg')); + } + + public function testHeight() { + $this->assertEquals([ + '1/2/file.jpg', null, 100, [] + ], $this->url->parse('uploads/1/2/file-_x100.jpg')); + } + + public function testWidthAndHeight() { + $this->assertEquals([ + '1/2/file.jpg', 200, 100, [] + ], $this->url->parse('uploads/1/2/file-200x100.jpg')); + } + + public function testWidthAndHeightAndOptions() { + $this->assertEquals([ + '1/2/file.jpg', 200, 100, ['resize' => null] + ], $this->url->parse('uploads/1/2/file-200x100-resize.jpg')); + } + + public function testWidthAndHeightAndOptionsWithValue() { + $this->assertEquals([ + '1/2/file.jpg', 200, 100, ['quadrant' => ['T']] + ], $this->url->parse('uploads/1/2/file-200x100-quadrant(T).jpg')); + } + +} \ No newline at end of file From 7dc597857984289124d874a74de3a87f1aa82708 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 3 Apr 2015 16:48:12 -0700 Subject: [PATCH 12/39] Handler is no longer a controller --- src/Bkwld/Croppa/Handler.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index 9f318f6..984aadb 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -1,12 +1,9 @@ url->parse($request); + // Parse the path. In the case there is an error (the pattern on the route + // SHOULD have caught all errors with the pattern) just return + if (!$params = $this->url->parse($request)) return; + list($path, $width, $height, $options) = $params; + + // If the crops_dir is a remote disk, check if the path exists on it and redirect // Look for the src image print_r($this->url->parse($request)); From 7c1a508963f3cf17bfc61b8b1870a6151bd25e7d Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 6 Apr 2015 15:31:01 -0700 Subject: [PATCH 13/39] Building Flysystem disks via Storage class and starting on a beefier Image class --- composer.json | 6 +- src/Bkwld/Croppa/Handler.php | 24 ++++-- src/Bkwld/Croppa/Image.php | 21 ++--- src/Bkwld/Croppa/ServiceProvider.php | 10 ++- src/Bkwld/Croppa/Storage.php | 116 +++++++++++++++++++++++++++ src/Bkwld/Croppa/URL.php | 14 ++++ 6 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 src/Bkwld/Croppa/Storage.php diff --git a/composer.json b/composer.json index 51a7af8..de68e9f 100644 --- a/composer.json +++ b/composer.json @@ -7,11 +7,13 @@ "php": ">=5.4.0", "illuminate/support": "4.0 - 5.0", "weotch/PHPThumb": "~1.0", - "symfony/http-kernel": "~2.0" + "symfony/http-kernel": "~2.0", + "league/flysystem": "~1.0" }, "require-dev": { "phpunit/phpunit": "3.7.*", - "mikey179/vfsStream": "~1.3" + "mikey179/vfsStream": "~1.3", + "mockery/mockery": "~0.9" }, "autoload": { "psr-0": { diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index 984aadb..d07c3ee 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -5,6 +5,11 @@ */ class Handler { + /** + * @var Bkwld\Croppa\Storage + */ + private $storage; + /** * @var Bkwld\Croppa\URL */ @@ -14,9 +19,11 @@ class Handler { * Dependency injection * * @param Bkwld\Croppa\URL $url + * @param Bkwld\Croppa\Storage $storage */ - public function __construct(URL $url) { + public function __construct(URL $url, Storage $storage) { $this->url = $url; + $this->storage = $storage; } /** @@ -33,16 +40,23 @@ public function handle($request) { list($path, $width, $height, $options) = $params; // If the crops_dir is a remote disk, check if the path exists on it and redirect + if ($this->storage->cropsAreRemote()) { + // WILL NEED TO ADD A CONFIG TO SET THE PREFIX URL FOR THIS, LIKE UPCHUCK + } - // Look for the src image - print_r($this->url->parse($request)); + // Build a new image using fetched image data + $image = new Image( + $this->storage->getSrc($path), + $this->url->phpThumbConfig($options) + ); - // Crop the image + // Process the image + // Write the image to the crop dir // Render the image to the browser - + return $image->show(); diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 8558cba..51fb116 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -8,11 +8,6 @@ */ class Image { - /** - * @var string - */ - private $path; - /** * @var GdThumb */ @@ -21,29 +16,27 @@ class Image { /** * Constructor * - * @param GdThumb $thumb A thumb instance - * @param string $path Absolute path to the file in the filesystem + * @param string $data Image data as a string + * @param array $options */ - public function __construct(GdThumb $thumb, $path) { - $this->thumb = $thumb; - $this->path = $path; + public function __construct($data, $config) { + $this->thumb = PhpThumbFactory::create($src, $config, true); } /** - * Output an image to the browser. Accepts a string path - * or a PhpThumb instance + * Output to the browser. * * @return Binary image data */ public function show() { - // If headers already sent, abort. + // If headers already sent, abort if (headers_sent()) return; // Set the header for the filesize and a bunch of other stuff header("Content-Transfer-Encoding: binary"); header("Accept-Ranges: bytes"); - header("Content-Length: ".filesize($this->path)); + // header("Content-Length: ".filesize($this->path)); // Display it $this->thumb->show(); diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index c93aa53..c295367 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -32,9 +32,14 @@ public function register() { return new URL($app->make('config')->get('croppa::config')); }); - // Bind the Croppa URL generator and parser + // Handle the request for an image, this cooridnates the main logic $this->app->singleton('croppa.handler', function($app) { - return new Handler($app['croppa.url']); + return new Handler($app['croppa.url'], $app['croppa.storage']); + }); + + // Interact with the disk + $this->app->singleton('croppa.storage', function($app) { + return Storage::make($app, $app->make('config')->get('croppa::config')); }); } @@ -99,6 +104,7 @@ public function provides() { 'croppa', 'croppa.url', 'croppa.handler', + 'croppa.storage', ]; } } diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php new file mode 100644 index 0000000..5bc50c5 --- /dev/null +++ b/src/Bkwld/Croppa/Storage.php @@ -0,0 +1,116 @@ +app = $app; + $this->config = $config; + } + + /** + * Factory function to create an instance and then "mount" disks + * + * @param Illuminate\Container\Container + * @param array $config + * @return Bkwld\Croppa\Storage + */ + static public function make($app, $config) { + return with(new static($app, $config))->mount(); + } + + /** + * "Mount" disks give the config + * + * @return $this + */ + public function mount() { + $this->src_disk = $this->makeDisk($this->config['src_dir']); + $this->crops_disk = $this->makeDisk($this->config['crops_dir']); + return $this; + } + + /** + * Return whether crops are stored remotely + * + * @return boolean + */ + public function cropsAreRemote() { + + // Currently, the CachedAdapter doesn't have a getAdapter method so I can't + // tell if the adapter is local or not. I'm assuming that if they are using + // the CachedAdapter, they're probably using a remote disk. I've written + // a PR to add getAdapter to it. + // https://github.com/thephpleague/flysystem-cached-adapter/pull/9 + if (!method_exists($this->crops_disk, 'getAdapter')) return true; + + // Check if the crop disk is not local + return !is_a($this->crops_disk, 'League\Flysystem\Adapter\Local'); + } + + /** + * Get the src image data or throw an exception + * + * @param string $path Path to image relative to dir + * @throws Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * @return string + */ + public function getSrc($path) { + if ($this->src_disk->has($path)) return $this->src_disk->read($path); + else throw new NotFoundHttpException('Croppa: Referenced file missing'); + } + + /** + * Use or instantiate a Flysystem disk + * + * @param string $dir The value from one of the config dirs + * @return League\Flysystem\Filesystem | League\Flysystem\Cached\CachedAdapter + */ + public function makeDisk($dir) { + + // Check if the dir refers to an IoC binding and return it + if ($this->app->bound($dir) + && ($instance = $this->app->make($dir)) + && (is_a($instance, 'League\Flysystem\Filesystem') + || is_a($instance, 'League\Flysystem\Cached\CachedAdapter')) + ) return $instance; + + // Instantiate a new Flysystem instance for local dirs + return new Filesystem(new Adapter($dir)); + + } + +} \ No newline at end of file diff --git a/src/Bkwld/Croppa/URL.php b/src/Bkwld/Croppa/URL.php index 325ed50..2a906ae 100644 --- a/src/Bkwld/Croppa/URL.php +++ b/src/Bkwld/Croppa/URL.php @@ -112,4 +112,18 @@ protected function parseOptions($option_params) { return $options; } + /** + * Take options in the URL and options from the config file and produce a + * config array in the format that PhpThumb expects + * + * @param array $options The url options from `parseOptions()` + * @return array + */ + public function phpThumbConfig($options) { + return [ + 'jpegQuality' => isset($options['quality']) ? $options['quality'] : $this->config['jpeg_quality'], + 'interlace' => isset($options['interlace']) ? $options['interlace'] : $this->config['interlace'], + ]; + } + } \ No newline at end of file From 0661918118cf371a4b4ae3d82a56be84ae72779c Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 6 Apr 2015 15:31:46 -0700 Subject: [PATCH 14/39] Fixing some fatal errors Images are passing through now --- src/Bkwld/Croppa/Image.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 51fb116..5017f1e 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -1,7 +1,7 @@ thumb = PhpThumbFactory::create($src, $config, true); + $this->thumb = PhpThumbFactory::create($data, $config, true); } /** From 5bc1eb68e36b889ff0c070b348feaaf233919e77 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 6 Apr 2015 15:52:50 -0700 Subject: [PATCH 15/39] Porting trim functionality over --- src/Bkwld/Croppa/Handler.php | 10 ++++-- src/Bkwld/Croppa/Image.php | 67 ++++++++++++++++++++++++++++++++++++ src/config/config.php | 11 +++--- 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index d07c3ee..4ee00f3 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -39,10 +39,16 @@ public function handle($request) { if (!$params = $this->url->parse($request)) return; list($path, $width, $height, $options) = $params; + // Check if there are too many crops already + // if ($this->storage->tooManyCrops()) throw new Exception('Croppa: Max crops reached'); + // If the crops_dir is a remote disk, check if the path exists on it and redirect if ($this->storage->cropsAreRemote()) { // WILL NEED TO ADD A CONFIG TO SET THE PREFIX URL FOR THIS, LIKE UPCHUCK - } + } + + // Increase memory limit, cause some images require a lot to resize + ini_set('memory_limit', '128M'); // Build a new image using fetched image data $image = new Image( @@ -51,7 +57,7 @@ public function handle($request) { ); // Process the image - + $image->process($width, $height, $options); // Write the image to the crop dir diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 5017f1e..206db86 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -23,6 +23,73 @@ public function __construct($data, $config) { $this->thumb = PhpThumbFactory::create($data, $config, true); } + /** + * Take the input from the URL and apply transformations on the image + * + * @param integer $width + * @param integer $height + * @param array $options + * @return $this + */ + public function process($width, $height, $options) { + return $this + ->autoRotate() + ->trim($options) + ; + } + + /** + * Auto rotate the image based on exif data (like from phones) + * https://github.com/nik-kor/PHPThumb/blob/master/src/thumb_plugins/jpg_rotate.inc.php + */ + public function autoRotate() { + $this->thumb->rotateJpg(); + return $this; + } + + /** + * Trim the source before applying the crop. This is designed to be used in + * conjunction with a cropping UI tool like jCrop. + * http://deepliquid.com/content/Jcrop.html + * + * @param array $options + * @return $this + */ + public function trim($options) { + if (isset($options['trim'])) $this->trimPixels($options['trim']); + else if (isset($options['trim_perc'])) $this->trimPerc($options['trim_perc']); + return $this; + } + + /** + * Trim the source before applying the crop with as offset pixels + * + * @param array $options Cropping instructions as pixels + * @return void + */ + public function trimPixels($options) { + list($x1, $y1, $x2, $y2) = $options; + $this->thumb->crop($x1, $y1, $x2 - $x1, $y2 - $y1); + } + + /** + * Trim the source before applying the crop with offset percentages + * + * @param array $options Cropping instructions as percentages + * @return void + */ + public function trimPerc($options) { + list($x1, $y1, $x2, $y2) = $options; + $size = (object) $this->thumb->getCurrentDimensions(); + + // Convert percentage values to what GdThumb expects + $x = round($x1 * $size->width); + $y = round($y1 * $size->height); + $width = round($x2 * $size->width - $x); + $height = round($y2 * $size->height - $y); + $this->thumb->crop($x, $y, $width, $height); + } + /** * Output to the browser. * diff --git a/src/config/config.php b/src/config/config.php index f8b01c7..b52dd1d 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -34,11 +34,12 @@ 'path' => 'uploads/(.*)$', /** - * A regex pattern that works like `path` except it is applied AFTER Croppa - * handles a request. Croppa will create a cropped file, but it will passthru - * the source image. This is designed, in particular, for animated gifs which - * lose animation when cropped. - * + * A regex pattern that works like `path` except it is only used by the + * `Croppa::url($url)` generator function. If the $path url matches, it is + * passed through with no Croppa URL suffixes added. Thus, it will not be + * cropped. This is designed, in particular, for animated gifs which lose + * animation when cropped. + * * @var string */ 'passthru' => '\.(?:gif|GIF)$', From 08b3818c5f4ca3ecca6d98124430009de22002cd Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 6 Apr 2015 16:07:37 -0700 Subject: [PATCH 16/39] Cropping and reisizing ported over --- src/Bkwld/Croppa/Image.php | 90 +++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 206db86..315af70 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -35,6 +35,7 @@ public function process($width, $height, $options) { return $this ->autoRotate() ->trim($options) + ->resizeAndOrCrop($width, $height, $options) ; } @@ -48,38 +49,37 @@ public function autoRotate() { } /** - * Trim the source before applying the crop. This is designed to be used in - * conjunction with a cropping UI tool like jCrop. - * http://deepliquid.com/content/Jcrop.html + * Determine which trim to apply. * * @param array $options * @return $this */ public function trim($options) { - if (isset($options['trim'])) $this->trimPixels($options['trim']); - else if (isset($options['trim_perc'])) $this->trimPerc($options['trim_perc']); + if (isset($options['trim'])) return $this->trimPixels($options['trim']); + if (isset($options['trim_perc'])) return $this->trimPerc($options['trim_perc']); return $this; } /** * Trim the source before applying the crop with as offset pixels * - * @param array $options Cropping instructions as pixels - * @return void + * @param array $coords Cropping instructions as pixels + * @return $this */ - public function trimPixels($options) { + public function trimPixels($coords) { list($x1, $y1, $x2, $y2) = $options; $this->thumb->crop($x1, $y1, $x2 - $x1, $y2 - $y1); + return $this; } /** * Trim the source before applying the crop with offset percentages * - * @param array $options Cropping instructions as percentages - * @return void + * @param array $coords Cropping instructions as percentages + * @return $this */ - public function trimPerc($options) { - list($x1, $y1, $x2, $y2) = $options; + public function trimPerc($coords) { + list($x1, $y1, $x2, $y2) = $coords; $size = (object) $this->thumb->getCurrentDimensions(); // Convert percentage values to what GdThumb expects @@ -88,6 +88,72 @@ public function trimPerc($options) { $width = round($x2 * $size->width - $x); $height = round($y2 * $size->height - $y); $this->thumb->crop($x, $y, $width, $height); + return $this; + } + + /** + * Determine which resize and crop to apply + * + * @param integer $width + * @param integer $height + * @param array $options + * @return $this + */ + public function resizeAndOrCrop($width, $height, $options) { + if (isset($options['quadrant'])) return $this->cropQuadrant($width, $height, $options); + if (isset($options['resize']) || !$width || !$height) return $this->resize($width, $height); + return $this->crop($width, $height); + } + + /** + * Do a quadrant adaptive resize. Supported quadrant values are: + * +---+---+---+ + * | | T | | + * +---+---+---+ + * | L | C | R | + * +---+---+---+ + * | | B | | + * +---+---+---+ + * + * @param integer $width + * @param integer $height + * @param array $options + * @throws Exception + * @return $this + */ + public function cropQuadrant($width, $height, $options) { + if (!$height|| !$width) throw new Exception('Croppa: Qudrant option needs width and height'); + if (empty($options['quadrant'][0])) throw new Exception('Croppa:: No quadrant specified'); + $quadrant = strtoupper($options['quadrant'][0]); + if (!in_array($quadrant, array('T','L','C','R','B'))) throw new Exception('Croppa:: Invalid quadrant'); + $this->thumb->adaptiveResizeQuadrant($width, $height, $quadrant); + return $this; + } + + /** + * Resize with no cropping + * + * @param integer $width + * @param integer $height + * @return $this + */ + public function resize($width, $height) { + if ($width && $height) $this->thumb->resize($width, $height); + else if (!$width) $this->thumb->resize(99999, $height); + else if (!$height) $this->thumb->resize($width, 99999); + return $this; + } + + /** + * Resize and crop + * + * @param integer $width + * @param integer $height + * @return $this + */ + public function crop($width, $height) { + $this->thumb->adaptiveResize($width, $height); + return $this; } /** From 692c9849392f23bb055fd6c245f25258909032c9 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 6 Apr 2015 17:26:00 -0700 Subject: [PATCH 17/39] Writing cropped files, rendering them back out, and implementing redirect to remote crop URLs --- composer.json | 5 ++-- src/Bkwld/Croppa/Handler.php | 56 ++++++++++++++++++++++++------------ src/Bkwld/Croppa/Image.php | 20 ++++--------- src/Bkwld/Croppa/Storage.php | 40 ++++++++++++++++++++++++-- src/Bkwld/Croppa/URL.php | 12 ++++---- src/config/config.php | 7 +++++ 6 files changed, 96 insertions(+), 44 deletions(-) diff --git a/composer.json b/composer.json index de68e9f..69dc686 100644 --- a/composer.json +++ b/composer.json @@ -6,9 +6,10 @@ "require": { "php": ">=5.4.0", "illuminate/support": "4.0 - 5.0", - "weotch/PHPThumb": "~1.0", + "league/flysystem": "~1.0", + "symfony/http-foundation": "~2.0", "symfony/http-kernel": "~2.0", - "league/flysystem": "~1.0" + "weotch/PHPThumb": "~1.0" }, "require-dev": { "phpunit/phpunit": "3.7.*", diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index 4ee00f3..935124f 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -1,5 +1,9 @@ url->relativePath($request); + + // If the crops_dir is a remote disk, check if the path exists on it and redirect + if ($this->storage->cropsAreRemote() + && $this->storage->cropExists($crop_path)) { + return new RedirectResponse($this->storage->cropUrl($crop_path), 301); + } + // Parse the path. In the case there is an error (the pattern on the route // SHOULD have caught all errors with the pattern) just return if (!$params = $this->url->parse($request)) return; @@ -42,35 +55,42 @@ public function handle($request) { // Check if there are too many crops already // if ($this->storage->tooManyCrops()) throw new Exception('Croppa: Max crops reached'); - // If the crops_dir is a remote disk, check if the path exists on it and redirect - if ($this->storage->cropsAreRemote()) { - // WILL NEED TO ADD A CONFIG TO SET THE PREFIX URL FOR THIS, LIKE UPCHUCK - } - // Increase memory limit, cause some images require a lot to resize ini_set('memory_limit', '128M'); // Build a new image using fetched image data $image = new Image( - $this->storage->getSrc($path), + $this->storage->readSrc($path), $this->url->phpThumbConfig($options) ); - - // Process the image - $image->process($width, $height, $options); - // Write the image to the crop dir - - // Render the image to the browser - return $image->show(); - + // Process the image, get the image data back, and write to disk + $file = $this->storage->writeCrop($crop_path, + $image->process($width, $height, $options)->get() + ); + // Redirect to remote crops or render the image + if (preg_match('#^https?://#', $file)) return new RedirectResponse($file, 301); + else return new BinaryFileResponse($file, 200, [ + 'Content-Type' => $this->getContentType($path), + ]); - // $image = $croppa->generate($path); - // return \Response::stream(function() use ($image) { - // return $image->show(); - // }); + } + /** + * Symfony kept returning the MIME-type of my testing jpgs as PNGs, so + * determining it explicitly via looking at the path name. + * + * @param string $path + * @return string + */ + public function getContentType($path) { + switch(pathinfo($path, PATHINFO_EXTENSION)) { + case 'jpeg': + case 'jpg': return 'image/jpeg'; + case 'gif': return 'image/gif'; + case 'png': return 'image/png'; + } } } \ No newline at end of file diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 315af70..21a3c55 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -157,21 +157,11 @@ public function crop($width, $height) { } /** - * Output to the browser. - * - * @return Binary image data + * Get the image data + * + * @return string Image data */ - public function show() { - - // If headers already sent, abort - if (headers_sent()) return; - - // Set the header for the filesize and a bunch of other stuff - header("Content-Transfer-Encoding: binary"); - header("Accept-Ranges: bytes"); - // header("Content-Length: ".filesize($this->path)); - - // Display it - $this->thumb->show(); + public function get() { + return $this->thumb->getImageAsString(); } } \ No newline at end of file diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index 5bc50c5..1133e59 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -78,7 +78,30 @@ public function cropsAreRemote() { if (!method_exists($this->crops_disk, 'getAdapter')) return true; // Check if the crop disk is not local - return !is_a($this->crops_disk, 'League\Flysystem\Adapter\Local'); + return !is_a($this->crops_disk->getAdapter(), 'League\Flysystem\Adapter\Local'); + } + + /** + * Check if a remote crop exists + * + * @param string $path + * @return boolean + */ + public function cropExists($path) { + return $this->crops_disk->has($path); + } + + /** + * Get the URL to a remote crop + * + * @param string $path + * @throws Exception + * @return string + */ + public function cropUrl($path) { + if (empty($this->config['url_prefix'])) { + throw new Exception('Croppa: You must set a `url_prefix` with remote crop disks.'); + } return $this->config['url_prefix'].$path; } /** @@ -88,7 +111,7 @@ public function cropsAreRemote() { * @throws Symfony\Component\HttpKernel\Exception\NotFoundHttpException * @return string */ - public function getSrc($path) { + public function readSrc($path) { if ($this->src_disk->has($path)) return $this->src_disk->read($path); else throw new NotFoundHttpException('Croppa: Referenced file missing'); } @@ -110,7 +133,18 @@ public function makeDisk($dir) { // Instantiate a new Flysystem instance for local dirs return new Filesystem(new Adapter($dir)); - } + /** + * Write the cropped image contents to disk + * + * @param string $path Where to save the crop + * @param string $contents The image data + * @param string Return the abolute path to the image OR its redirect URL + */ + public function writeCrop($path, $contents) { + $this->crops_disk->write($path, $contents); + if ($this->cropsAreRemote()) return $this->cropUrl($path); + else return $this->config['crops_dir'].'/'.$path; + } } \ No newline at end of file diff --git a/src/Bkwld/Croppa/URL.php b/src/Bkwld/Croppa/URL.php index 2a906ae..f9a40ab 100644 --- a/src/Bkwld/Croppa/URL.php +++ b/src/Bkwld/Croppa/URL.php @@ -69,10 +69,10 @@ public function routePattern() { public function parse($request) { if (!preg_match('#'.$this->pattern().'#', $request, $matches)) return false; return [ - $this->parseRelativePath($matches[1].'.'.$matches[5]), // Path - $matches[2] == '_' ? null : (int) $matches[2], // Width - $matches[3] == '_' ? null : (int) $matches[3], // Height - $this->parseOptions($matches[4]), // Options + $this->relativePath($matches[1].'.'.$matches[5]), // Path + $matches[2] == '_' ? null : (int) $matches[2], // Width + $matches[3] == '_' ? null : (int) $matches[3], // Height + $this->options($matches[4]), // Options ]; } @@ -83,7 +83,7 @@ public function parse($request) { * @param string $path * @return string */ - protected function parseRelativePath($path) { + public function relativePath($path) { preg_match('#'.$this->config['path'].'#', $path, $matches); return $matches[1]; } @@ -95,7 +95,7 @@ protected function parseRelativePath($path) { * @param string $option_params Options string in the Croppa URL style * @return array */ - protected function parseOptions($option_params) { + public function options($option_params) { $options = array(); // These will look like: "-quadrant(T)-resize" diff --git a/src/config/config.php b/src/config/config.php index b52dd1d..9c2a3b2 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -33,6 +33,13 @@ */ 'path' => 'uploads/(.*)$', + /** + * A string that is prepended to the path captured by the `path` pattern + * (above) that is used to from the URL to remote crops. Only relevant if your + * `crops_dir` is an IoC binding to a non-local Flysystem instance. + */ + // 'url_prefix' => 'https://your-bucket.s3.amazonaws.com/uploads/', + /** * A regex pattern that works like `path` except it is only used by the * `Croppa::url($url)` generator function. If the $path url matches, it is From 65906e35e83b32914fde7ad432b0db0b212c2faa Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Mon, 6 Apr 2015 17:30:17 -0700 Subject: [PATCH 18/39] Re-organizinbg config file --- src/config/config.php | 64 +++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/src/config/config.php b/src/config/config.php index 9c2a3b2..9498732 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -1,5 +1,11 @@ public_path().'/uploads', + /** + * Maximum number of sizes to allow for a particular source file. This is to + * limit scripts from filling up your hard drive with images. Set to false or + * comment out to have no limit. + * + * @var integer | boolean + */ + 'max_crops' => App::isLocal() ? false : 12, + + /* + |-------------------------------------------------------------------------- + | URL parsing and generation + |-------------------------------------------------------------------------- + */ + /** * A regex pattern that compares against the Request path (`Request::path()`) * to find the image path to the image relative to the crops_dir. This path @@ -33,13 +54,6 @@ */ 'path' => 'uploads/(.*)$', - /** - * A string that is prepended to the path captured by the `path` pattern - * (above) that is used to from the URL to remote crops. Only relevant if your - * `crops_dir` is an IoC binding to a non-local Flysystem instance. - */ - // 'url_prefix' => 'https://your-bucket.s3.amazonaws.com/uploads/', - /** * A regex pattern that works like `path` except it is only used by the * `Croppa::url($url)` generator function. If the $path url matches, it is @@ -50,15 +64,29 @@ * @var string */ 'passthru' => '\.(?:gif|GIF)$', - + /** - * Maximum number of sizes to allow for a particular source file. This is to - * limit scripts from filling up your hard drive with images. Set to false or - * comment out to have no limit. + * A string that is prepended to the path captured by the `path` pattern + * (above) that is used to from the URL to remote crops. Only relevant if your + * `crops_dir` is an IoC binding to a non-local Flysystem instance. + */ + // 'url_prefix' => 'https://your-bucket.s3.amazonaws.com/uploads/', + + /** + * Specify the host for Croppa::url() to use when generating URLs. An + * altenative to the default is to use the app.url setting: + * + * preg_replace('#https?:#', '', Config::get('app.url')) * - * @var integer | boolean + * @var string */ - 'max_crops' => App::isLocal() ? false : 12, + 'host' => '//'.Request::getHttpHost(), + + /* + |-------------------------------------------------------------------------- + | Image settings + |-------------------------------------------------------------------------- + */ /** * The jpeg quality of generated images. The difference between 100 and 95 @@ -76,15 +104,5 @@ * @var boolean */ 'interlace' => true, - - /** - * Specify the host for Croppa::url() to use when generating URLs. An - * altenative to the default is to use the app.url setting: - * - * preg_replace('#https?:#', '', Config::get('app.url')) - * - * @var string - */ - 'host' => '//'.Request::getHttpHost(), ); \ No newline at end of file From 836790f2377a7a1d57f53f583960f0ca5efac83b Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 09:58:18 -0700 Subject: [PATCH 19/39] Adding check for too may crops and upating test --- src/Bkwld/Croppa/Handler.php | 3 +- src/Bkwld/Croppa/Storage.php | 58 ++++++++++++++++++++++++++++++-- tests/TestTooManyCrops.php | 64 +++++++++++++++++++----------------- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index 935124f..94a88dd 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -34,6 +34,7 @@ public function __construct(URL $url, Storage $storage) { * Handles a Croppa style route * * @param string $request The `Request::path()` + * @throws Exception * @return Symfony\Component\HttpFoundation\StreamedResponse */ public function handle($request) { @@ -53,7 +54,7 @@ public function handle($request) { list($path, $width, $height, $options) = $params; // Check if there are too many crops already - // if ($this->storage->tooManyCrops()) throw new Exception('Croppa: Max crops reached'); + if ($this->storage->tooManyCrops($path)) throw new Exception('Croppa: Max crops'); // Increase memory limit, cause some images require a lot to resize ini_set('memory_limit', '128M'); diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index 1133e59..52be91b 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -11,7 +11,7 @@ class Storage { /** - * @var Illuminate\Foundation\Application + * @var Illuminate\Container\Container */ private $app; @@ -58,11 +58,29 @@ static public function make($app, $config) { * @return $this */ public function mount() { - $this->src_disk = $this->makeDisk($this->config['src_dir']); - $this->crops_disk = $this->makeDisk($this->config['crops_dir']); + $this->setSrcDisk($this->makeDisk($this->config['src_dir'])); + $this->setCropsDisk($this->makeDisk($this->config['crops_dir'])); return $this; } + /** + * Set the crops disk + * + * @param League\Flysystem\Filesystem | League\Flysystem\Cached\CachedAdapter + */ + public function setCropsDisk($disk) { + $this->crops_disk = $disk; + } + + /** + * Set the src disk + * + * @param League\Flysystem\Filesystem | League\Flysystem\Cached\CachedAdapter + */ + public function setSrcDisk($disk) { + $this->src_disk = $disk; + } + /** * Return whether crops are stored remotely * @@ -147,4 +165,38 @@ public function writeCrop($path, $contents) { if ($this->cropsAreRemote()) return $this->cropUrl($path); else return $this->config['crops_dir'].'/'.$path; } + + /** + * Count up the number of crops that have already been created + * and return true if they are at the max number. + * + * @param string $path Path to the src image + * @return boolean + */ + public function tooManyCrops($path) { + if (empty($this->config['max_crops'])) return false; + return count($this->listCrops($path)) >= $this->config['max_crops']; + } + + /** + * Find all the crops that have been generated for a src path + * + * @param string $path + * @return array + */ + public function listCrops($path) { + $src = basename($path); + + // Loop through the contents of the crops directory for the path + return array_filter($this->crops_disk->listContents(dirname($path)), + function($file) use ($src) { + + // Don't match the src file + return $file['basename'] != $src + + // Check if the file begins with non-ext filename + && strpos($file['basename'], pathinfo($src, PATHINFO_FILENAME)) === 0; + }); + } + } \ No newline at end of file diff --git a/tests/TestTooManyCrops.php b/tests/TestTooManyCrops.php index b557d69..cbbbb1b 100644 --- a/tests/TestTooManyCrops.php +++ b/tests/TestTooManyCrops.php @@ -1,45 +1,49 @@ root = vfsStream::setup('dir', null, array( - 'me.jpg' => $image, - 'me-200x100.jpg' => $image, - 'me-200x200.jpg' => $image, - 'me-200x300.jpg' => $image, - )); - + // Mock the IoC container + $this->app = Mockery::mock('Illuminate\Container\Container') + ->shouldReceive('bound') + ->andReturn(false) + ->getMock(); + + // Mock flysystem + $this->dir = Mockery::mock('League\Flysystem\Filesystem') + ->shouldReceive('listContents') + ->withAnyArgs() + ->andReturn([ + ['basename' => 'me.jpg'], + ['basename' => 'me-200x100.jpg'], + ['basename' => 'me-200x200.jpg'], + ['basename' => 'me-200x300.jpg'], + ['basename' => 'unrelated.jpg'], + ]) + ->getMock(); + } public function testAcceptableNumber() { - $croppa = new Croppa(array( - 'public' => vfsStream::url('dir'), - 'src_dirs' => array(vfsStream::url('dir')), - 'max_crops' => 3, - )); - $this->assertFalse($croppa->tooManyCrops(vfsStream::url('dir/me.jpg'))); + $storage = new Storage($this->app, [ + 'max_crops' => 4, + ]); + $storage->setCropsDisk($this->dir); + $this->assertFalse($storage->tooManyCrops('me.jpg')); } public function testTooMany() { - $croppa = new Croppa(array( - 'public' => vfsStream::url('dir'), - 'src_dirs' => array(vfsStream::url('dir')), - 'max_crops' => 2, - )); - $this->assertTrue($croppa->tooManyCrops(vfsStream::url('dir/me.jpg'))); + $storage = new Storage($this->app, [ + 'max_crops' => 3, + ]); + $storage->setCropsDisk($this->dir); + $this->assertTrue($storage->tooManyCrops('me.jpg')); } } \ No newline at end of file From fb7021e009f51f322206ab8c8d3a48e7f49b8062 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 10:32:50 -0700 Subject: [PATCH 20/39] listCrops returns a simple array of paths now --- composer.json | 1 - src/Bkwld/Croppa/Storage.php | 17 +++++++++++------ tests/TestTooManyCrops.php | 21 +++++++++++++++------ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 69dc686..3afda74 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,6 @@ }, "require-dev": { "phpunit/phpunit": "3.7.*", - "mikey179/vfsStream": "~1.3", "mockery/mockery": "~0.9" }, "autoload": { diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index 52be91b..614de0d 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -187,16 +187,21 @@ public function tooManyCrops($path) { public function listCrops($path) { $src = basename($path); - // Loop through the contents of the crops directory for the path - return array_filter($this->crops_disk->listContents(dirname($path)), + // Map the filtered list to get just the paths + return array_map(function($file) { + return $file['path']; + + // Filter the list of files in the dir to find crops. Using array_values + // to reset the indexes to be 0 based, mostly for unit testing. + }, array_values(array_filter($this->crops_disk->listContents(dirname($path)), function($file) use ($src) { // Don't match the src file return $file['basename'] != $src - // Check if the file begins with non-ext filename - && strpos($file['basename'], pathinfo($src, PATHINFO_FILENAME)) === 0; - }); - } + // Check if the file begins with non-ext filename + && strpos($file['basename'], pathinfo($src, PATHINFO_FILENAME)) === 0; + }))); + } } \ No newline at end of file diff --git a/tests/TestTooManyCrops.php b/tests/TestTooManyCrops.php index cbbbb1b..6e37666 100644 --- a/tests/TestTooManyCrops.php +++ b/tests/TestTooManyCrops.php @@ -1,7 +1,6 @@ shouldReceive('listContents') ->withAnyArgs() ->andReturn([ - ['basename' => 'me.jpg'], - ['basename' => 'me-200x100.jpg'], - ['basename' => 'me-200x200.jpg'], - ['basename' => 'me-200x300.jpg'], - ['basename' => 'unrelated.jpg'], + ['path' => 'me.jpg', 'basename' => 'me.jpg'], + ['path' => 'me-200x100.jpg', 'basename' => 'me-200x100.jpg'], + ['path' => 'me-200x200.jpg', 'basename' => 'me-200x200.jpg'], + ['path' => 'me-200x300.jpg', 'basename' => 'me-200x300.jpg'], + ['path' => 'unrelated.jpg', 'basename' => 'unrelated.jpg'], ]) ->getMock(); } + public function testListCrops() { + $storage = new Storage($this->app, []); + $storage->setCropsDisk($this->dir); + $this->assertEquals([ + 'me-200x100.jpg', + 'me-200x200.jpg', + 'me-200x300.jpg', + ], $storage->listCrops('me.jpg')); + } + public function testAcceptableNumber() { $storage = new Storage($this->app, [ 'max_crops' => 4, From 5a10bd7f9759f00874a4f26dc07bdef3ed07c135 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 10:46:24 -0700 Subject: [PATCH 21/39] These tests are no longer needed The new config API is much simpler, this is no longer needed --- tests/TestCheckForFile.php | 64 -------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 tests/TestCheckForFile.php diff --git a/tests/TestCheckForFile.php b/tests/TestCheckForFile.php deleted file mode 100644 index d4a56f6..0000000 --- a/tests/TestCheckForFile.php +++ /dev/null @@ -1,64 +0,0 @@ -root = vfsStream::setup('dir', null, array( - 'uploads' => array( - '00' => array( - 'me-200x100.jpg' => $image, - ) - ) - )); - - // Share a croppa instance - $this->croppa = new Croppa(array( - 'public' => vfsStream::url('dir'), - 'src_dirs' => array(vfsStream::url('dir/uploads')) - )); - } - - public function testIfExistsByURL() { - $this->assertEquals( - vfsStream::url('dir/uploads/00/me-200x100.jpg'), - $this->croppa->checkForFile('/uploads/00/me-200x100.jpg') - ); - } - - public function testIfDoesntExistsByURL() { - $this->assertFalse($this->croppa->checkForFile('/uploads/00/me-200x200.jpg')); - } - - public function testIfExistsByPath() { - $this->assertEquals( - vfsStream::url('dir/uploads/00/me-200x100.jpg'), - $this->croppa->checkForFileByPath('00/me-200x100.jpg') - ); - } - - public function testIfDoesntExistsByPath() { - $this->assertFalse($this->croppa->checkForFileByPath('00/me-200x200.jpg')); - } - - public function testIfNoLeadingSlash() { - $this->assertEquals( - vfsStream::url('dir/uploads/00/me-200x100.jpg'), - $this->croppa->checkForFile('uploads/00/me-200x100.jpg') - ); - } - -} \ No newline at end of file From 1027a2ed9f29674c2fa0096e35c89be951e1beea Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 10:46:39 -0700 Subject: [PATCH 22/39] Adding delete logic and porting tests --- src/Bkwld/Croppa/Storage.php | 20 +++++++++++ tests/TestDelete.php | 65 +++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index 614de0d..2d1cb26 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -166,6 +166,26 @@ public function writeCrop($path, $contents) { else return $this->config['crops_dir'].'/'.$path; } + /** + * Delete src image + * + * @param string $path Path to src image + */ + public function deleteSrc($path) { + $this->src_disk->delete($path); + } + + /** + * Delete crops + * + * @param string $path Path to src image + */ + public function deleteCrops($path) { + foreach($this->listCrops($path) as $crop) { + $this->crops_disk->delete($crop); + } + } + /** * Count up the number of crops that have already been created * and return true if they are at the max number. diff --git a/tests/TestDelete.php b/tests/TestDelete.php index d3d71a2..0a6a52a 100644 --- a/tests/TestDelete.php +++ b/tests/TestDelete.php @@ -1,11 +1,68 @@ app = Mockery::mock('Illuminate\Container\Container') + ->shouldReceive('bound') + ->andReturn(false) + ->getMock(); + + } + + public function testDeleteSrc() { + + $disk = Mockery::mock('League\Flysystem\Filesystem') + ->shouldReceive('listContents') + ->withAnyArgs() + ->andReturn([ + ['path' => 'me.jpg', 'basename' => 'me.jpg'], + ['path' => 'me-200x100.jpg', 'basename' => 'me-200x100.jpg'], + ['path' => 'me-200x200.jpg', 'basename' => 'me-200x200.jpg'], + ['path' => 'me-200x300.jpg', 'basename' => 'me-200x300.jpg'], + ['path' => 'unrelated.jpg', 'basename' => 'unrelated.jpg'], + ]) + ->shouldReceive('delete') + ->withAnyArgs() + ->once() + ->getMock(); + + $storage = new Storage($this->app, []); + $storage->setSrcDisk($disk); + $this->assertNull($storage->deleteSrc('me.jpg')); + } + + public function testDeleteCrops() { + + $disk = Mockery::mock('League\Flysystem\Filesystem') + ->shouldReceive('listContents') + ->withAnyArgs() + ->andReturn([ + ['path' => 'me.jpg', 'basename' => 'me.jpg'], + ['path' => 'me-200x100.jpg', 'basename' => 'me-200x100.jpg'], + ['path' => 'me-200x200.jpg', 'basename' => 'me-200x200.jpg'], + ['path' => 'me-200x300.jpg', 'basename' => 'me-200x300.jpg'], + ['path' => 'unrelated.jpg', 'basename' => 'unrelated.jpg'], + ]) + ->shouldReceive('delete') + ->withAnyArgs() + ->times(3) + ->getMock(); + + $storage = new Storage($this->app, []); + $storage->setCropsDisk($disk); + $this->assertNull($storage->deleteCrops('me.jpg')); + + } + + + /* private $root; private $croppa; public function setUp() { @@ -68,5 +125,5 @@ public function testFilenameWithDimensions() { $this->croppa->findFilesToDelete('/uploads/00/me-200x100.jpg') ); } - + */ } \ No newline at end of file From 90589e409f640c36fce9ab88f0c51fb630f9aa36 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 10:56:57 -0700 Subject: [PATCH 23/39] Simplifying tests --- src/Bkwld/Croppa/Image.php | 2 +- src/Bkwld/Croppa/Storage.php | 2 +- tests/TestDelete.php | 16 ++-------------- tests/TestTooManyCrops.php | 17 +++-------------- 4 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 21a3c55..7475262 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -19,7 +19,7 @@ class Image { * @param string $data Image data as a string * @param array $options */ - public function __construct($data, $config) { + public function __construct($data, $config = []) { $this->thumb = PhpThumbFactory::create($data, $config, true); } diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index 2d1cb26..bd15769 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -36,7 +36,7 @@ class Storage { * @param Illuminate\Container\Container * @param array $config */ - public function __construct($app, $config) { + public function __construct($app = null, $config = null) { $this->app = $app; $this->config = $config; } diff --git a/tests/TestDelete.php b/tests/TestDelete.php index 0a6a52a..bb24b6b 100644 --- a/tests/TestDelete.php +++ b/tests/TestDelete.php @@ -4,18 +4,6 @@ class TestDelete extends PHPUnit_Framework_TestCase { - private $app; - - public function setUp() { - - // Mock the IoC container - $this->app = Mockery::mock('Illuminate\Container\Container') - ->shouldReceive('bound') - ->andReturn(false) - ->getMock(); - - } - public function testDeleteSrc() { $disk = Mockery::mock('League\Flysystem\Filesystem') @@ -33,7 +21,7 @@ public function testDeleteSrc() { ->once() ->getMock(); - $storage = new Storage($this->app, []); + $storage = new Storage(); $storage->setSrcDisk($disk); $this->assertNull($storage->deleteSrc('me.jpg')); } @@ -55,7 +43,7 @@ public function testDeleteCrops() { ->times(3) ->getMock(); - $storage = new Storage($this->app, []); + $storage = new Storage(); $storage->setCropsDisk($disk); $this->assertNull($storage->deleteCrops('me.jpg')); diff --git a/tests/TestTooManyCrops.php b/tests/TestTooManyCrops.php index 6e37666..ec803e8 100644 --- a/tests/TestTooManyCrops.php +++ b/tests/TestTooManyCrops.php @@ -4,17 +4,10 @@ class TestTooManyCrops extends PHPUnit_Framework_TestCase { - private $app; private $dir; public function setUp() { - // Mock the IoC container - $this->app = Mockery::mock('Illuminate\Container\Container') - ->shouldReceive('bound') - ->andReturn(false) - ->getMock(); - // Mock flysystem $this->dir = Mockery::mock('League\Flysystem\Filesystem') ->shouldReceive('listContents') @@ -31,7 +24,7 @@ public function setUp() { } public function testListCrops() { - $storage = new Storage($this->app, []); + $storage = new Storage(); $storage->setCropsDisk($this->dir); $this->assertEquals([ 'me-200x100.jpg', @@ -41,17 +34,13 @@ public function testListCrops() { } public function testAcceptableNumber() { - $storage = new Storage($this->app, [ - 'max_crops' => 4, - ]); + $storage = new Storage(null, [ 'max_crops' => 4, ]); $storage->setCropsDisk($this->dir); $this->assertFalse($storage->tooManyCrops('me.jpg')); } public function testTooMany() { - $storage = new Storage($this->app, [ - 'max_crops' => 3, - ]); + $storage = new Storage(null, [ 'max_crops' => 3, ]); $storage->setCropsDisk($this->dir); $this->assertTrue($storage->tooManyCrops('me.jpg')); } From 1557e903da69332f089cd54234b1782ca2d22ede Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 11:06:08 -0700 Subject: [PATCH 24/39] Re-implementing reszing tests --- src/Bkwld/Croppa/Image.php | 2 +- tests/TestGeneration.php | 71 -------------------------------------- tests/TestResizing.php | 54 +++++++++++++++++++++++++++++ tests/TestUrlParsing.php | 6 ++++ 4 files changed, 61 insertions(+), 72 deletions(-) delete mode 100644 tests/TestGeneration.php create mode 100644 tests/TestResizing.php diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 7475262..dba737d 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -101,7 +101,7 @@ public function trimPerc($coords) { */ public function resizeAndOrCrop($width, $height, $options) { if (isset($options['quadrant'])) return $this->cropQuadrant($width, $height, $options); - if (isset($options['resize']) || !$width || !$height) return $this->resize($width, $height); + if (array_key_exists('resize', $options) || !$width || !$height) return $this->resize($width, $height); return $this->crop($width, $height); } diff --git a/tests/TestGeneration.php b/tests/TestGeneration.php deleted file mode 100644 index 3190f48..0000000 --- a/tests/TestGeneration.php +++ /dev/null @@ -1,71 +0,0 @@ -src = ob_get_clean(); - - // Make virtual fileystem - $this->root = vfsStream::setup('dir', null, array( - 'uploads' => array( - '00' => array( - 'file.jpg' => $this->src, - ) - ) - )); - - // Share a croppa instance - $this->croppa = new Croppa(array( - 'public' => vfsStream::url('dir'), - 'src_dirs' => array(vfsStream::url('dir/uploads')), - )); - } - - public function testPasthru() { - $this->croppa->generate('/uploads/00/file-_x_.jpg'); - $size = getimagesize(vfsStream::url('dir/uploads/00/file-_x_.jpg')); - $this->assertEquals('500x400', $size[0].'x'.$size[1]); - } - - public function testWidthConstraint() { - $this->croppa->generate('/uploads/00/file-200x_.jpg'); - $size = getimagesize(vfsStream::url('dir/uploads/00/file-200x_.jpg')); - $this->assertEquals('200x160', $size[0].'x'.$size[1]); - } - - public function testHeightConstraint() { - $this->croppa->generate('/uploads/00/file-_x200.jpg'); - $size = getimagesize(vfsStream::url('dir/uploads/00/file-_x200.jpg')); - $this->assertEquals('250x200', $size[0].'x'.$size[1]); - } - - public function testWidthAndHeightConstraint() { - $this->croppa->generate('/uploads/00/file-200x100.jpg'); - $size = getimagesize(vfsStream::url('dir/uploads/00/file-200x100.jpg')); - $this->assertEquals('200x100', $size[0].'x'.$size[1]); - } - - public function testWidthAndHeightResize() { - $this->croppa->generate('/uploads/00/file-200x200-resize.jpg'); - $size = getimagesize(vfsStream::url('dir/uploads/00/file-200x200-resize.jpg')); - $this->assertEquals('200x160', $size[0].'x'.$size[1]); - } - - public function testWidthAndHeightTrim() { - $this->croppa->generate('/uploads/00/file-200x200-trim_perc(0.25,0.25,0.75,0.75).jpg'); - $size = getimagesize(vfsStream::url('dir/uploads/00/file-200x200-trim_perc(0.25,0.25,0.75,0.75).jpg')); - $this->assertEquals('200x200', $size[0].'x'.$size[1]); - } - -} \ No newline at end of file diff --git a/tests/TestResizing.php b/tests/TestResizing.php new file mode 100644 index 0000000..b47b4cc --- /dev/null +++ b/tests/TestResizing.php @@ -0,0 +1,54 @@ +src = ob_get_clean(); + } + + public function testPasthru() { + $image = new Image($this->src); + $size = getimagesizefromstring($image->process(null, null)->get()); + $this->assertEquals('500x400', $size[0].'x'.$size[1]); + } + + public function testWidthConstraint() { + $image = new Image($this->src); + $size = getimagesizefromstring($image->process(200, null)->get()); + $this->assertEquals('200x160', $size[0].'x'.$size[1]); + } + + public function testHeightConstraint() { + $image = new Image($this->src); + $size = getimagesizefromstring($image->process(null, 200)->get()); + $this->assertEquals('250x200', $size[0].'x'.$size[1]); + } + + public function testWidthAndHeightConstraint() { + $image = new Image($this->src); + $size = getimagesizefromstring($image->process(200, 100)->get()); + $this->assertEquals('200x100', $size[0].'x'.$size[1]); + } + + public function testWidthAndHeightResize() { + $image = new Image($this->src); + $size = getimagesizefromstring($image->process(200, 200, ['resize' => null])->get()); + $this->assertEquals('200x160', $size[0].'x'.$size[1]); + } + + public function testWidthAndHeightTrim() { + $image = new Image($this->src); + $size = getimagesizefromstring($image->process(200, 200, ['trim_perc' => [0.25,0.25,0.75,0.75]])->get()); + $this->assertEquals('200x200', $size[0].'x'.$size[1]); + } + +} \ No newline at end of file diff --git a/tests/TestUrlParsing.php b/tests/TestUrlParsing.php index e87e3d5..ea4aa70 100644 --- a/tests/TestUrlParsing.php +++ b/tests/TestUrlParsing.php @@ -47,4 +47,10 @@ public function testWidthAndHeightAndOptionsWithValue() { ], $this->url->parse('uploads/1/2/file-200x100-quadrant(T).jpg')); } + public function testWidthAndHeightAndOptionsWithValueList() { + $this->assertEquals([ + '1/2/file.jpg', 200, 100, ['trim_perc' => [0.25,0.25,0.75,0.75]] + ], $this->url->parse('uploads/1/2/file-200x100-trim_perc(0.25,0.25,0.75,0.75).jpg')); + } + } \ No newline at end of file From 0d971d8ab8517701b1440a9c459388922ada03a6 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 13:16:13 -0700 Subject: [PATCH 25/39] Adding URL generator and updating tests --- src/Bkwld/Croppa/URL.php | 57 +++++++++++++++++++++++++++++++++++++- src/config/config.php | 4 +-- tests/TestUrlGenerator.php | 42 ++++++++++++++++------------ 3 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/Bkwld/Croppa/URL.php b/src/Bkwld/Croppa/URL.php index f9a40ab..d08d718 100644 --- a/src/Bkwld/Croppa/URL.php +++ b/src/Bkwld/Croppa/URL.php @@ -15,7 +15,7 @@ class URL { * * @param array $config */ - public function __construct($config) { + public function __construct($config = []) { $this->config = $config; } @@ -43,6 +43,61 @@ public function pattern() { return $pattern; } + /** + * Insert Croppa parameter suffixes into a URL. For use as a helper in views + * when rendering image src attributes. + * + * @param string $url URL of an image that should be cropped + * @param integer $width Target width + * @param integer $height Target height + * @param array $options Addtional Croppa options, passed as key/value pairs. Like array('resize') + * @return string The new path to your thumbnail + */ + public function generate($url, $width = null, $height = null, $options = null) { + + // Extract the path from a URL if a URL was provided instead of a path. It + // will have a leading slash. + $path = parse_url($url, PHP_URL_PATH); + + // Skip croppa requests for images the ignore regexp + if (isset($this->config['ignore']) + && preg_match('#'.$this->config['ignore'].'#', $path)) { + return $this->addHost($path); + } + + // Defaults + if (empty($path)) return; // Don't allow empty strings + if (!$width && !$height) return $this->addHost($path); // Pass through if empty + $width = $width ? round($width) : '_'; + $height = $height ? round($height) : '_'; + + // Produce width, height, and options + $suffix = '-'.$width.'x'.$height; + if ($options && is_array($options)) { + foreach($options as $key => $val) { + if (is_numeric($key)) $suffix .= '-'.$val; + elseif (is_array($val)) $suffix .= '-'.$key.'('.implode(',',$val).')'; + else $suffix .= '-'.$key.'('.$val.')'; + } + } + + // Assemble the new path and return + $parts = pathinfo($path); + $path = '/'.trim($parts['dirname'],'/').'/'.$parts['filename'].$suffix; + if (isset($parts['extension'])) $path .= '.'.$parts['extension']; + return $this->addHost($path); + } + + /** + * Append host to the path if it was defined + * + * @param string $path URL path (with leading slash) + * @return string + */ + public function addHost($path) { + return empty($this->config['host']) ? $path : $this->config['host'].$path; + } + /** * Make the regex for the route definition. This works by wrapping both the * basic Croppa pattern and the `path` config in positive regex lookaheads so diff --git a/src/config/config.php b/src/config/config.php index 9498732..79d88d6 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -63,7 +63,7 @@ * * @var string */ - 'passthru' => '\.(?:gif|GIF)$', + 'ignore' => '\.(gif|GIF)$', /** * A string that is prepended to the path captured by the `path` pattern @@ -80,7 +80,7 @@ * * @var string */ - 'host' => '//'.Request::getHttpHost(), + // 'host' => '//'.Request::getHttpHost(), /* |-------------------------------------------------------------------------- diff --git a/tests/TestUrlGenerator.php b/tests/TestUrlGenerator.php index 6c8bbcb..5566064 100644 --- a/tests/TestUrlGenerator.php +++ b/tests/TestUrlGenerator.php @@ -1,41 +1,49 @@ assertEquals('/path/file-200x100.png', $croppa->url('/path/file.png', 200, 100)); + $url = new URL(); + $this->assertEquals('/path/file-200x100.png', $url->generate('/path/file.png', 200, 100)); } public function testIgnore() { - $croppa = new Croppa(array( - 'ignore' => '.+\.gif$', - )); - $this->assertEquals('/path/file.gif', $croppa->url('/path/file.gif', 200, 100)); - $this->assertEquals('/path/file-200x100.png', $croppa->url('/path/file.png', 200, 100)); + $url = new URL([ 'ignore' => '\.(?:gif|GIF)$' ]); + $this->assertEquals('/path/file.gif', $url->generate('/path/file.gif', 200, 100)); + $this->assertEquals('/path/file-200x100.png', $url->generate('/path/file.png', 200, 100)); } public function testNoWidthOrHeight() { - $croppa = new Croppa(); - $this->assertEquals('/path/file.png', $croppa->url('/path/file.png')); + $url = new URL(); + $this->assertEquals('/path/file.png', $url->generate('/path/file.png')); } public function testNoWidth() { - $croppa = new Croppa(); - $this->assertEquals('/path/file-_x100.png', $croppa->url('/path/file.png', null, 100)); + $url = new URL(); + $this->assertEquals('/path/file-_x100.png', $url->generate('/path/file.png', null, 100)); } public function testNoHeight() { - $croppa = new Croppa(); - $this->assertEquals('/path/file-200x_.png', $croppa->url('/path/file.png', 200)); + $url = new URL(); + $this->assertEquals('/path/file-200x_.png', $url->generate('/path/file.png', 200)); } public function testHostInSrc() { - $croppa = new Croppa(); - $this->assertEquals('/path/file-200x_.png', $croppa->url('http://domain.tld/path/file.png', 200)); - $this->assertEquals('/path/file-200x_.png', $croppa->url('https://domain.tld/path/file.png', 200)); + $url = new URL(); + $this->assertEquals('/path/file-200x_.png', $url->generate('http://domain.tld/path/file.png', 200)); + $this->assertEquals('/path/file-200x_.png', $url->generate('https://domain.tld/path/file.png', 200)); + } + + public function testHostConfig() { + $url = new URL([ 'host' => '//domain.tld', ]); + $this->assertEquals('//domain.tld/path/file-200x_.png', $url->generate('/path/file.png', 200)); + } + + public function testHostAndSchemaConfig() { + $url = new URL([ 'host' => 'https://domain.tld', ]); + $this->assertEquals('https://domain.tld/path/file-200x_.png', $url->generate('http://domain.tld/path/file.png', 200)); } } \ No newline at end of file From 8e89bc9104778fdb3aec0edf6a2c07a76c201da8 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 13:20:37 -0700 Subject: [PATCH 26/39] Fixing image tests --- src/Bkwld/Croppa/Image.php | 3 ++- tests/TestResizing.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index dba737d..5dc3465 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -31,7 +31,7 @@ public function __construct($data, $config = []) { * @param array $options * @return $this */ - public function process($width, $height, $options) { + public function process($width = null, $height = null, $options = []) { return $this ->autoRotate() ->trim($options) @@ -100,6 +100,7 @@ public function trimPerc($coords) { * @return $this */ public function resizeAndOrCrop($width, $height, $options) { + if (!$width && !$height) return $this; if (isset($options['quadrant'])) return $this->cropQuadrant($width, $height, $options); if (array_key_exists('resize', $options) || !$width || !$height) return $this->resize($width, $height); return $this->crop($width, $height); diff --git a/tests/TestResizing.php b/tests/TestResizing.php index b47b4cc..b579220 100644 --- a/tests/TestResizing.php +++ b/tests/TestResizing.php @@ -15,7 +15,7 @@ public function setUp() { $this->src = ob_get_clean(); } - public function testPasthru() { + public function testPassthru() { $image = new Image($this->src); $size = getimagesizefromstring($image->process(null, null)->get()); $this->assertEquals('500x400', $size[0].'x'.$size[1]); From c0e26fbf8713671349e73474471906e98c86e55c Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 13:41:53 -0700 Subject: [PATCH 27/39] Reordering code --- src/Bkwld/Croppa/Storage.php | 60 ++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index bd15769..8f8dab6 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -52,17 +52,6 @@ static public function make($app, $config) { return with(new static($app, $config))->mount(); } - /** - * "Mount" disks give the config - * - * @return $this - */ - public function mount() { - $this->setSrcDisk($this->makeDisk($this->config['src_dir'])); - $this->setCropsDisk($this->makeDisk($this->config['crops_dir'])); - return $this; - } - /** * Set the crops disk * @@ -81,6 +70,36 @@ public function setSrcDisk($disk) { $this->src_disk = $disk; } + /** + * "Mount" disks give the config + * + * @return $this + */ + public function mount() { + $this->setSrcDisk($this->makeDisk($this->config['src_dir'])); + $this->setCropsDisk($this->makeDisk($this->config['crops_dir'])); + return $this; + } + + /** + * Use or instantiate a Flysystem disk + * + * @param string $dir The value from one of the config dirs + * @return League\Flysystem\Filesystem | League\Flysystem\Cached\CachedAdapter + */ + public function makeDisk($dir) { + + // Check if the dir refers to an IoC binding and return it + if ($this->app->bound($dir) + && ($instance = $this->app->make($dir)) + && (is_a($instance, 'League\Flysystem\Filesystem') + || is_a($instance, 'League\Flysystem\Cached\CachedAdapter')) + ) return $instance; + + // Instantiate a new Flysystem instance for local dirs + return new Filesystem(new Adapter($dir)); + } + /** * Return whether crops are stored remotely * @@ -134,25 +153,6 @@ public function readSrc($path) { else throw new NotFoundHttpException('Croppa: Referenced file missing'); } - /** - * Use or instantiate a Flysystem disk - * - * @param string $dir The value from one of the config dirs - * @return League\Flysystem\Filesystem | League\Flysystem\Cached\CachedAdapter - */ - public function makeDisk($dir) { - - // Check if the dir refers to an IoC binding and return it - if ($this->app->bound($dir) - && ($instance = $this->app->make($dir)) - && (is_a($instance, 'League\Flysystem\Filesystem') - || is_a($instance, 'League\Flysystem\Cached\CachedAdapter')) - ) return $instance; - - // Instantiate a new Flysystem instance for local dirs - return new Filesystem(new Adapter($dir)); - } - /** * Write the cropped image contents to disk * From e276ecbf72ac851c45dd9eac35b13dd2c099590b Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 14:13:28 -0700 Subject: [PATCH 28/39] Adding API layer via facade and helpers file --- src/Bkwld/Croppa/Croppa.php | 468 --------------------------- src/Bkwld/Croppa/Facade.php | 2 +- src/Bkwld/Croppa/Helpers.php | 66 ++++ src/Bkwld/Croppa/Image.php | 2 +- src/Bkwld/Croppa/ServiceProvider.php | 12 +- 5 files changed, 74 insertions(+), 476 deletions(-) delete mode 100755 src/Bkwld/Croppa/Croppa.php create mode 100644 src/Bkwld/Croppa/Helpers.php diff --git a/src/Bkwld/Croppa/Croppa.php b/src/Bkwld/Croppa/Croppa.php deleted file mode 100755 index 0d17988..0000000 --- a/src/Bkwld/Croppa/Croppa.php +++ /dev/null @@ -1,468 +0,0 @@ -config = array_merge(array( - 'host' => null, - 'ignore' => null, - 'public' => null, - ), $config); - } - - /** - * Create a URL in the Croppa syntax given different parameters. This is a helper - * designed to be used from view files. - * - * @param string $src The path to the source - * @param integer $width Target width - * @param integer $height Target height - * @param array $options Addtional Croppa options, passed as key/value pairs. Like array('resize') - * @return string The new path to your thumbnail - */ - public function url($src, $width = null, $height = null, $options = null) { - - // Extract the path from a URL if a URL was provided instead of a path - $src = parse_url($src, PHP_URL_PATH); - - // Skip croppa requests for images the ignore regexp - if ($this->config['ignore'] && preg_match('#'.$this->config['ignore'].'#', $src)) return $this->config['host'].$src; - - // Defaults - if (empty($src)) return; // Don't allow empty strings - if (!$width && !$height) return $this->config['host'].$src; // Pass through if both width and height are empty - if (!$width) $width = '_'; - else $width = round($width); - if (!$height) $height = '_'; - else $height = round($height); - - // Produce the croppa syntax - $suffix = '-'.$width.'x'.$height; - - // Add options. If the key has no arguments (like resize), the key will be like [1] - if ($options && is_array($options)) { - foreach($options as $key => $val) { - if (is_numeric($key)) $suffix .= '-'.$val; - elseif (is_array($val)) $suffix .= '-'.$key.'('.implode(',',$val).')'; - else $suffix .= '-'.$key.'('.$val.')'; - } - } - - // Break the path apart and put back together again - $parts = pathinfo($src); - $parts['dirname'] = ltrim($parts['dirname'], '/'); - $url = $this->config['host'].'/'. $parts['dirname'].'/'.$parts['filename'].$suffix; - if (!empty($parts['extension'])) $url .= '.'.$parts['extension']; - return $url; - } - - /** - * Take the provided URL and, if it matches the Croppa URL schema, create - * the thumnail as defined in the URL schema. If no source image can be found - * the function returns false. If the URL exists, that image is outputted. If - * a thumbnail can be produced, it is, and then it is outputted to the browser. - * - * @param string $url This is actually the path, like "uploads/image.jpg" - * @return boolean - */ - public function generate($url) { - - // Check if the current url looks like a croppa URL. Btw, this is a good - // resource: http://regex101.com/. - if (!preg_match('#'.$this->pattern().'#i', $url, $matches)) return false; - $path = $matches[1].'.'.$matches[5]; - $width = $matches[2]; - $height = $matches[3]; - $options = $matches[4]; // These are not parsed, all options are grouped together raw - - // Increase memory limit, cause some images require a lot to resize - ini_set('memory_limit', '128M'); - - // Break apart options - $options = $this->makeOptions($options); - - // See if the referenced file exists and is an image. This gives us the absolute - // to the image, given the $path which is relative to a src_dir - if (!($src = $this->checkForFile($path))) throw new NotFoundHttpException('Croppa: Referenced file missing'); - - // Put the croped output in the same directory as the src image - $dst = dirname($src).'/'.basename($url); - - // Make sure destination is writeable - if (!is_writable(dirname($dst))) throw new Exception('Croppa: Destination is not writeable'); - - // Configure PHP Thumb - $phpthumb_config = array(); - if (array_key_exists('quality', $options)) $phpthumb_config['jpegQuality'] = $options['quality'][0]; - else if (!empty($this->config['jpeg_quality'])) $phpthumb_config['jpegQuality'] = $this->config['jpeg_quality']; - if (array_key_exists('interlace', $options)) $phpthumb_config['interlace'] = !empty($options['interlace'][0]); - else if (!empty($this->config['interlace'])) $phpthumb_config['interlace'] = true; - - // Create PHP Thumb and Croppa/Image instance - $thumb = PhpThumbFactory::create($src, $phpthumb_config); - $image = new Image($thumb, $dst); - - // If width and height are both wildcarded, just copy the file and be done with it - if ($width == '_' && $height == '_') { - copy($src, $dst); - return $image; - } - - // Make sure that we won't exceed the the max number of crops for this image - if ($this->tooManyCrops($src)) throw new Exception('Croppa: Max crops reached'); - - // Auto rotate the image based on exif data (like from phones) - // Uses: https://github.com/nik-kor/PHPThumb/blob/master/src/thumb_plugins/jpg_rotate.inc.php - $thumb->rotateJpg(); - - // Trim the source before applying the crop. This is designed to be used in conjunction - // with a cropping UI tool. - if (array_key_exists('trim', $options) && array_key_exists('trim_perc', $options)) throw new Exception('Specify a trim OR a trip_perc option, not both'); - else if (array_key_exists('trim', $options)) $this->trim($thumb, $options['trim']); - else if (array_key_exists('trim_perc', $options)) $this->trimPerc($thumb, $options['trim_perc']); - - // Do a quadrant adaptive resize. Supported quadrant values are: - // +---+---+---+ - // | | T | | - // +---+---+---+ - // | L | C | R | - // +---+---+---+ - // | | B | | - // +---+---+---+ - if (array_key_exists('quadrant', $options)) { - if ($height == '_' || $width == '_') throw new Exception('Croppa: Qudrant option needs width and height'); - if (empty($options['quadrant'][0])) throw new Exception('Croppa:: No quadrant specified'); - $quadrant = strtoupper($options['quadrant'][0]); - if (!in_array($quadrant, array('T','L','C','R','B'))) throw new Exception('Croppa:: Invalid quadrant'); - $thumb->adaptiveResizeQuadrant($width, $height, $quadrant); - - // Force to 'resize' - } elseif (array_key_exists('resize', $options)) { - if ($height == '_' || $width == '_') throw new Exception('Croppa: Resize option needs width and height'); - $thumb->resize($width, $height); - - // Produce a standard crop - } else { - if ($height == '_') $thumb->resize($width, 99999); // If no height, resize by width - elseif ($width == '_') $thumb->resize(99999, $height); // If no width, resize by height - else $thumb->adaptiveResize($width, $height); // There is width and height, so crop - } - - // Save it to disk - $thumb->save($dst); - - // Return Image instance - return $image; - } - - /** - * Delete all crops but keep original (call after changing original) - * - * @param $url - * @throws Exception - */ - public function reset($url) { - foreach($this->findFilesToDelete($url, false) as $file) { - if (!unlink($file)) throw new Exception('Croppa unlink failed: '.$file); - } - } - - /** - * Delete the source image and all the crops - * - * @param string $url Relative path to the original source image - * @return null - */ - public function delete($url) { - foreach($this->findFilesToDelete($url) as $file) { - if (!unlink($file)) throw new Exception('Croppa unlink failed: '.$file); - } - } - - /** - * Make an array of the files to delete given the source image - * - * @param string $url Relative path to the original source image. Generally preceeded with a '/' - * @param bool $delete_original include original image in list (needed for deleting) if true, - * omit original if false (needed for updating with new image) - * @return array List of absolute paths of images - */ - public function findFilesToDelete($url, $delete_original = true) { - $deleting = array(); - - // Need to decode the url so that we can handle things like space characters - $url = urldecode($url); - - // Add the source image to the list if deleting, don't add if resetting - if (!($src = $this->checkForFile($url))) return array(); - if ($delete_original) $deleting[] = $src; - - // Loop through the contents of the source directory and delete - // any images that contain the source directories filename and also match - // the Croppa URL pattern - $parts = pathinfo($src); - $files = scandir($parts['dirname']); - foreach($files as $file) { - $path = $parts['dirname'].'/'.$file; - if (strpos($file, $parts['filename']) === 0 // Quick check to check for src - && !in_array($path, $deleting) // Not already added (because of $delete_original) - && preg_match('#'.$this->pattern().'#', $path)) { - $deleting[] = $path; - } - } - // Return the list - return $deleting; - - } - - /** - * Return width and height values for putting in an img tag. Uses the same arguments as Croppa::url(). - * Used in cases where you are resizing an image along one dimension and don't know what the wildcarded - * image size is. They are formatted for putting in a style() attribute. This seems to have better support - * that using the old school width and height attributes for setting the initial height. - * - * @param string $src The path to the source - * @param integer $width Target width - * @param integer $height Target height - * @param array $options Addtional Croppa options, passed as key/value pairs. Like array('resize') - * @return string i.e. "width='200px' height='200px'" - */ - public function sizes($src, $width = null, $height = null, $options = null) { - - // Get the URL to the file - $url = $this->url($src, $width, $height, $options); - - // Find the local path to this file by removing the URL base and then adding the - // path to the public directory - $path = $this->config['public'].substr($url, strlen($this->config['host'])); - - // Get the sizes - if (!file_exists($path)) return null; // It may not exist if this is the first request for the img - if (!($size = getimagesize($path))) throw new Exception('Dimensions could not be read'); - return "width:{$size[0]}px; height:{$size[1]}px;"; - - } - - /** - * Create an image tag rather than just the URL. Accepts the same params as Croppa::url() - * - * @param string $src The path to the source - * @param integer $width Target width - * @param integer $height Target height - * @param array $options Addtional Croppa options, passed as key/value pairs. Like array('resize') - * @return string i.e. - */ - public function tag($src, $width = null, $height = null, $options = null) { - return ''; - } - - /** - * Return the Croppa URL regex - * - * @return string - */ - public function pattern() { - $pattern = ''; - - // Add rest of the path up to croppa's extension - $pattern .= '(.+)'; - - // Check for the size bounds - $pattern .= '-([0-9_]+)x([0-9_]+)'; - - // Check for options that may have been added - $pattern .= '(-[0-9a-zA-Z(),\-._]+)*'; - - // Check for possible image suffixes. - $pattern .= '\.(jpg|jpeg|png|gif|JPG|JPEG|PNG|GIF)$'; - - // Return it - return $pattern; - } - - /** - * Return a pattern that enforces the src_dirs - * - * @return string - */ - public function directoryPattern() { - $pattern = '^'; - - // Make leading slashes optional - $pattern .= '\/?'; - - // Make sure it starts with a src dir - $public = $this->config['public']; - $pattern .= '(?:'.implode('|', array_map(function($dir) use ($public) { - return preg_quote( // Escape unsafe chars - ltrim( // Don't allow leading slashes, the generate($path) lacks them - str_replace($public, '', $dir), - '/'), '#'); - }, $this->config['src_dirs'])).')'; - - // Return it with the file pattern - return $pattern.$this->pattern(); - } - - // ------------------------------------------------------------------ - // Generally internal methods only to follow - // ------------------------------------------------------------------ - - /** - * See if there is an existing image file that matches the request given - * a relative path to the image. - * - * @param string $path An absolute path to an image - * @return string|boolean The absolute path to the file or FALSE - */ - public function checkForFile($path) { - - // Expect there to be a leading slash - if (substr($path, 0, 1) != '/') $path = '/'.$path; - - // Strip src_dirs and leading slashes from the path - $public = $this->config['public']; - $path = ltrim(str_replace(array_map(function($dir) use ($public) { - return str_replace($public, '', $dir); - }, $this->config['src_dirs']), '', $path), '/'); - - // Check in path - return $this->checkForFileByPath($path); - } - - /** - * See if there is an existing image file that matches the request given - * a path relative to a src_dir - * - * @param string $url A path to the image relative to a src_dir - * @return string|boolean The absolute path to the file or FALSE - */ - public function checkForFileByPath($path) { - - // Loop through all the directories files may be uploaded to - $src_dirs = $this->config['src_dirs']; - foreach($src_dirs as $dir) { - - // Check that directory exists - if (!is_dir($dir)) continue; - if (substr($dir, -1, 1) != '/') $dir .= '/'; - - // Look for the image in the directory - $src = $dir.$path; - if (is_file($src) && getimagesize($src) !== false) { - return $src; - } - } - - // None found - return false; - } - - /** - * Count up the number of crops that have already been created - * and return true if they are at the max number. - * For https://github.com/BKWLD/croppa/issues/1 - * - * @param string $src Absolute path to a src image - * @return boolean - */ - public function tooManyCrops($src) { - - // If there is no max set, we are applying no limit - if (empty($this->config['max_crops'])) return false; - - // Count up the crops - $found = 0; - $parts = pathinfo($src); - $files = scandir($parts['dirname']); - foreach($files as $file) { - $path = $parts['dirname'].'/'.$file; - - // Check if this file, when stripped of Croppa suffixes, has the same name - // as the source image. - if (pathinfo(preg_replace('#'.$this->pattern().'#', "$1", $path), PATHINFO_FILENAME) == $parts['filename']) $found++; - - // We're matching against the max + 1 because the source file - // will match but doesn't count against the crop limit - if ($found > $this->config['max_crops'] + 1) return true; - } - - // There aren't too many crops, so return false - return false; - } - - /** - * Create options array where each key is an option name - * and the value if an array of the passed arguments - * - * @param string $option_params Options string in the Croppa URL style - * @return array - */ - public function makeOptions($option_params) { - $options = array(); - - // These will look like: "-quadrant(T)-resize" - $option_params = explode('-', $option_params); - - // Loop through the params and make the options key value pairs - foreach($option_params as $option) { - if (!preg_match('#(\w+)(?:\(([\w,.]+)\))?#i', $option, $matches)) continue; - if (isset($matches[2])) $options[$matches[1]] = explode(',', $matches[2]); - else $options[$matches[1]] = null; - } - - // Return new options array - return $options; - } - - /** - * Trim the source before applying the crop where the input is given as - * offset pixels - * - * @param PhpThumb $thumb - * @param array $options Cropping instructions as pixels - * @return void - */ - public function trim($thumb, $options) { - list($x1, $y1, $x2, $y2) = $options; - - // Apply crop to the thumb before resizing happens - $thumb->crop($x1, $y1, $x2 - $x1, $y2 - $y1); - } - - /** - * Trim the source before applying the crop where the input is given as - * offset percentages - * - * @param PhpThumb $thumb - * @param array $options Cropping instructions as percentages - * @return void - */ - public function trimPerc($thumb, $options) { - list($x1, $y1, $x2, $y2) = $options; - - // Get the current dimensions - $size = (object) $thumb->getCurrentDimensions(); - - // Convert percentage values to what GdThumb expects - $x = round($x1 * $size->width); - $y = round($y1 * $size->height); - $width = round($x2 * $size->width - $x); - $height = round($y2 * $size->height - $y); - - // Apply crop to the thumb before resizing happens - $thumb->crop($x, $y, $width, $height); - } - -} diff --git a/src/Bkwld/Croppa/Facade.php b/src/Bkwld/Croppa/Facade.php index f562f34..72a97f5 100644 --- a/src/Bkwld/Croppa/Facade.php +++ b/src/Bkwld/Croppa/Facade.php @@ -1,4 +1,4 @@ url = $url; + $this->storage = $storage; + } + + /** + * Delete source image and all of it's crops + * + * @see Bkwld\Croppa\Storage::deleteSrc() + */ + public function delete() { + return call_user_func_array([$this->storage, 'deleteSrc'], func_get_args()); + } + + /** + * Delete just the crops, leave the source image + * + * @see Bkwld\Croppa\Storage::deleteCrops() + */ + public function reset() { + return call_user_func_array([$this->storage, 'deleteCrops'], func_get_args()); + } + + /** + * Create an image tag rather than just the URL. Accepts the same params as url() + * + * @see Bkwld\Croppa\URL::generate() + */ + public function tag() { + return ''; + } + + /** + * Pass through URL requrests to URL->generate(). + * + * @see Bkwld\Croppa\URL::generate() + */ + public function url() { + return call_user_func_array([$this->url, 'generate'], func_get_args()); + } + +} \ No newline at end of file diff --git a/src/Bkwld/Croppa/Image.php b/src/Bkwld/Croppa/Image.php index 5dc3465..4b82119 100644 --- a/src/Bkwld/Croppa/Image.php +++ b/src/Bkwld/Croppa/Image.php @@ -4,7 +4,7 @@ use PhpThumbFactory; /** - * Wraps a croped image to provide rendering functionality + * Wraps PhpThumb with the API used by Croppa to transform the src image */ class Image { diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index c295367..9c6c7f0 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -22,11 +22,6 @@ public function register() { // Version specific registering if ($this->version() == 5) $this->registerLaravel5(); - // Bind a new singleton instance of Croppa to the app - $this->app->singleton('croppa', function($app) { - return new Croppa($app->make('config')->get('croppa::config')); - }); - // Bind the Croppa URL generator and parser $this->app->singleton('croppa.url', function($app) { return new URL($app->make('config')->get('croppa::config')); @@ -41,6 +36,11 @@ public function register() { $this->app->singleton('croppa.storage', function($app) { return Storage::make($app, $app->make('config')->get('croppa::config')); }); + + // API for use in apps + $this->app->singleton('croppa.helpers', function($app) { + return new Helpers($app['croppa.url'], $app['croppa.storage']); + }); } /** @@ -101,10 +101,10 @@ public function bootLaravel5() { */ public function provides() { return [ - 'croppa', 'croppa.url', 'croppa.handler', 'croppa.storage', + 'croppa.helpers', ]; } } From b534a412c6a48ecee091fe8f1401f1a82d6bb615 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 14:49:01 -0700 Subject: [PATCH 29/39] Updating README --- README.md | 102 +++++++++++++++++++++++++++--------------- src/config/config.php | 2 +- 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 6bfd6b4..f912303 100755 --- a/README.md +++ b/README.md @@ -2,19 +2,20 @@ [![Packagist](https://img.shields.io/packagist/v/bkwld/croppa.svg)](https://packagist.org/packages/bkwld/croppa) [![Build Status](https://img.shields.io/travis/BKWLD/croppa.svg)](https://travis-ci.org/BKWLD/croppa) -Croppa is an thumbnail generator bundle for Laravel 4.x. It follows a different approach from libraries that store your thumbnail dimensions in the model, like [Paperclip](https://github.com/thoughtbot/paperclip). Instead, the resizing and cropping instructions come from specially formatted urls. For instance, say you have an image with this path: +Croppa is an thumbnail generator bundle for Laravel 4.x and 5.x. It follows a different approach from libraries that store your thumbnail dimensions in the model, like [Paperclip](https://github.com/thoughtbot/paperclip). Instead, the resizing and cropping instructions come from specially formatted urls. For instance, say you have an image with this path: - /uploads/09/03/screenshot.png + /uploads/09/03/screenshot.png To produce a 300x200 thumbnail of this, you would change the path to: - /uploads/09/03/screenshot-300x200.png + /uploads/09/03/screenshot-300x200.png This file, of course, doesn't exist yet. Croppa listens for specifically formatted image routes and build this thumbnail on the fly, outputting the image data (with correct headers) to the browser instead of the 404 response. At the same time, it saves the newly cropped image to the disk in the same location (the "…-300x200.png" path) that you requested. As a result, **all future requests get served directly from the disk**, bybassing PHP and all that overhead. This is a differentiating point compared to other, similar libraries. + ## Installation #### Server Requirements: @@ -22,39 +23,69 @@ At the same time, it saves the newly cropped image to the disk in the same locat * [gd](http://php.net/manual/en/book.image.php) * [exif](http://php.net/manual/en/book.exif.php) - Required if you want to have Croppa auto-rotate images from devices like mobile phones based on exif meta data. + #### Installation: -1. Add Croppa to your composer.json's requires: `"bkwld/croppa": "~3.0"`. Then do a regular composer install. -2. Add Croppa as a provider in your app/config/app.php's provider list: `'Bkwld\Croppa\ServiceProvider',` -3. Add the facade to your app/config/app.php's aliases: `'Croppa' => 'Bkwld\Croppa\Facade'`, +1. Add Croppa to your composer.json's requires: `"bkwld/croppa": "~4.0"`. Then do a regular composer install. +2. Add Croppa as a provider in your `app` config's provider list: `'Bkwld\Croppa\ServiceProvider',` +3. Add the facade to your `app` config's aliases: `'Croppa' => 'Bkwld\Croppa\Facade'`, + + ## Configuration -* **src_dirs**: An array of absolute paths where your relative image paths are searched for. The first match is used. By default, Croppa looks in /public/uploads, expecting you to store the relative path of "/uploads/path/to/file.png" in your database. -* **max_crops** (12): An optional number that limits how many crops you allow Croppa to create per source image. -* **jpeg_quality** (95): An integer from 0-100 for the quality of generated jpgs. -* **interlace** (true): This boolean affects whether progressive jpgs are created. -* **host** (undefined): Specify the host for Croppa::url() to use when generating absolute paths to images. If undefined and using Laravel, the `Request::host()` is used by default. -* **public** (undefined): Specify the route to the document_root of your app. If undefined and using Laravel, the `public_path()` is used by default. -* **ignore** (undefined): Ignore cropping for image URLs that match a regular expression. Useful for returning animated gifs. +Read the [source of the config file](https://github.com/BKWLD/croppa/blob/master/src/config/config.php) for documentation of the config options. Here are some examples of common setups. + + +#### Local src and crops directories + +The most common scenario, the src images and their crops are created in the doc_root's "uploads" directory. + +```php +return [ + 'src_dir' => public_path().'/uploads', + 'crops_dir' => public_path().'/uploads', + 'path' => 'uploads/(.*)$', +]; +``` + +#### Src images on S3, local crops + +This is a good solution for a load balanced enviornment. Each app server will end up with it's own cache of cropped images, so there is some wasted space. But the web server (Apache, etc) can still serve the crops directly on subsequent crop requests. + +```php +// Early in App bootstrapping, bind a Flysystem instance. This example assumes +// you are using the `graham-campbell/flysystem` Laravel adapter package +// https://github.com/GrahamCampbell/Laravel-Flysystem +App::singleton('s3', function($app) { + return $app['flysystem']->connection(); +}); + +// Croppa config.php +return [ + 'src_dir' => 's3', + 'crops_dir' => public_path().'/uploads', + 'path' => 'uploads/(.*)$', +]; +``` + -Note: Croppa will attempt to create the crops in the same directory as the source image. Thus, this directory **must be made writeable**. ## Usage The URL schema that Croppa uses is: - /path/to/image-widthxheight-option1-option2(arg1,arg2).ext + /path/to/image-widthxheight-option1-option2(arg1,arg2).ext So these are all valid: - /uploads/image-300x200.png // Crop to fit in 300x200 - /uploads/image-_x200.png // Resize to height of 200px - /uploads/image-300x_.png // Resize to width of 300px - /uploads/image-300x200-resize.png // Resize to fit within 300x200 - /uploads/image-300x200-quadrant(T).png // See the quadrant description below + /uploads/image-300x200.png // Crop to fit in 300x200 + /uploads/image-_x200.png // Resize to height of 200px + /uploads/image-300x_.png // Resize to width of 300px + /uploads/image-300x200-resize.png // Resize to fit within 300x200 + /uploads/image-300x200-quadrant(T).png // See the quadrant description below -### Croppa::url($src, $width, $height, array($options)) +#### Croppa::url($url, $width, $height, array($options)) To make preparing the URLs that Croppa expects an easier job, you can use the following view helper: @@ -72,20 +103,20 @@ To make preparing the URLs that Croppa expects an easier job, you can use the fo These are the arguments that Croppa::url() takes: -* $src : The relative path to your image. It is relative to a directory that you specified in the config's **src_dirs** +* $url : The URL of your source image. The path to the image relative to the `src_dir` will be extracted using the `path` config regex. * $width : A number or null for wildcard * $height : A number or null for wildcard * $options - An array of key value pairs, where the value is an optional array of arguments for the option. Supported option are: - * `resize` - Make the image fit in the provided width and height through resizing. When omitted, the default is to crop to fit in the bounds (unless one of sides is a wildcard). - * `quadrant($quadrant)` - Crop the remaining overflow of an image using the passed quadrant heading. The supported `$quadrant` values are: `T` - Top (good for headshots), `B` - Bottom, `L` - Left, `R` - Right, `C` - Center (default). See the [PHPThumb documentation](https://github.com/masterexploder/PHPThumb/blob/master/src/PHPThumb/GD.php#L485) for more info. - * `trim($x1, $y1, $x2, $y2)` - Crop the source image to the size defined by the two sets of coordinates ($x1, $y1, ...) BEFORE applying the $width and $height parameters. This is designed to be used with a frontend cropping UI like [jcrop](http://deepliquid.com/content/Jcrop.html) so that you can respect a cropping selection that the user has defined but then output thumbnails or sized down versions of that selection with Croppa. - * `trim_perc($x1_perc, $y1_perc, $x2_perc, $y2_perc)` - Has the same effect as `trim()` but accepts coordinates as percentages. Thus, the the upper left of the image is "0" and the bottom right of the image is "1". So if you wanted to trim the image to half the size around the center, you would add an option of `trim_perc(0.25,0.25,0.75,0.75)` - * `quality($int)` - Set the jpeg compression quality from 0 to 100. - * `interlace($bool)` - Set to `1` or `0` to turn interlacing on or off + * `resize` - Make the image fit in the provided width and height through resizing. When omitted, the default is to crop to fit in the bounds (unless one of sides is a wildcard). + * `quadrant($quadrant)` - Crop the remaining overflow of an image using the passed quadrant heading. The supported `$quadrant` values are: `T` - Top (good for headshots), `B` - Bottom, `L` - Left, `R` - Right, `C` - Center (default). See the [PHPThumb documentation](https://github.com/masterexploder/PHPThumb/blob/master/src/PHPThumb/GD.php#L485) for more info. + * `trim($x1, $y1, $x2, $y2)` - Crop the source image to the size defined by the two sets of coordinates ($x1, $y1, ...) BEFORE applying the $width and $height parameters. This is designed to be used with a frontend cropping UI like [jcrop](http://deepliquid.com/content/Jcrop.html) so that you can respect a cropping selection that the user has defined but then output thumbnails or sized down versions of that selection with Croppa. + * `trim_perc($x1_perc, $y1_perc, $x2_perc, $y2_perc)` - Has the same effect as `trim()` but accepts coordinates as percentages. Thus, the the upper left of the image is "0" and the bottom right of the image is "1". So if you wanted to trim the image to half the size around the center, you would add an option of `trim_perc(0.25,0.25,0.75,0.75)` + * `quality($int)` - Set the jpeg compression quality from 0 to 100. + * `interlace($bool)` - Set to `1` or `0` to turn interlacing on or off Note: Croppa will not upscale images. In other words, if you ask for a size bigger than the source, it will **only** create an image as big as the original source (though possibly cropped to give you the aspect ratio you requested). -### Croppa::delete($src) +#### Croppa::delete($url) You can delete a source image and all of it's crops (like if a related DB row was deleted) by running: @@ -93,21 +124,21 @@ You can delete a source image and all of it's crops (like if a related DB row wa Croppa::delete('/path/to/src.png'); ``` -### Croppa::sizes($src, $width, $height, array($options)) - +#### Croppa::reset($url) -You can get the width and height of the image for putting in a style tag by passing the same args as `Croppa::url()` expexts to `Croppa::sizes()`: +Similar to `Croppa::delete()` except the source image is preserved, only the crops are deleted. ```php - +Croppa::reset('/path/to/src.png'); ``` + ## croppa.js A module is included to prepare formatted URLs from JS. This can be helpful when you are creating views from JSON responses from an AJAX request; you don't need to format the URLs on the server. It can be loaded via Require.js, CJS, or as browser global variable. -### croppa.url(src, width, height, options) +### croppa.url(url, width, height, options) Works just like the PHP `Croppa::url` except for how options get formatted (since JS doesn't have associative arrays). @@ -120,6 +151,7 @@ croppa.url('/path/to/img.jpg', 300, 200, ['resize', {quadrant: ['T']}]); Run `php artisan bundle:publish croppa` to have Laravel copy the JS to your public directory. It will go to /public/bundles/croppa/js by default. + ## Thanks -This bundle uses [PHPThumb](https://github.com/masterexploder/PHPThumb) to do all the [image resizing](https://github.com/masterexploder/PHPThumb/wiki/Basic-Usage). "Crop" is equivalent to it's adaptiveResize() and "resize" is … resize(). +This bundle uses [PHPThumb](https://github.com/masterexploder/PHPThumb) to do all the [image resizing](https://github.com/masterexploder/PHPThumb/wiki/Basic-Usage). "Crop" is equivalent to it's adaptiveResize() and "resize" is … resize(). Support for interacting with non-local disks provided by [Flysystem](http://flysystem.thephpleague.com/). diff --git a/src/config/config.php b/src/config/config.php index 79d88d6..53358e0 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -30,7 +30,7 @@ /** * Maximum number of sizes to allow for a particular source file. This is to - * limit scripts from filling up your hard drive with images. Set to false or + * limit scripts from filling up your hard drive with images. Set to falsey or * comment out to have no limit. * * @var integer | boolean From 2d0cc92c0d0c1801528dd60e4349d420bfb646ec Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 15:00:17 -0700 Subject: [PATCH 30/39] Including minor versions of Laravel --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3afda74..7552ce5 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "require": { "php": ">=5.4.0", - "illuminate/support": "4.0 - 5.0", + "illuminate/support": "4 - 5", "league/flysystem": "~1.0", "symfony/http-foundation": "~2.0", "symfony/http-kernel": "~2.0", From 7b6fa7481bd701f36f47665424e5665b2ad69fe2 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 16:05:12 -0700 Subject: [PATCH 31/39] Adding MIT license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6f09a63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 BKWLD + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From d4b83a6dcb3e29c66886fa3fa792765dcdb2caa5 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Tue, 7 Apr 2015 16:12:27 -0700 Subject: [PATCH 32/39] Fixing calls to delete and reset And ditching the call_user_func approach --- src/Bkwld/Croppa/Helpers.php | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/Bkwld/Croppa/Helpers.php b/src/Bkwld/Croppa/Helpers.php index cff41cb..cff0b7b 100644 --- a/src/Bkwld/Croppa/Helpers.php +++ b/src/Bkwld/Croppa/Helpers.php @@ -29,38 +29,52 @@ public function __construct(URL $url, Storage $storage) { /** * Delete source image and all of it's crops - * + * + * @param string $url + * @return void * @see Bkwld\Croppa\Storage::deleteSrc() */ - public function delete() { - return call_user_func_array([$this->storage, 'deleteSrc'], func_get_args()); + public function delete($url) { + return $this->storage->deleteSrc($this->url->relativePath($url)); } /** * Delete just the crops, leave the source image - * + * + * @param string $url + * @return void * @see Bkwld\Croppa\Storage::deleteCrops() */ - public function reset() { - return call_user_func_array([$this->storage, 'deleteCrops'], func_get_args()); + public function reset($url) { + return $this->storage->deleteCrops($this->url->relativePath($url)); } /** * Create an image tag rather than just the URL. Accepts the same params as url() * + * @param string $url URL of an image that should be cropped + * @param integer $width Target width + * @param integer $height Target height + * @param array $options Addtional Croppa options, passed as key/value pairs. Like array('resize') + * @return string An HTML img tag for the new image * @see Bkwld\Croppa\URL::generate() */ - public function tag() { - return ''; + public function tag($url, $width = null, $height = null, $options = null) { + return ''; } /** * Pass through URL requrests to URL->generate(). * + * @param string $url URL of an image that should be cropped + * @param integer $width Target width + * @param integer $height Target height + * @param array $options Addtional Croppa options, passed as key/value pairs. Like array('resize') + * @return string The new path to your thumbnail * @see Bkwld\Croppa\URL::generate() */ - public function url() { - return call_user_func_array([$this->url, 'generate'], func_get_args()); + public function url($url, $width = null, $height = null, $options = null) { + return $this->url->generate($url, $width, $height, $options); } } \ No newline at end of file From 13d991b721485077b93f6ef4cedcd593674fe7dd Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 8 Apr 2015 16:20:31 -0700 Subject: [PATCH 33/39] Removing "host" config option #80 --- src/Bkwld/Croppa/Handler.php | 15 +++++++++------ src/Bkwld/Croppa/Storage.php | 17 +---------------- src/Bkwld/Croppa/URL.php | 19 ++++++++++--------- src/config/config.php | 28 ++++++++++------------------ tests/TestUrlGenerator.php | 11 ++++++++--- 5 files changed, 38 insertions(+), 52 deletions(-) diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index 94a88dd..f1954e7 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -43,7 +43,7 @@ public function handle($request) { $crop_path = $this->url->relativePath($request); // If the crops_dir is a remote disk, check if the path exists on it and redirect - if ($this->storage->cropsAreRemote() + if (($remote_crops = $this->storage->cropsAreRemote()) && $this->storage->cropExists($crop_path)) { return new RedirectResponse($this->storage->cropUrl($crop_path), 301); } @@ -65,14 +65,17 @@ public function handle($request) { $this->url->phpThumbConfig($options) ); - // Process the image, get the image data back, and write to disk - $file = $this->storage->writeCrop($crop_path, + // Process the image and write its data to disk + $this->storage->writeCrop($crop_path, $image->process($width, $height, $options)->get() ); - // Redirect to remote crops or render the image - if (preg_match('#^https?://#', $file)) return new RedirectResponse($file, 301); - else return new BinaryFileResponse($file, 200, [ + // Redirect to remote crops ... + if ($remote_crops) { + return new RedirectResponse($this->url->pathToUrl($crop_path), 301); + + // ... or echo the image data to the browser + } else return new BinaryFileResponse($file, 200, [ 'Content-Type' => $this->getContentType($path), ]); diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index 8f8dab6..0799e4c 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -128,19 +128,6 @@ public function cropExists($path) { return $this->crops_disk->has($path); } - /** - * Get the URL to a remote crop - * - * @param string $path - * @throws Exception - * @return string - */ - public function cropUrl($path) { - if (empty($this->config['url_prefix'])) { - throw new Exception('Croppa: You must set a `url_prefix` with remote crop disks.'); - } return $this->config['url_prefix'].$path; - } - /** * Get the src image data or throw an exception * @@ -158,12 +145,10 @@ public function readSrc($path) { * * @param string $path Where to save the crop * @param string $contents The image data - * @param string Return the abolute path to the image OR its redirect URL + * @param void */ public function writeCrop($path, $contents) { $this->crops_disk->write($path, $contents); - if ($this->cropsAreRemote()) return $this->cropUrl($path); - else return $this->config['crops_dir'].'/'.$path; } /** diff --git a/src/Bkwld/Croppa/URL.php b/src/Bkwld/Croppa/URL.php index d08d718..dfa6168 100644 --- a/src/Bkwld/Croppa/URL.php +++ b/src/Bkwld/Croppa/URL.php @@ -55,19 +55,18 @@ public function pattern() { */ public function generate($url, $width = null, $height = null, $options = null) { - // Extract the path from a URL if a URL was provided instead of a path. It - // will have a leading slash. - $path = parse_url($url, PHP_URL_PATH); + // Extract the path from a URL and remove it's leading slash + $path = ltrim(parse_url($url, PHP_URL_PATH), '/'); // Skip croppa requests for images the ignore regexp if (isset($this->config['ignore']) && preg_match('#'.$this->config['ignore'].'#', $path)) { - return $this->addHost($path); + return $this->pathToUrl($path); } // Defaults if (empty($path)) return; // Don't allow empty strings - if (!$width && !$height) return $this->addHost($path); // Pass through if empty + if (!$width && !$height) return $this->pathToUrl($path); // Pass through if empty $width = $width ? round($width) : '_'; $height = $height ? round($height) : '_'; @@ -83,9 +82,9 @@ public function generate($url, $width = null, $height = null, $options = null) { // Assemble the new path and return $parts = pathinfo($path); - $path = '/'.trim($parts['dirname'],'/').'/'.$parts['filename'].$suffix; + $path = trim($parts['dirname'],'/').'/'.$parts['filename'].$suffix; if (isset($parts['extension'])) $path .= '.'.$parts['extension']; - return $this->addHost($path); + return $this->pathToUrl($path); } /** @@ -94,8 +93,10 @@ public function generate($url, $width = null, $height = null, $options = null) { * @param string $path URL path (with leading slash) * @return string */ - public function addHost($path) { - return empty($this->config['host']) ? $path : $this->config['host'].$path; + public function pathToUrl($path) { + if (empty($this->config['url_prefix'])) return '/'.$path; + else if (empty($this->config['path'])) return rtrim($this->config['url_prefix'], '/').'/'.$path; + else return rtrim($this->config['url_prefix'], '/').'/'.$this->relativePath($path); } /** diff --git a/src/config/config.php b/src/config/config.php index 53358e0..1a56f8b 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -1,9 +1,9 @@ App::isLocal() ? false : 12, + /* - |-------------------------------------------------------------------------- + |----------------------------------------------------------------------------- | URL parsing and generation - |-------------------------------------------------------------------------- + |----------------------------------------------------------------------------- */ /** @@ -67,25 +68,16 @@ /** * A string that is prepended to the path captured by the `path` pattern - * (above) that is used to from the URL to remote crops. Only relevant if your - * `crops_dir` is an IoC binding to a non-local Flysystem instance. + * (above) that is used to from the URL to crops. */ - // 'url_prefix' => 'https://your-bucket.s3.amazonaws.com/uploads/', + // 'url_prefix' => '//'.Request::getHttpHost().'/uploads/', // Local + // 'url_prefix' => 'https://your-bucket.s3.amazonaws.com/uploads/', // S3 - /** - * Specify the host for Croppa::url() to use when generating URLs. An - * altenative to the default is to use the app.url setting: - * - * preg_replace('#https?:#', '', Config::get('app.url')) - * - * @var string - */ - // 'host' => '//'.Request::getHttpHost(), /* - |-------------------------------------------------------------------------- + |----------------------------------------------------------------------------- | Image settings - |-------------------------------------------------------------------------- + |----------------------------------------------------------------------------- */ /** diff --git a/tests/TestUrlGenerator.php b/tests/TestUrlGenerator.php index 5566064..d7469ff 100644 --- a/tests/TestUrlGenerator.php +++ b/tests/TestUrlGenerator.php @@ -37,13 +37,18 @@ public function testHostInSrc() { } public function testHostConfig() { - $url = new URL([ 'host' => '//domain.tld', ]); + $url = new URL([ 'url_prefix' => '//domain.tld', ]); $this->assertEquals('//domain.tld/path/file-200x_.png', $url->generate('/path/file.png', 200)); } - public function testHostAndSchemaConfig() { - $url = new URL([ 'host' => 'https://domain.tld', ]); + public function testUrlPrefixWithSchema() { + $url = new URL([ 'url_prefix' => 'https://domain.tld/' ]); $this->assertEquals('https://domain.tld/path/file-200x_.png', $url->generate('http://domain.tld/path/file.png', 200)); } + public function testUrlPrefixWithPath() { + $url = new URL([ 'url_prefix' => 'https://domain.tld/path/', 'path' => 'path/(.*)$' ]); + $this->assertEquals('https://domain.tld/path/file-200x_.png', $url->generate('/path/file.png', 200)); + } + } \ No newline at end of file From da8496737c6f23dbe2609588365d4b46ba184760 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 8 Apr 2015 16:35:30 -0700 Subject: [PATCH 34/39] The cached adapter class IS the adapter --- src/Bkwld/Croppa/Storage.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index 0799e4c..de308fe 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -106,16 +106,17 @@ public function makeDisk($dir) { * @return boolean */ public function cropsAreRemote() { + $adapter = $this->crops_disk->getAdapter(); // Currently, the CachedAdapter doesn't have a getAdapter method so I can't // tell if the adapter is local or not. I'm assuming that if they are using // the CachedAdapter, they're probably using a remote disk. I've written // a PR to add getAdapter to it. // https://github.com/thephpleague/flysystem-cached-adapter/pull/9 - if (!method_exists($this->crops_disk, 'getAdapter')) return true; + if (is_a($adapter, 'League\Flysystem\Cached\CachedAdapter')) return true; // Check if the crop disk is not local - return !is_a($this->crops_disk->getAdapter(), 'League\Flysystem\Adapter\Local'); + return !is_a($adapter, 'League\Flysystem\Adapter\Local'); } /** From a927f39323af3deda70fabaae248073c39d65a7b Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Wed, 8 Apr 2015 16:36:05 -0700 Subject: [PATCH 35/39] Fixing the absolute path of local crops #80 --- src/Bkwld/Croppa/Handler.php | 9 ++++++--- src/Bkwld/Croppa/Storage.php | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index f1954e7..5cabeab 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -75,9 +75,12 @@ public function handle($request) { return new RedirectResponse($this->url->pathToUrl($crop_path), 301); // ... or echo the image data to the browser - } else return new BinaryFileResponse($file, 200, [ - 'Content-Type' => $this->getContentType($path), - ]); + } else { + $absolute_path = $this->storage->getLocalCropsDirPath().'/'.$crop_path; + return new BinaryFileResponse($absolute_path, 200, [ + 'Content-Type' => $this->getContentType($path), + ]); + } } diff --git a/src/Bkwld/Croppa/Storage.php b/src/Bkwld/Croppa/Storage.php index de308fe..e6c0c85 100644 --- a/src/Bkwld/Croppa/Storage.php +++ b/src/Bkwld/Croppa/Storage.php @@ -152,6 +152,15 @@ public function writeCrop($path, $contents) { $this->crops_disk->write($path, $contents); } + /** + * Get a local crops disks absolute path + * + * @return string + */ + public function getLocalCropsDirPath() { + return $this->crops_disk->getAdapter()->getPathPrefix(); + } + /** * Delete src image * From ef46a326b6b047b67bbee4d4263f83ca2bc639c7 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 10 Apr 2015 16:00:47 -0700 Subject: [PATCH 36/39] L5 doesn't let facades be invoked from config files --- src/config/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.php b/src/config/config.php index 1a56f8b..4c505c8 100755 --- a/src/config/config.php +++ b/src/config/config.php @@ -35,7 +35,7 @@ * * @var integer | boolean */ - 'max_crops' => App::isLocal() ? false : 12, + 'max_crops' => 12, /* From 29c143c246bd7278c5a71dfc68d15d6d51fd7635 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 10 Apr 2015 16:01:28 -0700 Subject: [PATCH 37/39] L5 keys the the config slightly differently --- src/Bkwld/Croppa/ServiceProvider.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index 9c6c7f0..e0e4505 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -24,7 +24,7 @@ public function register() { // Bind the Croppa URL generator and parser $this->app->singleton('croppa.url', function($app) { - return new URL($app->make('config')->get('croppa::config')); + return new URL($app->make('config')->get('croppa')); }); // Handle the request for an image, this cooridnates the main logic @@ -34,7 +34,7 @@ public function register() { // Interact with the disk $this->app->singleton('croppa.storage', function($app) { - return Storage::make($app, $app->make('config')->get('croppa::config')); + return Storage::make($app, $app->make('config')->get('croppa')); }); // API for use in apps @@ -91,7 +91,17 @@ public function bootLaravel4() { public function bootLaravel5() { $this->publishes([ __DIR__.'/../../config/config.php' => config_path('croppa.php') - ], 'config'); + ], 'croppa'); + } + + /** + * Get the configuration, which is keyed differently in L5 vs l4 + * + * @return array + */ + public function getConfig() { + $key = $this->version() == 5 ? 'croppa' : 'croppa::config'; + return $this->app->make('config')->get($key); } /** From 23f57cddd4f980f52476fb9f0a82d484085b9a1a Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 10 Apr 2015 16:03:02 -0700 Subject: [PATCH 38/39] Using the new config --- src/Bkwld/Croppa/ServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bkwld/Croppa/ServiceProvider.php b/src/Bkwld/Croppa/ServiceProvider.php index e0e4505..a55ea15 100755 --- a/src/Bkwld/Croppa/ServiceProvider.php +++ b/src/Bkwld/Croppa/ServiceProvider.php @@ -24,7 +24,7 @@ public function register() { // Bind the Croppa URL generator and parser $this->app->singleton('croppa.url', function($app) { - return new URL($app->make('config')->get('croppa')); + return new URL($this->getConfig()); }); // Handle the request for an image, this cooridnates the main logic @@ -34,7 +34,7 @@ public function register() { // Interact with the disk $this->app->singleton('croppa.storage', function($app) { - return Storage::make($app, $app->make('config')->get('croppa')); + return Storage::make($app, $this->getConfig()); }); // API for use in apps From b8163ca0b73f8d656958b17b2c713cb451f69030 Mon Sep 17 00:00:00 2001 From: Robert Reinhard Date: Fri, 10 Apr 2015 16:38:09 -0700 Subject: [PATCH 39/39] Making Handler class a Controller instance so it route can be cached #81 --- composer.json | 1 + src/Bkwld/Croppa/Handler.php | 3 ++- src/Bkwld/Croppa/ServiceProvider.php | 26 +++++++++++++------------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index 7552ce5..b9711c1 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "require": { "php": ">=5.4.0", "illuminate/support": "4 - 5", + "illuminate/routing": "4 - 5", "league/flysystem": "~1.0", "symfony/http-foundation": "~2.0", "symfony/http-kernel": "~2.0", diff --git a/src/Bkwld/Croppa/Handler.php b/src/Bkwld/Croppa/Handler.php index 5cabeab..f1959ff 100644 --- a/src/Bkwld/Croppa/Handler.php +++ b/src/Bkwld/Croppa/Handler.php @@ -1,13 +1,14 @@ version() == 5) $this->registerLaravel5(); // Bind the Croppa URL generator and parser - $this->app->singleton('croppa.url', function($app) { + $this->app->singleton('Bkwld\Croppa\URL', function($app) { return new URL($this->getConfig()); }); // Handle the request for an image, this cooridnates the main logic - $this->app->singleton('croppa.handler', function($app) { - return new Handler($app['croppa.url'], $app['croppa.storage']); + $this->app->singleton('Bkwld\Croppa\Handler', function($app) { + return new Handler($app['Bkwld\Croppa\URL'], $app['Bkwld\Croppa\Storage']); }); // Interact with the disk - $this->app->singleton('croppa.storage', function($app) { + $this->app->singleton('Bkwld\Croppa\Storage', function($app) { return Storage::make($app, $this->getConfig()); }); // API for use in apps - $this->app->singleton('croppa.helpers', function($app) { - return new Helpers($app['croppa.url'], $app['croppa.storage']); + $this->app->singleton('Bkwld\Croppa\Helpers', function($app) { + return new Helpers($app['Bkwld\Croppa\URL'], $app['Bkwld\Croppa\Storage']); }); } @@ -67,9 +67,9 @@ public function boot() { } // Listen for Cropa style URLs, these are how Croppa gets triggered - $this->app['router']->get('{path}', function($path) { - return $this->app['croppa.handler']->handle($path); - })->where('path', app('croppa.url')->routePattern()); + $this->app['router'] + ->get('{path}', 'Bkwld\Croppa\Handler@handle') + ->where('path', $this->app['Bkwld\Croppa\URL']->routePattern()); } /** @@ -111,10 +111,10 @@ public function getConfig() { */ public function provides() { return [ - 'croppa.url', - 'croppa.handler', - 'croppa.storage', - 'croppa.helpers', + 'Bkwld\Croppa\URL', + 'Bkwld\Croppa\Handler', + 'Bkwld\Croppa\Storage', + 'Bkwld\Croppa\Helpers', ]; } }