First commit. Implement a very basic CDN server to serve and resize images

This commit is contained in:
Lewis Dale 2023-08-25 20:50:52 +01:00
commit 9907bde834
10 changed files with 1333 additions and 0 deletions

0
.env.sample Normal file
View File

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/.env
/vendor/
/*.db
/.idea/

22
composer.json Normal file
View 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

File diff suppressed because it is too large Load Diff

20
src/ImageResizer.php Normal file
View 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
View 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
View 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
View 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
View 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');
}
}

View 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;
}
}