Compare commits

...

12 commits

14 changed files with 322 additions and 80 deletions

View file

@ -6,7 +6,8 @@
"twig/twig": "^1.44.2",
"lifo/ip": "^1.0",
"gettext/gettext": "^1.0",
"mrclay/minify": "^2.1.6"
"mrclay/minify": "^2.1.6",
"xantios/mimey": "^2.2"
},
"autoload": {
"classmap": ["inc/"],
@ -25,7 +26,9 @@
"inc/polyfill.php",
"inc/error.php",
"inc/functions.php",
"inc/functions/net.php"
"inc/functions/net.php",
"inc/functions/fs.php"
]
},
"license": "Tinyboard + vichan",

56
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "346d80deda89b0298a414b565213f312",
"content-hash": "1339c8c595b0cde0968d91be0edc1908",
"packages": [
{
"name": "gettext/gettext",
@ -316,15 +316,63 @@
}
],
"time": "2021-11-25T13:31:46+00:00"
},
{
"name": "xantios/mimey",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/Xantios/mimey.git",
"reference": "8cb6f0c29b8eadde38777ed947847f4253c00b60"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Xantios/mimey/zipball/8cb6f0c29b8eadde38777ed947847f4253c00b60",
"reference": "8cb6f0c29b8eadde38777ed947847f4253c00b60",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.4",
"phpunit/phpunit": "^9.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Mimey\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Xantios Krugor",
"email": "git@xantios.nl"
},
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "PHP package for converting file extensions to MIME types and vice versa.",
"support": {
"issues": "https://github.com/Xantios/mimey/issues",
"source": "https://github.com/Xantios/mimey/tree/v2.2.0"
},
"time": "2021-06-12T14:33:14+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}

View file

@ -0,0 +1,15 @@
<?php
namespace Vichan\Data;
class ImageMetadataResult {
public int $width;
public int $height;
public string $mime;
public function __construct(int $width, int $height, string $mime) {
$this->width = $$width;
$this->height = $$height;
$this->mime = $$mime;
}
}

View file

@ -4,6 +4,7 @@ namespace Vichan\Data;
class ThumbGenerationResult {
public string $thumb_file_path;
public string $thumb_mime;
public bool $is_thumb_file_temporary;
public int $width;
public int $height;

View file

@ -0,0 +1,29 @@
<?php
namespace Vichan\Service\Media;
use Vichan\Data\ImageMetadataResult;
use Vichan\Data\MagickMetadataReader;
/**
* Do not use this if you can.
*
* Some formats may contain no image or may contain multiple images. In these cases, getimagesize() might not be
* able to properly determine the image size. getimagesize() will return zero for width and height in these cases.
*
* getimagesize() is agnostic of any image metadata.
* If e.g. the Exif Orientation flag is set to a value which rotates the image by 90 or 270 degress, index 0 and 1
* are swapped, i.e. the contain the height and width, respectively.
*/
class DefaultImageMetadataReader implements ImageMetadataReader {
public function getMetadata(string $file_path): ImageMetadataResult {
$ret = \getimagesize($file_path, $info);
if ($ret === false) {
throw new \RuntimeException("Could not read image sizes of '$file_path'");
}
if ($ret[2] == \IMAGETYPE_UNKNOWN) {
throw new \RuntimeException("Error '$file_path' is not an image");
}
return new ImageMetadataResult($ret[0], $ret[1], $ret['mime']);
}
}

View file

@ -5,16 +5,18 @@ use Vichan\Data\ThumbGenerationResult;
class FallbackThumbGenerator implements ThumbGenerator {
private string $thumb_path;
private string $thumb_width;
private string $thumb_height;
private string $path;
private int $width;
private int $height;
private string $mime;
public function __construct(ImageFormatReader $image_format_reader, string $default_thumb_path) {
list($width, $height) = $image_format_reader->getSizes($default_thumb_path);
$this->thumb_path = $default_thumb_path;
$this->thumb_width = $width;
$this->thumb_height = $height;
public function __construct(ImageMetadataReader $image_metadate_reader, string $default_thumb_path) {
$res = $image_metadate_reader->getMetadata($default_thumb_path);
$this->path = $default_thumb_path;
$this->width = $res->width;
$this->height = $res->height;
$this->mime = $res->mime;
}
public function supportsMime(string $mime): bool {
@ -23,15 +25,18 @@ class FallbackThumbGenerator implements ThumbGenerator {
public function generateThumb(
string $source_file_path,
string $source_file_mime,
string $preferred_out_file_path,
string $preferred_out_mime,
int $max_width,
int $max_height
): ThumbGenerationResult {
$res = new ThumbGenerationResult();
$res->thumb_file_path = $this->thumb_path;
$res->thumb_file_path = $this->path;
$res->thumb_mime = $this->mime;
$res->is_thumb_file_temporary = false;
$res->width = \min($this->thumb_width, $max_width);
$res->height = \min($this->thumb_height, $max_height);
$res->width = \min($this->width, $max_width);
$res->height = \min($this->height, $max_height);
return $res;
}
}

View file

@ -1,18 +0,0 @@
<?php
namespace Vichan\Service\Media;
class GdImageFormatReader implements ImageFormatReader {
/**
* getimagesize() is agnostic of any image metadata.
* If e.g. the Exif Orientation flag is set to a value which rotates the image by 90 or 270 degress, index 0 and 1
* are swapped, i.e. the contain the height and width, respectively.
*/
public function getSizes(string $file_path): array {
$ret = \getimagesize($file_path);
if ($ret === false) {
throw new \RuntimeException("Could not read image sizes of '$file_path'");
}
return $ret;
}
}

View file

@ -0,0 +1,132 @@
<?php
namespace Vichan\Service\Media;
use Vichan\Data\ThumbGenerationResult;
use function Vichan\Functions\Fs\link_or_copy;
class GdThumbGenerator implements ThumbGenerator {
private const PHP81 = PHP_MAJOR_VERSION >= 8 && PHP_MINOR_VERSION >= 1;
private const MIME_TO_EXT = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'image/bmp' => 'bmp',
'image/avif' => 'avif'
];
private static function imageCreateFrom(string $file, string $mime): mixed {
switch ($mime) {
case 'image/jpeg':
return imagecreatefromjpeg($file);
case 'image/png':
return imagecreatefrompng($file);
case 'image/gif':
return imagecreatefromgif($file);
case 'image/webp':
return imagecreatefromwbmp($file);
case 'image/bmp':
return imagecreatefrombmp($file);
case 'image/avif':
return self::PHP81 ? imagecreatefromavif($file) : false;
}
return false;
}
public static function imageSaveTo(mixed $gd, string $file, string $mime) {
// Somebody should tune the quality and speed values...
switch ($mime) {
case 'image/jpeg':
return imagejpeg($gd, $file, 50);
case 'image/png':
return imagepng($gd, $file, 7);
case 'image/gif':
return imagegif($gd, $file);
case 'image/webp':
return imagewbmp($gd, $file, 50);
case 'image/bmp':
return imagebmp($gd, $file, true);
case 'image/avif':
return self::PHP81 ? imageavif($gd, $file, 30, 6) : false;
}
return false;
}
private static function createCanvas(string $mime, int $width, int $height) {
$gd = \imagecreatetruecolor($width, $height);
switch ($mime) {
case 'image/png':
imagecolortransparent($gd, imagecolorallocatealpha($gd, 0, 0, 0, 0));
imagesavealpha($gd, true);
imagealphablending($gd, false);
break;
case 'image/gif':
\imagecolortransparent($gd, \imagecolorallocatealpha($gd, 0, 0, 0, 0));
\imagesavealpha($gd, true);
break;
}
return $gd;
}
public function supportsMime(string $mime): bool {
$info = \gd_info();
return ($mime === 'image/jpeg' && $info['JPEG Support'])
|| ($mime === 'image/png' && $info['PNG Support'])
|| ($mime === 'image/gif' && $info['GIF Read Support'] && $info['GIF Create Support'])
|| ($mime === 'image/webp' && $info['WebP Support'])
|| $mime === 'image/bmp'
|| ($mime === 'image/avif' && self::PHP81 && $info['AVIF Support']);
}
public function generateThumb(
string $source_file_path,
string $source_file_mime,
string $preferred_out_file_path,
string $preferred_out_mime,
int $max_width,
int $max_height
): ThumbGenerationResult {
$gd = self::imageCreateFrom($source_file_path, $source_file_mime);
if ($gd === false) {
throw new \RuntimeException("Could not open '$source_file_path'");
}
$width = \imagesx($gd);
$height = \imagesy($gd);
if ($width <= $max_width && $height <= $max_height) {
$out_path = $preferred_out_file_path . '.' . self::MIME_TO_EXT[$source_file_mime];
if (!link_or_copy($source_file_path, $out_path)) {
throw new \RuntimeException("Could not link or copy '$source_file_path' to '$out_path'");
}
$res = new ThumbGenerationResult();
$res->thumb_file_path = $out_path;
$res->thumb_mime = $source_file_mime;
$res->is_thumb_file_temporary = false;
$res->width = $width;
$res->height = $height;
return $res;
} else {
$out_path = $preferred_out_file_path . '.' . self::MIME_TO_EXT[$preferred_out_mime];
$gd_other = self::createCanvas($preferred_out_mime, $max_width, $max_height);
\imagecopyresampled($gd_other, $gd, 0, 0, 0, 0, $max_width, $max_height, $width, $height);
if (!self::imageSaveTo($gd_other, $out_path, $preferred_out_mime)) {
throw new \RuntimeException("Could not create thumbnail file at '$out_path'");
}
$res = new ThumbGenerationResult();
$res->thumb_file_path = $out_path;
$res->thumb_mime = $preferred_out_mime;
$res->is_thumb_file_temporary = false;
$res->width = $max_width;
$res->height = $max_height;
return $res;
}
}
}

View file

@ -1,11 +0,0 @@
<?php
namespace Vichan\Service\Media;
interface ImageFormatReader {
/**
* @param string $file_path Image file path.
* @return array An array with width and height.
*/
public function getSizes(string $file_path): array;
}

View file

@ -0,0 +1,12 @@
<?php
namespace Vichan\Service\Media;
use Vichan\Data\ImageMetadataResult;
interface ImageMetadataReader {
/**
* @param string $file_path Image file path.
*/
public function getMetadata(string $file_path): ImageMetadataResult;
}

View file

@ -1,33 +0,0 @@
<?php
namespace Vichan\Service\Media;
class MagickImageFormatReader implements ImageFormatReader {
private string $prefix;
public static function createImageMagickReader(): MagickImageFormatReader {
return new self('');
}
public static function createGraphicsMagickReader(): MagickImageFormatReader {
return new self('gm ');
}
private function __construct(string $prefix) {
$this->prefix = $prefix;
}
public function getSizes(string $file_path): array {
$arg = escapeshellarg("$file_path[0]");
$ret_exec = shell_exec_error("{$this->prefix}identify -format \"%w %h\" $arg");
if (!\is_string($ret_exec)) {
throw new \RuntimeException("Error while executing identify");
}
$ret_match = \preg_match('/^(\d+) (\d+)$/', $ret_exec, $m);
if (!$ret_match) {
throw new \RuntimeException("Could not parse identify output");
}
return [ $m[1], $m[2] ];
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Vichan\Service\Media;
use Mimey\MimeTypes;
use Vichan\Data\ImageMetadataResult;
class MagickImageMetadataReader implements ImageMetadataReader {
private string $prefix;
private MimeTypes $mime_types;
public static function createImageMagickReader(MimeTypes $mime_types): MagickImageMetadataReader {
return new self('', $mime_types);
}
public static function createGraphicsMagickReader(MimeTypes $mime_types): MagickImageMetadataReader {
return new self('gm ', $mime_types);
}
private function __construct(string $prefix, MimeTypes $mime_types) {
$this->prefix = $prefix;
$this->mime_types = $mime_types;
}
public function getMetadata(string $file_path): ImageMetadataResult {
$arg = escapeshellarg("$file_path[0]");
$ret_exec = shell_exec_error("{$this->prefix}identify -format \"%w %h %m\" $arg");
if (!\is_string($ret_exec)) {
throw new \RuntimeException("Error while executing identify");
}
$ret_match = \preg_match('/^(\d+) (\d+) ([\w\d]+)$/', $ret_exec, $m);
if (!$ret_match) {
throw new \RuntimeException("Could not parse identify output");
}
$mime = $this->mime_types->getMimeType($m[3]) ?? 'application/octet-stream';
return new ImageMetadataResult($m[1], $m[2], $mime);
}
}

View file

@ -11,10 +11,19 @@ interface ThumbGenerator {
* Generates a thumbnail from the given file.
*
* @param string $source_file_path
* @param string $source_file_mime
* @param string $preferred_out_file_path
* @param string $preferred_out_mime
* @param int $max_width
* @param int $max_height
* @return ThumbGenerationResult
*/
public function generateThumb(string $source_file_path, string $preferred_out_file_path, int $max_width, int $max_height): ThumbGenerationResult;
public function generateThumb(
string $source_file_path,
string $source_file_mime,
string $preferred_out_file_path,
string $preferred_out_mime,
int $max_width,
int $max_height
): ThumbGenerationResult;
}

10
inc/functions/fs.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace Vichan\Functions\Fs;
function link_or_copy(string $from, string $to) {
if (!\link($from, $to)) {
return \copy($from, $to);
}
return true;
}