<?php
/**
* @class imageTools
* @brief Image manipulations
*
* Class to manipulate images. Some methods are based on https://dev.media-box.net/big/
*
* @package Clearbricks
* @subpackage Images
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
class imageTools
{
public $res; ///< resource: Image resource
public $memory_limit = null;
/**
* Constructor, no parameters.
*/
public function __construct()
{
if (!function_exists('imagegd2')) {
throw new Exception('GD is not installed');
}
$this->res = null;
}
/**
* Close
*
* Destroy image resource
*/
public function close()
{
if (!empty($this->res)) {
imagedestroy($this->res);
}
if ($this->memory_limit) {
ini_set('memory_limit', $this->memory_limit);
}
}
/**
* Load image
*
* Loads an image content in memory and set {@link $res} property.
*
* @param string $f Image file path
*/
public function loadImage($f)
{
if (!file_exists($f)) {
throw new Exception('Image doest not exists');
}
if (($info = @getimagesize($f)) !== false) {
$this->memoryAllocate(
$info[0],
$info[1],
$info['channels'] ?? 4
);
switch ($info[2]) {
case 3: // PNG
$this->res = @imagecreatefrompng($f);
if (!empty($this->res)) {
@imagealphablending($this->res, false);
@imagesavealpha($this->res, true);
}
break;
case 2: // JPEG
$this->res = @imagecreatefromjpeg($f);
break;
case 1: // GIF
$this->res = @imagecreatefromgif($f);
break;
case 18: // WEBP
if (function_exists('imagecreatefromwebp')) {
$this->res = @imagecreatefromwebp($f);
if (!empty($this->res)) {
@imagealphablending($this->res, false);
@imagesavealpha($this->res, true);
}
} else {
throw new Exception('WebP image format not supported');
}
break;
}
}
if (empty($this->res)) {
throw new Exception('Unable to load image');
}
}
/**
* Image width
*
* @return integer Image width
*/
public function getW()
{
return imagesx($this->res);
}
/**
* Image height
*
* @return integer Image height
*/
public function getH()
{
return imagesy($this->res);
}
public function memoryAllocate($w, $h, $bpp = 4)
{
$mem_used = function_exists('memory_get_usage') ? @memory_get_usage() : 4000000;
$mem_limit = @ini_get('memory_limit');
if ($mem_limit && trim((string) $mem_limit) === '-1' || !files::str2bytes($mem_limit)) {
// Cope with memory_limit set to -1 in PHP.ini
return;
}
if ($mem_used && $mem_limit) {
$mem_limit = files::str2bytes($mem_limit);
$mem_avail = $mem_limit - $mem_used - (512 * 1024);
$mem_needed = $w * $h * $bpp;
if ($mem_needed > $mem_avail) {
if (@ini_set('memory_limit', (string) ($mem_limit + $mem_needed + $mem_used)) === false) {
throw new Exception(__('Not enough memory to open image.'));
}
if (!$this->memory_limit) {
$this->memory_limit = $mem_limit;
}
}
}
}
/**
* Image output
*
* Returns image content in a file or as HTML output (with headers)
*
* @param string $type Image type (png or jpg)
* @param string|null $file Output file. If null, output will be echoed in STDOUT
* @param integer $qual JPEG image quality
*/
public function output($type = 'png', $file = null, $qual = 90)
{
if (!$file) {
header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
header('Pragma: no-cache');
switch (strtolower($type)) {
case 'png':
header('Content-type: image/png');
imagepng($this->res);
return true;
case 'jpeg':
case 'jpg':
header('Content-type: image/jpeg');
imagejpeg($this->res, null, $qual);
return true;
case 'wepb':
if (function_exists('imagewebp')) {
header('Content-type: image/webp');
imagewebp($this->res, null, $qual);
return true;
}
return false;
default:
return false;
}
} elseif (is_writable(dirname($file))) {
switch (strtolower($type)) {
case 'png':
return imagepng($this->res, $file);
case 'jpeg':
case 'jpg':
return imagejpeg($this->res, $file, $qual);
case 'webp':
if (function_exists('imagewebp')) {
return imagewebp($this->res, $file, $qual);
}
return false;
default:
return false;
}
}
return false;
}
/**
* Resize image
*
* @param mixed $WIDTH Image width (px or percent)
* @param mixed $HEIGHT Image height (px or percent)
* @param string $MODE Crop mode (force, crop, ratio)
* @param boolean $EXPAND Allow resize of image
*/
public function resize($WIDTH, $HEIGHT, $MODE = 'ratio', $EXPAND = false)
{
$_h = 0;
$_w = 0;
$imgWidth = $this->getW();
$imgHeight = $this->getH();
if (strpos((string) $WIDTH, '%', 0)) {
$WIDTH = $imgWidth * $WIDTH / 100;
}
if (strpos((string) $HEIGHT, '%', 0)) {
$HEIGHT = $imgHeight * $HEIGHT / 100;
}
$ratio = $imgWidth / $imgHeight;
// guess resize ($_w et $_h)
if ($MODE == 'ratio') {
$_w = 99999;
if ($HEIGHT > 0) {
$_h = $HEIGHT;
$_w = $_h * $ratio;
}
if ($WIDTH > 0 && $_w > $WIDTH) {
$_w = $WIDTH;
$_h = $_w / $ratio;
}
if (!$EXPAND && $_w > $imgWidth) {
$_w = $imgWidth;
$_h = $imgHeight;
}
} else {
// crop source image
$_w = $WIDTH;
$_h = $HEIGHT;
}
if ($MODE == 'force') {
if ($WIDTH > 0) {
$_w = $WIDTH;
} else {
$_w = $HEIGHT * $ratio;
}
if ($HEIGHT > 0) {
$_h = $HEIGHT;
} else {
$_h = $WIDTH / $ratio;
}
if (!$EXPAND && $_w > $imgWidth) {
$_w = $imgWidth;
$_h = $imgHeight;
}
$cropW = $imgWidth;
$cropH = $imgHeight;
$decalW = 0;
$decalH = 0;
} else {
// guess real viewport of image
$innerRatio = $_w / $_h;
if ($ratio >= $innerRatio) {
$cropH = $imgHeight;
$cropW = $imgHeight * $innerRatio;
$decalH = 0;
$decalW = ($imgWidth - $cropW) / 2;
} else {
$cropW = $imgWidth;
$cropH = $imgWidth / $innerRatio;
$decalW = 0;
$decalH = ($imgHeight - $cropH) / 2;
}
}
if ($_w < 1) {
$_w = 1;
}
if ($_h < 1) {
$_h = 1;
}
// convert float to int
settype($decalW, 'int');
settype($decalH, 'int');
settype($_w, 'int');
settype($_h, 'int');
settype($cropW, 'int');
settype($cropH, 'int');
# truecolor is 24 bit RGB, ie. 3 bytes per pixel.
$this->memoryAllocate($_w, $_h, 3);
$dest = imagecreatetruecolor($_w, $_h);
$fill = imagecolorallocate($dest, 128, 128, 128);
imagefill($dest, 0, 0, $fill);
@imagealphablending($dest, false);
@imagesavealpha($dest, true);
imagecopyresampled($dest, $this->res, 0, 0, $decalW, $decalH, $_w, $_h, $cropW, $cropH);
imagedestroy($this->res);
$this->res = $dest;
return true;
}
}