Skip to content

Commit

Permalink
Implementing the "Flexible thumbnail formats" feature. See: BKWLD#212.…
Browse files Browse the repository at this point in the history
… WIP
  • Loading branch information
bdteo committed Mar 26, 2023
1 parent ade5592 commit 659b39a
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 53 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ composer.lock
/node_modules
/yarn.lock
.idea

### PHPUnit ###
# Covers PHPUnit
# Reference: https://phpunit.de/

# Generated files
.phpunit.result.cache
.phpunit.cache

172 changes: 136 additions & 36 deletions src/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace Bkwld\Croppa;

use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Routing\Redirector;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

Expand Down Expand Up @@ -35,7 +38,12 @@ class Handler extends Controller
/**
* Dependency injection.
*/
public function __construct(URL $url, Storage $storage, Request $request, ?array $config = null)
public function __construct(
URL $url,
Storage $storage,
Request $request,
?array $config = null
)
{
$this->url = $url;
$this->storage = $storage;
Expand All @@ -46,9 +54,12 @@ public function __construct(URL $url, Storage $storage, Request $request, ?array
/**
* Handles a Croppa style route.
*
* @param string $requestPath
* @return BinaryFileResponse|Application|Redirector|RedirectResponse
* @throws Exception
*/
public function handle(string $requestPath): mixed
public function handle(string $requestPath):
BinaryFileResponse|Application|Redirector|RedirectResponse
{
// Validate the signing token
$token = $this->url->signingToken($requestPath);
Expand All @@ -61,7 +72,12 @@ public function handle(string $requestPath): mixed

// Redirect to remote crops ...
if ($this->storage->cropsAreRemote()) {
return redirect(app('filesystem')->disk($this->config['crops_disk'])->url($cropPath), 301);
return redirect(
app('filesystem')
->disk($this->config['crops_disk'])
->url($cropPath),
301
);
// ... or echo the image data to the browser
}
$absolutePath = $this->storage->getLocalCropPath($cropPath);
Expand All @@ -73,68 +89,152 @@ public function handle(string $requestPath): mixed

/**
* Render image. Return the path to the crop relative to the storage disk.
* @param string $requestPath
* @return string|null
* @throws Exception
*/
public function render(string $requestPath): ?string
{
// Get crop path relative to it’s dir
$cropPath = $this->url->relativePath($requestPath);
$params = ParameterBucket::createFrom($requestPath);
$urlOptions = $params?->getUrlOptions() ?? [];
$configOptions = $this->url->config($urlOptions);

$cropPath = $this->getRelativeCropPath(
$requestPath,
$configOptions
);

// If the crops_disk is a remote disk and if the crop has already been
// created. If it has, just return that path.
if ($this->storage->cropsAreRemote() && $this->storage->cropExists($cropPath)) {
if ($this->shouldReturnExistingCrop($cropPath)) {
return $cropPath;
}

// Parse the path. In the case there is an error (the pattern on the route
// SHOULD have caught all errors with the pattern), return null.
if (!$params = $this->url->parse($requestPath)) {
if (!$params) {
return null;
}
list($path, $width, $height, $options) = $params;

// Check if there are too many crops already
$this->checkCropLimit($params->getPath());
$this->increaseMemoryLimitIfNeeded();
$image = $this->buildImage($params);
$this->processAndWriteImage($image, $cropPath, $params);

return $cropPath;
}

/**
* Get crop path relative to its directory.
* @throws Exception
*/
protected function getRelativeCropPath(
string $requestPath,
array $options
): string
{
$relativePath = $this->url->relativePath($requestPath);

$format = data_get($options, 'format');

if ($format) {
$relativePath = $this->replaceOriginalFileSuffix(
$relativePath,
$format
);
}

return $relativePath;
}

protected function replaceOriginalFileSuffix(
string $path,
string $suffix
): string
{
$dirname = pathinfo($path, PATHINFO_DIRNAME);
$fileName = pathinfo($path, PATHINFO_FILENAME);

return sprintf(
'%s/%s.%s',
$dirname,
$fileName,
$suffix
);
}

/**
* Determine if the existing crop should be returned.
* @param string $cropPath
* @return bool
*/
protected function shouldReturnExistingCrop(string $cropPath): bool
{
return $this->storage->cropsAreRemote() &&
$this->storage->cropExists($cropPath);
}

/**
* Check if there are too many crops already.
* @param string $path
* @throws Exception
*/
protected function checkCropLimit(string $path): void
{
if ($this->storage->tooManyCrops($path)) {
throw new Exception('Croppa: Max crops');
}
}

// Increase memory limit, cause some images require a lot to resize
/**
* Increase memory limit if needed.
*/
protected function increaseMemoryLimitIfNeeded(): void
{
if ($this->config['memory_limit'] !== null) {
ini_set('memory_limit', $this->config['memory_limit']);
}
}

// Build a new image using fetched image data
$image = new Image(
$this->storage->path($path),
$this->url->config($options)
/**
* Build a new image using fetched image data.
*/
protected function buildImage(ParameterBucket $params): Image
{
return new Image(
$this->storage->path($params->getPath()),
$params->config()
);
}

/**
* Process the image and write its data to disk.
* @throws Exception
*/
protected function processAndWriteImage(
Image $image,
string $cropPath,
ParameterBucket $params
): void
{
$newImage = $image->process(
$params->getWidth(),
$params->getHeight(),
$params->getUrlOptions()
);

// Process the image and write its data to disk
$this->storage->writeCrop(
$cropPath,
$image->process($width, $height, $options)->get()
$newImage->get()
);

// Return the path to the crop, relative to the storage disk
return $cropPath;
}

/**
* Determining MIME-type via the path name.
*/
public function getContentType(string $path): string
{
switch (pathinfo($path, PATHINFO_EXTENSION)) {
case 'gif':
return 'image/gif';

case 'png':
return 'image/png';

case 'webp':
return 'image/webp';

default:
return 'image/jpeg';
}
return match (pathinfo($path, PATHINFO_EXTENSION)) {
'gif' => 'image/gif',
'png' => 'image/png',
'webp' => 'image/webp',
default => 'image/jpeg',
};
}
}
23 changes: 8 additions & 15 deletions src/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public function process(?int $width, ?int $height, array $options = []): self
}

/**
* Turn on interlacing to make progessive JPEG files.
* Turn on interlacing to make progressive JPEG files.
*/
public function interlace(): self
{
Expand Down Expand Up @@ -245,7 +245,7 @@ public function pad(?int $width, ?int $height, array $options): self
}

/**
* Apply filters that have been defined in the config as seperate classes.
* Apply filters that have been defined in the config as separate classes.
*/
public function applyFilters(array $options): self
{
Expand All @@ -260,19 +260,12 @@ public function applyFilters(array $options): self

private function getFormatFromPath(string $path): string
{
switch (pathinfo($path, PATHINFO_EXTENSION)) {
case 'gif':
return 'gif';

case 'png':
return 'png';

case 'webp':
return 'webp';

default:
return 'jpg';
}
return match (pathinfo($path, PATHINFO_EXTENSION)) {
'gif' => 'gif',
'png' => 'png',
'webp' => 'webp',
default => 'jpg',
};
}

/**
Expand Down
95 changes: 95 additions & 0 deletions src/ParameterBucket.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Bkwld\Croppa;

class ParameterBucket
{
protected string $path;
protected ?int $width;
protected ?int $height;
protected array $urlOptions;
protected URL|null $urlHelper;

public function __construct(
string $path,
?int $width,
?int $height,
array $urlOptions
)
{
$this->path = $path;
$this->width = $width;
$this->height = $height;
$this->urlOptions = $urlOptions;
}

/**
* @param string $requestPath
* @return static|null
* @throws Exception
*/
public static function createFrom(string $requestPath): ?static
{
$url = app(URL::class);
$params = $url->parse($requestPath);

if (!$params) {
return null;
}

[$path, $width, $height, $options] = $params;

return new self(
$path,
$width,
$height,
$options
);
}

/**
* @return string
*/
public function getPath(): string
{
return $this->path;
}

/**
* @return int|null
*/
public function getWidth(): ?int
{
return $this->width;
}

/**
* @return int|null
*/
public function getHeight(): ?int
{
return $this->height;
}

/**
* @return array
*/
public function getUrlOptions(): array
{
return $this->urlOptions;
}

/**
* Take options in the URL and options from the config file
* and produce a config array.
*/
public function config(): array
{
return $this->getUrlHelper()->config($this->getUrlOptions());
}

protected function getUrlHelper() {
$this->urlHelper = $this->urlHelper ?? app(URL::class);
return $this->urlHelper;
}
}
Loading

0 comments on commit 659b39a

Please sign in to comment.