First commit. Implement a very basic CDN server to serve and resize images
This commit is contained in:
commit
9907bde834
0
.env.sample
Normal file
0
.env.sample
Normal file
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/.env
|
||||||
|
/vendor/
|
||||||
|
/*.db
|
||||||
|
/.idea/
|
22
composer.json
Normal file
22
composer.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "lewis/image-resizer",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php-di/php-di": "^7.0",
|
||||||
|
"vlucas/phpdotenv": "^5.5",
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-pdo": "*",
|
||||||
|
"ext-pdo_sqlite": "*"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"ImageResizer\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Lewis Dale",
|
||||||
|
"email": "lewis@ihd.software"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1019
composer.lock
generated
Normal file
1019
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
src/ImageResizer.php
Normal file
20
src/ImageResizer.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
error_reporting(E_ERROR | E_PARSE);
|
||||||
|
|
||||||
|
require_once './vendor/autoload.php';
|
||||||
|
|
||||||
|
use ImageResizer\Lib\Resizer;
|
||||||
|
use ImageResizer\Lib\SqliteImageDb;
|
||||||
|
use ImageResizer\Models\ResizeParams;
|
||||||
|
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable([__DIR__, __DIR__ . "/.."]);
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$imageDb = new SqliteImageDb(new PDO("sqlite:{$_ENV["db_name"]}"));
|
||||||
|
|
||||||
|
$imgPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||||
|
$params = new ResizeParams($_GET);
|
||||||
|
|
||||||
|
$resizer = new Resizer($imageDb);
|
||||||
|
|
||||||
|
echo $resizer->performResize($imgPath, $params);
|
19
src/Lib/ImageDb.php
Normal file
19
src/Lib/ImageDb.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace ImageResizer\Lib;
|
||||||
|
|
||||||
|
use ImageResizer\Models\Image;
|
||||||
|
use ImageResizer\Models\ResizeParams;
|
||||||
|
|
||||||
|
interface ImageDb
|
||||||
|
{
|
||||||
|
public function findImage(string $path, ResizeParams $params): Image|null;
|
||||||
|
|
||||||
|
public function saveImage(
|
||||||
|
string $path,
|
||||||
|
ResizeParams $params,
|
||||||
|
string $content,
|
||||||
|
): void;
|
||||||
|
}
|
55
src/Lib/Resizer.php
Normal file
55
src/Lib/Resizer.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace ImageResizer\Lib;
|
||||||
|
|
||||||
|
use ImageResizer\Models\ResizeParams;
|
||||||
|
use ImageResizer\Models\Image;
|
||||||
|
class Resizer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ImageDb $imageDb
|
||||||
|
)
|
||||||
|
{}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function performResize(string $path, ResizeParams $params)
|
||||||
|
{
|
||||||
|
// Check if the image has already been processed
|
||||||
|
if ($image = $this->imageDb->findImage($path, $params)) {
|
||||||
|
|
||||||
|
header("Content-type: {${$image->getMime()}}");
|
||||||
|
return base64_decode($image->encode(75));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the image at the given URL
|
||||||
|
$original_img = Image::fromUrl($_ENV['root_domain'] . $path);
|
||||||
|
$converted = $this->createVariant($original_img, $params);
|
||||||
|
|
||||||
|
// Store the image in the database
|
||||||
|
$this->imageDb->saveImage(
|
||||||
|
$path,
|
||||||
|
$params,
|
||||||
|
$converted
|
||||||
|
);
|
||||||
|
|
||||||
|
header("Content-type: {$original_img->getMime()}");
|
||||||
|
return base64_decode($converted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createVariant(Image $original, ResizeParams $params): string
|
||||||
|
{
|
||||||
|
$quality = $params->quality ?: -1;
|
||||||
|
|
||||||
|
if ($params->width) {
|
||||||
|
$original->toWidth($params->width);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $original->encode($quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
56
src/Lib/SqliteImageDb.php
Normal file
56
src/Lib/SqliteImageDb.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ImageResizer\Lib;
|
||||||
|
|
||||||
|
use ImageResizer\Models\Image;
|
||||||
|
use ImageResizer\Models\ResizeParams;
|
||||||
|
|
||||||
|
class SqliteImageDb implements ImageDb {
|
||||||
|
public function __construct(
|
||||||
|
private readonly \PDO $db
|
||||||
|
) {
|
||||||
|
$this->up();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function up() {
|
||||||
|
$this->db->exec('CREATE TABLE IF NOT EXISTS images (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
width INTEGER NULLABLE,
|
||||||
|
height INTEGER NULLABLE,
|
||||||
|
quality INTEGER NULLABLE,
|
||||||
|
content TEXT NOT NULL
|
||||||
|
)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findImage(string $path, ResizeParams $params): Image | null {
|
||||||
|
$stmt = $this->db->prepare('SELECT * FROM images WHERE path = :path AND width = :width AND height = :height AND quality = :quality');
|
||||||
|
$stmt->execute([
|
||||||
|
':path' => $path,
|
||||||
|
':width' => $params->width,
|
||||||
|
':height' => $params->height,
|
||||||
|
':quality' => $params->quality
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $stmt->fetchObject();
|
||||||
|
|
||||||
|
return $result ? Image::fromString(base64_decode($result->content)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveImage(
|
||||||
|
string $path,
|
||||||
|
ResizeParams $params,
|
||||||
|
string $content,
|
||||||
|
): void {
|
||||||
|
$stmt = $this->db->prepare("INSERT INTO images (path, width, height, quality, content) VALUES (:path, :width, :height, :quality, :content)"
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
':path' => $path,
|
||||||
|
':width' => $params->width,
|
||||||
|
':height' => $params->height,
|
||||||
|
':quality' => $params->quality,
|
||||||
|
':content' => $content
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
119
src/Models/Image.php
Normal file
119
src/Models/Image.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace ImageResizer\Models;
|
||||||
|
|
||||||
|
class Image
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private \GdImage $image,
|
||||||
|
private int $type,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromUrl(string $url): self
|
||||||
|
{
|
||||||
|
$remote_source = file_get_contents($url);
|
||||||
|
|
||||||
|
if (!$remote_source) {
|
||||||
|
throw new \Exception("Remote image does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::fromString($remote_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromString(string $str): self
|
||||||
|
{
|
||||||
|
$image = imagecreatefromstring($str);
|
||||||
|
|
||||||
|
if (!$image) {
|
||||||
|
throw new \Exception("Failed to parse image");
|
||||||
|
}
|
||||||
|
|
||||||
|
[2 => $type] = getimagesizefromstring($str);
|
||||||
|
return new self($image, $type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWidth(): int
|
||||||
|
{
|
||||||
|
return imagesx($this->image);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeight(): int
|
||||||
|
{
|
||||||
|
return imagesy($this->image);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): int
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMime(): string
|
||||||
|
{
|
||||||
|
return image_type_to_mime_type($this->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExt(): string
|
||||||
|
{
|
||||||
|
return image_type_to_extension($this->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function raw(): \GdImage
|
||||||
|
{
|
||||||
|
return $this->image;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(int $type): void
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scale(int $ratio): void
|
||||||
|
{
|
||||||
|
$this->image = imagescale($this->image, $this->getWidth() * $ratio, $this->getHeight() * $ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toWidth(int $width): void
|
||||||
|
{
|
||||||
|
if ($width < $this->getWidth()) {
|
||||||
|
|
||||||
|
$ratio = $width / $this->getWidth();
|
||||||
|
$height = intval(floor($this->getHeight() * $ratio));
|
||||||
|
|
||||||
|
$this->image = imagescale($this->image, $width, $height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function encode(int $quality): string
|
||||||
|
{
|
||||||
|
ob_start();
|
||||||
|
$type = $this->clientSupportsWebp() ? IMAGETYPE_WEBP : $this->type;
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case IMAGETYPE_PNG:
|
||||||
|
imagepng($this->image, null, $quality);
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_GIF:
|
||||||
|
imagegif($this->image);
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_JPEG:
|
||||||
|
imagejpeg($this->image, null, $quality);
|
||||||
|
break;
|
||||||
|
case IMAGETYPE_WEBP:
|
||||||
|
imagewebp($this->image, null, $quality);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$image_data = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
|
||||||
|
return base64_encode($image_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clientSupportsWebp(): bool {
|
||||||
|
return str_contains($_SERVER['HTTP_ACCEPT'], 'image/webp');
|
||||||
|
}
|
||||||
|
}
|
19
src/Models/ResizeParams.php
Normal file
19
src/Models/ResizeParams.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
|
namespace ImageResizer\Models;
|
||||||
|
|
||||||
|
class ResizeParams
|
||||||
|
{
|
||||||
|
public int|null $width;
|
||||||
|
public int|null $height;
|
||||||
|
public int|null $quality;
|
||||||
|
|
||||||
|
public function __construct(array $params)
|
||||||
|
{
|
||||||
|
$this->width = intval($params['width']) ?: null;
|
||||||
|
$this->height = intval($params['height']) ?: null;
|
||||||
|
$this->quality = isset($params['quality']) ? intval($params['quality']) : null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user