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