$characterSet
$characterSet :
Character set to use.
Repository: mosbth/cimage
Branch: master
Commit: b44be78f063c
Files: 178
Total size: 2.4 MB
Directory structure:
gitextract_499basvb/
├── .gitignore
├── .scrutinizer.yml
├── .travis.yml
├── CAsciiArt.php
├── CCache.php
├── CFastTrackCache.php
├── CHttpGet.php
├── CImage.php
├── CRemoteImage.php
├── CWhitelist.php
├── LICENSE.txt
├── README.md
├── REVISION.md
├── SECURITY.md
├── autoload.php
├── bin/
│ ├── cache.bash
│ └── create-img-single.bash
├── cache/
│ └── .gitignore
├── composer.json
├── defines.php
├── docker-compose.yaml
├── docs/
│ └── api/
│ ├── .htaccess
│ ├── classes/
│ │ ├── CAsciiArt.html
│ │ ├── CHttpGet.html
│ │ ├── CImage.html
│ │ ├── CImage_RemoteDownloadTest.html
│ │ ├── CRemoteImage.html
│ │ ├── CWhitelist.html
│ │ └── CWhitelistTest.html
│ ├── css/
│ │ ├── jquery.iviewer.css
│ │ ├── phpdocumentor-clean-icons/
│ │ │ ├── Read Me.txt
│ │ │ ├── lte-ie7.js
│ │ │ └── style.css
│ │ ├── prism.css
│ │ └── template.css
│ ├── files/
│ │ ├── CAsciiArt.html
│ │ ├── CAsciiArt.php.txt
│ │ ├── CHttpGet.html
│ │ ├── CHttpGet.php.txt
│ │ ├── CImage.html
│ │ ├── CImage.php.txt
│ │ ├── CRemoteImage.html
│ │ ├── CRemoteImage.php.txt
│ │ ├── CWhitelist.html
│ │ ├── CWhitelist.php.txt
│ │ ├── autoload.html
│ │ ├── autoload.php.txt
│ │ ├── test%2FCImage_RemoteDownloadTest.php.txt
│ │ ├── test%2FCWhitelistTest.php.txt
│ │ ├── test%2Fconfig.php.txt
│ │ ├── test.CImage_RemoteDownloadTest.html
│ │ ├── test.CWhitelistTest.html
│ │ ├── test.config.html
│ │ ├── webroot/
│ │ │ ├── img.php.txt
│ │ │ └── img_config.php.txt
│ │ ├── webroot%2Fcheck_system.php.txt
│ │ ├── webroot%2Fcompare%2Fcompare-test.php.txt
│ │ ├── webroot%2Fcompare%2Fcompare.php.txt
│ │ ├── webroot%2Fimg.php.txt
│ │ ├── webroot%2Fimg_config.php.txt
│ │ ├── webroot%2Fimg_header.php.txt
│ │ ├── webroot%2Fimgd.php.txt
│ │ ├── webroot%2Fimgp.php.txt
│ │ ├── webroot%2Fimgs.php.txt
│ │ ├── webroot%2Ftest%2Fconfig.php.txt
│ │ ├── webroot%2Ftest%2Ftemplate.php.txt
│ │ ├── webroot%2Ftest%2Ftest.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue29.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue36_aro.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue36_rb-ra-180.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue36_rb-ra-270.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue36_rb-ra-45.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue36_rb-ra-90.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue38.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue40.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue49.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue52-cf.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue52-stretch.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue52.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue58.php.txt
│ │ ├── webroot%2Ftest%2Ftest_issue60.php.txt
│ │ ├── webroot%2Ftest%2Ftest_option-crop.php.txt
│ │ ├── webroot%2Ftest%2Ftest_option-no-upscale.php.txt
│ │ ├── webroot%2Ftest%2Ftest_option-save-as.php.txt
│ │ ├── webroot.check_system.html
│ │ ├── webroot.compare.compare-test.html
│ │ ├── webroot.compare.compare.html
│ │ ├── webroot.img.html
│ │ ├── webroot.img_config.html
│ │ ├── webroot.img_header.html
│ │ ├── webroot.imgd.html
│ │ ├── webroot.imgp.html
│ │ ├── webroot.imgs.html
│ │ ├── webroot.test.config.html
│ │ ├── webroot.test.template.html
│ │ ├── webroot.test.test.html
│ │ ├── webroot.test.test_issue29.html
│ │ ├── webroot.test.test_issue36_aro.html
│ │ ├── webroot.test.test_issue36_rb-ra-180.html
│ │ ├── webroot.test.test_issue36_rb-ra-270.html
│ │ ├── webroot.test.test_issue36_rb-ra-45.html
│ │ ├── webroot.test.test_issue36_rb-ra-90.html
│ │ ├── webroot.test.test_issue38.html
│ │ ├── webroot.test.test_issue40.html
│ │ ├── webroot.test.test_issue49.html
│ │ ├── webroot.test.test_issue52-cf.html
│ │ ├── webroot.test.test_issue52-stretch.html
│ │ ├── webroot.test.test_issue52.html
│ │ ├── webroot.test.test_issue58.html
│ │ ├── webroot.test.test_issue60.html
│ │ ├── webroot.test.test_option-crop.html
│ │ ├── webroot.test.test_option-no-upscale.html
│ │ └── webroot.test.test_option-save-as.html
│ ├── font/
│ │ └── FontAwesome.otf
│ ├── graphs/
│ │ └── class.html
│ ├── images/
│ │ └── iviewer/
│ │ ├── grab.cur
│ │ └── hand.cur
│ ├── index.html
│ ├── js/
│ │ ├── html5.js
│ │ ├── jquery.dotdotdot-1.5.9.js
│ │ ├── jquery.iviewer.js
│ │ ├── jquery.mousewheel.js
│ │ └── jquery.smooth-scroll.js
│ ├── namespaces/
│ │ └── default.html
│ └── reports/
│ ├── deprecated.html
│ ├── errors.html
│ └── markers.html
├── functions.php
├── icc/
│ └── sRGB_IEC61966-2-1_black_scaled.icc
├── phpcs.xml
├── phpdoc.xml
├── phpunit.xml
├── test/
│ ├── CCacheTest.php
│ ├── CImageDummyTest.php
│ ├── CImageRemoteDownloadTest.php
│ ├── CImageSRGBTest.php
│ ├── CWhitelistTest.php
│ └── config.php
└── webroot/
├── check_system.php
├── compare/
│ ├── compare-test.php
│ ├── compare.php
│ └── issue117-PNG24.php
├── htaccess
├── img/
│ ├── car-gif
│ ├── car-jpg
│ ├── car-png
│ └── lena.tif
├── img.php
├── img_config.php
├── img_header.php
├── imgd.php
├── imgf.php
├── imgp.php
├── imgs.php
├── js/
│ └── cimage.js
├── test/
│ ├── config.php
│ ├── template.php
│ ├── test.php
│ ├── test_issue101-dummy.php
│ ├── test_issue29.php
│ ├── test_issue36_aro.php
│ ├── test_issue36_rb-ra-180.php
│ ├── test_issue36_rb-ra-270.php
│ ├── test_issue36_rb-ra-45.php
│ ├── test_issue36_rb-ra-90.php
│ ├── test_issue38.php
│ ├── test_issue40.php
│ ├── test_issue49.php
│ ├── test_issue52-cf.php
│ ├── test_issue52-stretch.php
│ ├── test_issue52.php
│ ├── test_issue58.php
│ ├── test_issue60.php
│ ├── test_issue85.php
│ ├── test_option-crop.php
│ ├── test_option-no-upscale.php
│ └── test_option-save-as.php
└── tests.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Cache files
cache/*
# Test
coverage/
coverage.clover
# Composer
composer.lock
vendor
# Build and test
build/
# Mac OS
.DS_Store
================================================
FILE: .scrutinizer.yml
================================================
imports:
- php
filter:
excluded_paths:
- test/
- webroot/check*
- webroot/imgd.php
- webroot/imgp.php
- webroot/imgs.php
- webroot/test/
checks:
php:
code_rating: true
duplication: true
tools:
# Copy/Paste Detector
php_cpd: true
# Metrics
php_pdepend: true
# Some Metrics + Bug Detection/Auto-Fixes
php_analyzer: true
php_code_sniffer:
config:
standard: "PSR2"
php_sim:
min_mass: 16 # Defaults to 16
php_mess_detector:
#config:
# ruleset: ../your-phpmd-ruleset/ruleset.xml
build:
tests:
override:
-
command: 'phpunit'
coverage:
file: 'coverage.clover'
format: 'php-clover'
================================================
FILE: .travis.yml
================================================
language: php
php:
- 5.4
- 5.5
- 5.6
- hhvm
- nightly
- "7.0"
sudo: false
git:
submodules: false
addons:
apt:
packages:
#- php-codesniffer
#- phpmd
#- shellcheck
matrix:
allow_failures:
- php: hhvm
- php: nightly
before_script:
# Create a build directory for output
# Store all files in your own bin
#- install --directory build/bin
#- export PATH=$PATH:$PWD/build/bin/
# Install validation tools
#- npm install -g htmlhint csslint jshint jscs jsonlint js-yaml html-minifier@0.8.0 clean-css uglify-js
# Install phpcs
#- curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar
#- install --mode 0755 phpcs.phar $PWD/build/bin/phpcs
# Install phpmd
#- wget -c http://static.phpmd.org/php/latest/phpmd.phar
#- install --mode 0755 phpmd.phar $PWD/build/bin/phpmd
script:
# Check versions of validation tools
#- node --version
#- npm --version
#- htmlhint --version
#- csslint --version
#- jscs --version
#- jshint --version
#- phpcs --version
#- phpmd --version
#- jsonlint --version
#- js-yaml --version
#- shellcheck --version
#- html-minifier --version
#- cleancss --version
#- uglifyjs --version
# Run validation & publish
#- make phpunit
- composer validate
- phpunit
#- make phpcs
notifications:
irc: "irc.freenode.org#dbwebb"
webhooks:
urls:
- https://webhooks.gitter.im/e/a89832db4f939e85ba97
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: never # options: [always|never|change] default: always
================================================
FILE: CAsciiArt.php
================================================
"#0XT|:,.' ",
'two' => "@%#*+=-:. ",
'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "
);
/**
* Current character set.
*/
private $characters = null;
/**
* Length of current character set.
*/
private $charCount = null;
/**
* Scale of the area to swap to a character.
*/
private $scale = null;
/**
* Strategy to calculate luminance.
*/
private $luminanceStrategy = null;
/**
* Constructor which sets default options.
*/
public function __construct()
{
$this->setOptions();
}
/**
* Add a custom character set.
*
* @param string $key for the character set.
* @param string $value for the character set.
*
* @return $this
*/
public function addCharacterSet($key, $value)
{
$this->characterSet[$key] = $value;
return $this;
}
/**
* Set options for processing, defaults are available.
*
* @param array $options to use as default settings.
*
* @return $this
*/
public function setOptions($options = array())
{
$default = array(
"characterSet" => 'two',
"scale" => 14,
"luminanceStrategy" => 3,
"customCharacterSet" => null,
);
$default = array_merge($default, $options);
if (!is_null($default['customCharacterSet'])) {
$this->addCharacterSet('custom', $default['customCharacterSet']);
$default['characterSet'] = 'custom';
}
$this->scale = $default['scale'];
$this->characters = $this->characterSet[$default['characterSet']];
$this->charCount = strlen($this->characters);
$this->luminanceStrategy = $default['luminanceStrategy'];
return $this;
}
/**
* Create an Ascii image from an image file.
*
* @param string $filename of the image to use.
*
* @return string $ascii with the ASCII image.
*/
public function createFromFile($filename)
{
$img = imagecreatefromstring(file_get_contents($filename));
list($width, $height) = getimagesize($filename);
$ascii = null;
$incY = $this->scale;
$incX = $this->scale / 2;
for ($y = 0; $y < $height - 1; $y += $incY) {
for ($x = 0; $x < $width - 1; $x += $incX) {
$toX = min($x + $this->scale / 2, $width - 1);
$toY = min($y + $this->scale, $height - 1);
$luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY);
$ascii .= $this->luminance2character($luminance);
}
$ascii .= PHP_EOL;
}
return $ascii;
}
/**
* Get the luminance from a region of an image using average color value.
*
* @param string $img the image.
* @param integer $x1 the area to get pixels from.
* @param integer $y1 the area to get pixels from.
* @param integer $x2 the area to get pixels from.
* @param integer $y2 the area to get pixels from.
*
* @return integer $luminance with a value between 0 and 100.
*/
public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2)
{
$numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1);
$luminance = 0;
for ($x = $x1; $x <= $x2; $x++) {
for ($y = $y1; $y <= $y2; $y++) {
$rgb = imagecolorat($img, $x, $y);
$red = (($rgb >> 16) & 0xFF);
$green = (($rgb >> 8) & 0xFF);
$blue = ($rgb & 0xFF);
$luminance += $this->getLuminance($red, $green, $blue);
}
}
return $luminance / $numPixels;
}
/**
* Calculate luminance value with different strategies.
*
* @param integer $red The color red.
* @param integer $green The color green.
* @param integer $blue The color blue.
*
* @return float $luminance with a value between 0 and 1.
*/
public function getLuminance($red, $green, $blue)
{
switch ($this->luminanceStrategy) {
case 1:
$luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255;
break;
case 2:
$luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255;
break;
case 3:
$luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255;
break;
case 0:
default:
$luminance = ($red + $green + $blue) / (255 * 3);
}
return $luminance;
}
/**
* Translate the luminance value to a character.
*
* @param string $position a value between 0-100 representing the
* luminance.
*
* @return string with the ascii character.
*/
public function luminance2character($luminance)
{
$position = (int) round($luminance * ($this->charCount - 1));
$char = $this->characters[$position];
return $char;
}
}
================================================
FILE: CCache.php
================================================
path = $path;
return $this;
}
/**
* Get the path to the cache subdir and try to create it if its not there.
*
* @param string $subdir name of subdir
* @param array $create default is to try to create the subdir
*
* @return string | boolean as real path to the subdir or
* false if it does not exists
*/
public function getPathToSubdir($subdir, $create = true)
{
$path = realpath($this->path . "/" . $subdir);
if (is_dir($path)) {
return $path;
}
if ($create && defined('WINDOWS2WSL')) {
// Special case to solve Windows 2 WSL integration
$path = $this->path . "/" . $subdir;
if (mkdir($path)) {
return realpath($path);
}
}
if ($create && is_writable($this->path)) {
$path = $this->path . "/" . $subdir;
if (mkdir($path)) {
return realpath($path);
}
}
return false;
}
/**
* Get status of the cache subdir.
*
* @param string $subdir name of subdir
*
* @return string with status
*/
public function getStatusOfSubdir($subdir)
{
$path = realpath($this->path . "/" . $subdir);
$exists = is_dir($path);
$res = $exists ? "exists" : "does not exist";
if ($exists) {
$res .= is_writable($path) ? ", writable" : ", not writable";
}
return $res;
}
/**
* Remove the cache subdir.
*
* @param string $subdir name of subdir
*
* @return null | boolean true if success else false, null if no operation
*/
public function removeSubdir($subdir)
{
$path = realpath($this->path . "/" . $subdir);
if (is_dir($path)) {
return rmdir($path);
}
return null;
}
}
================================================
FILE: CFastTrackCache.php
================================================
enabled = $enabled;
return $this;
}
/**
* Set the path to the cache dir which must exist.
*
* @param string $path to the cache dir.
*
* @throws Exception when $path is not a directory.
*
* @return $this
*/
public function setCacheDir($path)
{
if (!is_dir($path)) {
throw new Exception("Cachedir is not a directory.");
}
$this->path = rtrim($path, "/");
return $this;
}
/**
* Set the filename to store in cache, use the querystring to create that
* filename.
*
* @param array $clear items to clear in $_GET when creating the filename.
*
* @return string as filename created.
*/
public function setFilename($clear)
{
$query = $_GET;
// Remove parts from querystring that should not be part of filename
foreach ($clear as $value) {
unset($query[$value]);
}
arsort($query);
$queryAsString = http_build_query($query);
$this->filename = md5($queryAsString);
if (CIMAGE_DEBUG) {
$this->container["query-string"] = $queryAsString;
}
return $this->filename;
}
/**
* Add header items.
*
* @param string $header add this as header.
*
* @return $this
*/
public function addHeader($header)
{
$this->container["header"][] = $header;
return $this;
}
/**
* Add header items on output, these are not output when 304.
*
* @param string $header add this as header.
*
* @return $this
*/
public function addHeaderOnOutput($header)
{
$this->container["header-output"][] = $header;
return $this;
}
/**
* Set path to source image to.
*
* @param string $source path to source image file.
*
* @return $this
*/
public function setSource($source)
{
$this->container["source"] = $source;
return $this;
}
/**
* Set last modified of source image, use to check for 304.
*
* @param string $lastModified
*
* @return $this
*/
public function setLastModified($lastModified)
{
$this->container["last-modified"] = $lastModified;
return $this;
}
/**
* Get filename of cached item.
*
* @return string as filename.
*/
public function getFilename()
{
return $this->path . "/" . $this->filename;
}
/**
* Write current item to cache.
*
* @return boolean if cache file was written.
*/
public function writeToCache()
{
if (!$this->enabled) {
return false;
}
if (is_dir($this->path) && is_writable($this->path)) {
$filename = $this->getFilename();
return file_put_contents($filename, json_encode($this->container)) !== false;
}
return false;
}
/**
* Output current item from cache, if available.
*
* @return void
*/
public function output()
{
$filename = $this->getFilename();
if (!is_readable($filename)) {
return;
}
$item = json_decode(file_get_contents($filename), true);
if (!is_readable($item["source"])) {
return;
}
foreach ($item["header"] as $value) {
header($value);
}
if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])
&& strtotime($_SERVER["HTTP_IF_MODIFIED_SINCE"]) == $item["last-modified"]) {
header("HTTP/1.0 304 Not Modified");
if (CIMAGE_DEBUG) {
trace(__CLASS__ . " 304");
}
exit;
}
foreach ($item["header-output"] as $value) {
header($value);
}
if (CIMAGE_DEBUG) {
trace(__CLASS__ . " 200");
}
readfile($item["source"]);
exit;
}
}
================================================
FILE: CHttpGet.php
================================================
request['header'] = array();
}
/**
* Build an encoded url.
*
* @param string $baseUrl This is the original url which will be merged.
* @param string $merge Thse parts should be merged into the baseUrl,
* the format is as parse_url.
*
* @return string $url as the modified url.
*/
public function buildUrl($baseUrl, $merge)
{
$parts = parse_url($baseUrl);
$parts = array_merge($parts, $merge);
$url = $parts['scheme'];
$url .= "://";
$url .= $parts['host'];
$url .= isset($parts['port'])
? ":" . $parts['port']
: "" ;
$url .= $parts['path'];
return $url;
}
/**
* Set the url for the request.
*
* @param string $url
*
* @return $this
*/
public function setUrl($url)
{
$parts = parse_url($url);
$path = "";
if (isset($parts['path'])) {
$pathParts = explode('/', $parts['path']);
unset($pathParts[0]);
foreach ($pathParts as $value) {
$path .= "/" . rawurlencode($value);
}
}
$url = $this->buildUrl($url, array("path" => $path));
$this->request['url'] = $url;
return $this;
}
/**
* Set custom header field for the request.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function setHeader($field, $value)
{
$this->request['header'][] = "$field: $value";
return $this;
}
/**
* Set header fields for the request.
*
* @param string $field
* @param string $value
*
* @return $this
*/
public function parseHeader()
{
//$header = explode("\r\n", rtrim($this->response['headerRaw'], "\r\n"));
$rawHeaders = rtrim($this->response['headerRaw'], "\r\n");
# Handle multiple responses e.g. with redirections (proxies too)
$headerGroups = explode("\r\n\r\n", $rawHeaders);
# We're only interested in the last one
$header = explode("\r\n", end($headerGroups));
$output = array();
if ('HTTP' === substr($header[0], 0, 4)) {
list($output['version'], $output['status']) = explode(' ', $header[0]);
unset($header[0]);
}
foreach ($header as $entry) {
$pos = strpos($entry, ':');
$output[trim(substr($entry, 0, $pos))] = trim(substr($entry, $pos + 1));
}
$this->response['header'] = $output;
return $this;
}
/**
* Perform the request.
*
* @param boolean $debug set to true to dump headers.
*
* @throws Exception when curl fails to retrieve url.
*
* @return boolean
*/
public function doGet($debug = false)
{
$options = array(
CURLOPT_URL => $this->request['url'],
CURLOPT_HEADER => 1,
CURLOPT_HTTPHEADER => $this->request['header'],
CURLOPT_AUTOREFERER => true,
CURLOPT_RETURNTRANSFER => true,
CURLINFO_HEADER_OUT => $debug,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 2,
);
$ch = curl_init();
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
if (!$response) {
throw new Exception("Failed retrieving url, details follows: " . curl_error($ch));
}
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$this->response['headerRaw'] = substr($response, 0, $headerSize);
$this->response['body'] = substr($response, $headerSize);
$this->parseHeader();
if ($debug) {
$info = curl_getinfo($ch);
echo "Request header
", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } /** * Get HTTP code of response. * * @return integer as HTTP status code or null if not available. */ public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } /** * Get file modification time of response. * * @return int as timestamp. */ public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } /** * Get content type. * * @return string as the content type or null if not existing or invalid. */ public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : ''; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } /** * Get file modification time of response. * * @param mixed $default as default value (int seconds) if date is * missing in response header. * * @return int as timestamp or $default if Date is missing in * response header. */ public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } /** * Get max age of cachable item. * * @param mixed $default as default value if date is missing in response * header. * * @return int as timestamp or false if not available. */ public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { // max-age=2592000 $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } /** * Get body of response. * * @return string as body. */ public function getBody() { return $this->response['body']; } } ================================================ FILE: CImage.php ================================================ 0, 'green' => 0, 'blue' => 0, 'alpha' => null, );*/ /** * Default background color to use. * * @todo remake when upgrading to PHP 5.5 */ //private $bgColorDefault = self::BACKGROUND_COLOR; private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); /** * Background color to use, specified as part of options. */ private $bgColor; /** * Where to save the target file. */ private $saveFolder; /** * The working image object. */ private $image; /** * Image filename, may include subdirectory, relative from $imageFolder */ private $imageSrc; /** * Actual path to the image, $imageFolder . '/' . $imageSrc */ private $pathToImage; /** * File type for source image, as provided by getimagesize() */ private $fileType; /** * File extension to use when saving image. */ private $extension; /** * Output format, supports null (image) or json. */ private $outputFormat = null; /** * Do lossy output using external postprocessing tools. */ private $lossy = null; /** * Verbose mode to print out a trace and display the created image */ private $verbose = false; /** * Keep a log/trace on what happens */ private $log = array(); /** * Handle image as palette image */ private $palette; /** * Target filename, with path, to save resulting image in. */ private $cacheFileName; /** * Set a format to save image as, or null to use original format. */ private $saveAs; /** * Path to command for lossy optimize, for example pngquant. */ private $pngLossy; private $pngLossyCmd; /** * Path to command for filter optimize, for example optipng. */ private $pngFilter; private $pngFilterCmd; /** * Path to command for deflate optimize, for example pngout. */ private $pngDeflate; private $pngDeflateCmd; /** * Path to command to optimize jpeg images, for example jpegtran or null. */ private $jpegOptimize; private $jpegOptimizeCmd; /** * Image dimensions, calculated from loaded image. */ private $width; // Calculated from source image private $height; // Calculated from source image /** * New image dimensions, incoming as argument or calculated. */ private $newWidth; private $newWidthOrig; // Save original value private $newHeight; private $newHeightOrig; // Save original value /** * Change target height & width when different dpr, dpr 2 means double image dimensions. */ private $dpr = 1; /** * Always upscale images, even if they are smaller than target image. */ const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; /** * Array with details on how to crop, incoming as argument and calculated. */ public $crop; public $cropOrig; // Save original value /** * String with details on how to do image convolution. String * should map a key in the $convolvs array or be a string of * 11 float values separated by comma. The first nine builds * up the matrix, then divisor and last offset. */ private $convolve; /** * Custom convolution expressions, matrix 3x3, divisor and offset. */ private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); /** * Resize strategy to fill extra area with background color. * True or false. */ private $fillToFit; /** * To store value for option scale. */ private $scale; /** * To store value for option. */ private $rotateBefore; /** * To store value for option. */ private $rotateAfter; /** * To store value for option. */ private $autoRotate; /** * To store value for option. */ private $sharpen; /** * To store value for option. */ private $emboss; /** * To store value for option. */ private $blur; /** * Used with option area to set which parts of the image to use. */ private $offset; /** * Calculate target dimension for image when using fill-to-fit resize strategy. */ private $fillWidth; private $fillHeight; /** * Allow remote file download, default is to disallow remote file download. */ private $allowRemote = false; /** * Path to cache for remote download. */ private $remoteCache; /** * Pattern to recognize a remote file. */ //private $remotePattern = '#^[http|https]://#'; private $remotePattern = '#^https?://#'; /** * Use the cache if true, set to false to ignore the cached file. */ private $useCache = true; /** * Disable the fasttrackCacke to start with, inject an object to enable it. */ private $fastTrackCache = null; /* * Set whitelist for valid hostnames from where remote source can be * downloaded. */ private $remoteHostWhitelist = null; /* * Do verbose logging to file by setting this to a filename. */ private $verboseFileName = null; /* * Output to ascii can take som options as an array. */ private $asciiOptions = array(); /* * Use interlaced progressive mode for JPEG images. */ private $interlace = false; /* * Image copy strategy, defaults to RESAMPLE. */ const RESIZE = 1; const RESAMPLE = 2; private $copyStrategy = NULL; /** * Properties, the class is mutable and the method setOptions() * decides (partly) what properties are created. * * @todo Clean up these and check if and how they are used */ public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $attr; // Calculated from source image /** * Constructor, can take arguments to init the object. * * @param string $imageSrc filename which may contain subdirectory. * @param string $imageFolder path to root folder for images. * @param string $saveFolder path to folder where to save the new file or null to skip saving. * @param string $saveName name of target file when saveing. */ public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } /** * Inject object and use it, must be available as member. * * @param string $property to set as object. * @param object $object to set to property. * * @return $this */ public function injectDependency($property, $object) { if (!property_exists($this, $property)) { $this->raiseError("Injecting unknown property."); } $this->$property = $object; return $this; } /** * Set verbose mode. * * @param boolean $mode true or false to enable and disable verbose mode, * default is true. * * @return $this */ public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } /** * Set save folder, base folder for saving cache files. * * @todo clean up how $this->saveFolder is used in other methods. * * @param string $path where to store cached files. * * @return $this */ public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } /** * Use cache or not. * * @param boolean $use true or false to use cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Create and save a dummy image. Use dimensions as stated in * $this->newWidth, or $width or default to 100 (same for height. * * @param integer $width use specified width for image dimension. * @param integer $height use specified width for image dimension. * * @return $this */ public function createDummyImage($width = null, $height = null) { $this->newWidth = $this->newWidth ?: $width ?: 100; $this->newHeight = $this->newHeight ?: $height ?: 100; $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); return $this; } /** * Allow or disallow remote image download. * * @param boolean $allow true or false to enable and disable. * @param string $cache path to cache dir. * @param string $pattern to use to detect if its a remote file. * * @return $this */ public function setRemoteDownload($allow, $cache, $pattern = null) { $this->allowRemote = $allow; $this->remoteCache = $cache; $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern; $this->log( "Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern ); return $this; } /** * Check if the image resource is a remote file or not. * * @param string $src check if src is remote. * * @return boolean true if $src is a remote file, else false. */ public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return !!$remote; } /** * Set whitelist for valid hostnames from where remote source can be * downloaded. * * @param array $whitelist with regexp hostnames to allow download from. * * @return $this */ public function setRemoteHostWhitelist($whitelist = null) { $this->remoteHostWhitelist = $whitelist; $this->log( "Setting remote host whitelist to: " . (is_null($whitelist) ? "null" : print_r($whitelist, 1)) ); return $this; } /** * Check if the hostname for the remote image, is on a whitelist, * if the whitelist is defined. * * @param string $src the remote source. * * @return boolean true if hostname on $src is in the whitelist, else false. */ public function isRemoteSourceOnWhitelist($src) { if (is_null($this->remoteHostWhitelist)) { $this->log("Remote host on whitelist not configured - allowing."); return true; } $whitelist = new CWhitelist(); $hostname = parse_url($src, PHP_URL_HOST); $allow = $whitelist->check($hostname, $this->remoteHostWhitelist); $this->log( "Remote host is on whitelist: " . ($allow ? "true" : "false") ); return $allow; } /** * Check if file extension is valid as a file extension. * * @param string $extension of image file. * * @return $this */ private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } /** * Normalize the file extension. * * @param string $extension of image file or skip to use internal. * * @return string $extension as a normalized file extension. */ private function normalizeFileExtension($extension = "") { $extension = strtolower($extension ? $extension : $this->extension ?? ""); if ($extension == 'jpeg') { $extension = 'jpg'; } return $extension; } /** * Download a remote image and return path to its local copy. * * @param string $src remote path to image. * * @return string as path to downloaded remote source. */ public function downloadRemoteSource($src) { if (!$this->isRemoteSourceOnWhitelist($src)) { throw new Exception("Hostname is not on whitelist for remote sources."); } $remote = new CRemoteImage(); if (!is_writable($this->remoteCache)) { $this->log("The remote cache is not writable."); } $remote->setCache($this->remoteCache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item is in local cache: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } /** * Set source file to use as image source. * * @param string $src of image. * @param string $dir as optional base directory where images are. * * @return $this */ public function setSource($src, $dir = null) { if (!isset($src)) { $this->imageSrc = null; $this->pathToImage = null; return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $imageFolder = rtrim($dir, '/'); $this->pathToImage = $imageFolder . '/' . $this->imageSrc; return $this; } /** * Set target file. * * @param string $src of target image. * @param string $dir as optional base directory where images are stored. * Uses $this->saveFolder if null. * * @return $this */ public function setTarget($src = null, $dir = null) { if (!isset($src)) { $this->cacheFileName = null; return $this; } if (isset($dir)) { $this->saveFolder = rtrim($dir, '/'); } $this->cacheFileName = $this->saveFolder . '/' . $src; // Sanitize filename $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } /** * Get filename of target file. * * @return Boolean|String as filename of target or false if not set. */ public function getTarget() { return $this->cacheFileName; } /** * Set options to use when processing image. * * @param array $args used when processing image. * * @return $this */ public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( // Options for calculate dimensions 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), 'area' => null, //'0,0,0,0', 'upscale' => self::UPSCALE_DEFAULT, // Options for caching or using original 'useCache' => true, 'useOriginal' => true, // Pre-processing, before resizing is done 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, // General options 'bgColor' => null, // Post-processing, after resizing is done 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, 'interlace' => null, // Output format 'outputFormat' => null, 'dpr' => 1, // Postprocessing using external tools 'lossy' => null, ); // Convert crop settings from string to array if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } // Convert area settings from string to array if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } // Convert filter settings from array of string to array of array if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } // Merge default arguments with incoming and set properties. //$args = array_merge_recursive($defaults, $args); $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } // Save original values to enable re-calculating $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } /** * Map filter name to PHP filter and id. * * @param string $name the name of the filter. * * @return array with filter settings * @throws Exception */ private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } /** * Load image details from original image file. * * @param string $file the file to load or null to use $this->pathToImage. * * @return $this * @throws Exception */ public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; // Special case to solve Windows 2 WSL integration if (!defined('WINDOWS2WSL')) { is_readable($file) or $this->raiseError('Image file does not exist.'); } $info = list($this->width, $this->height, $this->fileType) = getimagesize($file); if (empty($info)) { // To support webp $this->fileType = false; if (function_exists("exif_imagetype")) { $this->fileType = exif_imagetype($file); if ($this->fileType === false) { if (function_exists("imagecreatefromwebp")) { $webp = imagecreatefromwebp($file); if ($webp !== false) { $this->width = imagesx($webp); $this->height = imagesy($webp); $this->fileType = IMG_WEBP; } } } } } if (!$this->fileType) { throw new Exception("Loading image details, the file doesn't seem to be a valid image."); } if ($this->verbose) { $this->log("Loading image details for: {$file}"); $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType})."); $this->log(" Image filesize: " . filesize($file) . " bytes."); $this->log(" Image mimetype: " . $this->getMimeType()); } return $this; } /** * Get mime type for image type. * * @return $this * @throws Exception */ protected function getMimeType() { if ($this->fileType === IMG_WEBP) { return "image/webp"; } return image_type_to_mime_type($this->fileType); } /** * Init new width and height and do some sanity checks on constraints, before any * processing can be done. * * @return $this * @throws Exception */ public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // width as % if ($this->newWidth && $this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } // height as % if ($this->newHeight && $this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); // width & height from aspect ratio if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } // Change width & height based on dpr if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } // Check values to be within domain is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Calculate new width and height of image, based on settings. * * @return $this */ public function calculateNewWidthAndHeight() { // Crop, use cropped width and height as base for calulations $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // Check if there is an area to crop off if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; // Check if crop is set if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } // Calculate new width and height if keeping aspect-ratio. if ($this->keepRatio) { $this->log("Keep aspect ratio."); // Crop-to-fit and both new width and height are set. if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { // Use newWidth and newHeigh as width/height, image should fit in box. $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { // Both new width and height are set. // Use newWidth and newHeigh as max width/height, image should not be larger. $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { // Use new width as max-width $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { // Use new height as max-hight $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } else { // Use existing width and height as new width and height. $this->newWidth = $width; $this->newHeight = $height; } // Get image dimensions for pre-resize image. if ($this->cropToFit || $this->fillToFit) { // Get relations of original & target image $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } elseif ($this->fillToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } // Crop, ensure to set new width and height if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } // Fill to fit, ensure to set new width and height /*if ($this->fillToFit) { $this->log("FillToFit."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); }*/ // No new height or width is set, use existing measures. $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Re-calculate image dimensions when original image dimension has changed. * * @return $this */ public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } /** * Set extension for filename to save as. * * @param string $saveas extension to save image as * * @return $this */ public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image as: " . $this->extension); return $this; } /** * Set JPEG quality to use when saving image * * @param int $quality as the quality to set. * * @return $this */ public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } /** * Set PNG compressen algorithm to use when saving image * * @param int $compress as the algorithm to use. * * @return $this */ public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } /** * Use original image if possible, check options which affects image processing. * * @param boolean $useOrig default is to use original if possible, else set to false. * * @return $this */ public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) && !$this->lossy ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } /** * Generate filename to save file in cache. * * @param string $base as optional basepath for storing file. * @param boolean $useSubdir use or skip the subdir part when creating the * filename. * @param string $prefix to add as part of filename * * @return $this */ public function generateFilename($base = null, $useSubdir = true, $prefix = null) { $filename = basename($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $lossy = $this->lossy ? "_l" : null; $interlace = $this->interlace ? "_i" : null; $saveAs = $this->normalizeFileExtension(); $saveAs = $saveAs ? "_$saveAs" : null; $copyStrat = null; if ($this->copyStrategy === self::RESIZE) { $copyStrat = "_rs"; } $width = $this->newWidth ? '_' . $this->newWidth : null; $height = $this->newHeight ? '_' . $this->newHeight : null; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= "-".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $optimize = $this->jpegOptimize ? 'o' : null; $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = null; if ($useSubdir === true) { $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $subdir .= '_'; } $file = $prefix . $subdir . $filename . $width . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $compress . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . $copyStrat . $lossy . $interlace . $saveAs; return $this->setTarget($file, $base); } /** * Use cached version of image, if possible. * * @param boolean $useCache is default true, set to false to avoid using cached object. * * @return $this */ public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } /** * Load image from disk. Try to load image without verbose error message, * if fail, load again and display error messages. * * @param string $src of image. * @param string $dir as base directory where images are. * * @return $this * */ public function load($src = null, $dir = null) { if (isset($src)) { $this->setSource($src, $dir); } $this->loadImageDetails(); if ($this->fileType === IMG_WEBP) { $this->image = imagecreatefromwebp($this->pathToImage); } else { $imageAsString = file_get_contents($this->pathToImage); $this->image = imagecreatefromstring($imageAsString); } if ($this->image === false) { throw new Exception("Could not load image."); } /* Removed v0.7.7 if (image_type_to_mime_type($this->fileType) == 'image/png') { $type = $this->getPngType(); $hasFewColors = imagecolorstotal($this->image); if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) { if ($this->verbose) { $this->log("Handle this image as a palette image."); } $this->palette = true; } } */ if ($this->verbose) { $this->log("### Image successfully loaded from file."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->colorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } /** * Get the type of PNG image. * * @param string $filename to use instead of default. * * @return int as the type of the png-image * */ public function getPngType($filename = null) { $filename = $filename ? $filename : $this->pathToImage; $pngType = ord(file_get_contents($filename, false, null, 25, 1)); if ($this->verbose) { $this->log("Checking png type of: " . $filename); $this->log($this->getPngTypeAsString($pngType)); } return $pngType; } /** * Get the type of PNG image as a verbose string. * * @param integer $type to use, default is to check the type. * @param string $filename to use instead of default. * * @return int as the type of the png-image * */ private function getPngTypeAsString($pngType = null, $filename = null) { if ($filename || !$pngType) { $pngType = $this->getPngType($filename); } $index = imagecolortransparent($this->image); $transparent = null; if ($index != -1) { $transparent = " (transparent)"; } switch ($pngType) { case self::PNG_GREYSCALE: $text = "PNG is type 0, Greyscale$transparent"; break; case self::PNG_RGB: $text = "PNG is type 2, RGB$transparent"; break; case self::PNG_RGB_PALETTE: $text = "PNG is type 3, RGB with palette$transparent"; break; case self::PNG_GREYSCALE_ALPHA: $text = "PNG is type 4, Greyscale with alpha channel"; break; case self::PNG_RGB_ALPHA: $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)"; break; default: $text = "PNG is UNKNOWN type, is it really a PNG image?"; } return $text; } /** * Calculate number of colors in an image. * * @param resource $im the image. * * @return int */ private function colorsTotal($im) { if (imageistruecolor($im)) { $this->log("Colors as true color."); $h = imagesy($im); $w = imagesx($im); $c = array(); for ($x=0; $x < $w; $x++) { for ($y=0; $y < $h; $y++) { @$c['c'.imagecolorat($im, $x, $y)]++; } } return count($c); } else { $this->log("Colors as palette."); return imagecolorstotal($im); } } /** * Preprocess image before rezising it. * * @return $this */ public function preResize() { $this->log("### Pre-process before resizing"); // Rotate image if ($this->rotateBefore) { $this->log("Rotating image."); $this->rotate($this->rotateBefore, $this->bgColor) ->reCalculateDimensions(); } // Auto-rotate image if ($this->autoRotate) { $this->log("Auto rotating image."); $this->rotateExif() ->reCalculateDimensions(); } // Scale the original image before starting if (isset($this->scale)) { $this->log("Scale by {$this->scale}%"); $newWidth = $this->width * $this->scale / 100; $newHeight = $this->height * $this->scale / 100; $img = $this->CreateImageKeepTransparency($newWidth, $newHeight); imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height); $this->image = $img; $this->width = $newWidth; $this->height = $newHeight; } return $this; } /** * Resize or resample the image while resizing. * * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE * * @return $this */ public function setCopyResizeStrategy($strategy) { $this->copyStrategy = $strategy; return $this; } /** * Resize and or crop the image. * * @return void */ public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) { if($this->copyStrategy == self::RESIZE) { $this->log("Copy by resize"); imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } else { $this->log("Copy by resample"); imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } } /** * Resize and or crop the image. * * @return $this */ public function resize() { $this->log("### Starting to Resize()"); $this->log("Upscale = '$this->upscale'"); // Only use a specified area of the image, $this->offset is defining the area to use if (isset($this->offset)) { $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}"); $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']); imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']); $this->image = $img; $this->width = $this->offset['width']; $this->height = $this->offset['height']; } if ($this->crop) { // Do as crop, take only part of image $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}"); $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']); imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']); $this->image = $img; $this->width = $this->crop['width']; $this->height = $this->crop['height']; } if (!$this->upscale) { // Consider rewriting the no-upscale code to fit within this if-statement, // likely to be more readable code. // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch } if ($this->cropToFit) { // Resize by crop to fit $this->log("Resizing using strategy - Crop to fit"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight)) { $this->log("Resizing - smaller image, do not upscale."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newWidth < $this->width) { $cropX = round(($this->width/2) - ($this->newWidth/2)); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } if ($this->newHeight < $this->height) { $cropY = round(($this->height/2) - ($this->newHeight/2)); } $this->log(" cwidth: $this->cropWidth"); $this->log(" cheight: $this->cropHeight"); $this->log(" nwidth: $this->newWidth"); $this->log(" nheight: $this->newHeight"); $this->log(" width: $this->width"); $this->log(" height: $this->height"); $this->log(" posX: $posX"); $this->log(" posY: $posY"); $this->log(" cropX: $cropX"); $this->log(" cropY: $cropY"); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); } else { $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $imgPreCrop = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif ($this->fillToFit) { // Resize by fill to fit $this->log("Resizing using strategy - Fill to fit"); $posX = 0; $posY = 0; $ratioOrig = $this->width / $this->height; $ratioNew = $this->newWidth / $this->newHeight; // Check ratio for landscape or portrait if ($ratioOrig < $ratioNew) { $posX = round(($this->newWidth - $this->fillWidth) / 2); } else { $posY = round(($this->newHeight - $this->fillHeight) / 2); } if (!$this->upscale && ($this->width < $this->newWidth && $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) { // Resize it $this->log("Resizing, new height and/or width"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); if (!$this->keepRatio) { $this->log("Resizing - stretch to fit selected."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width && $this->newHeight > $this->height) { $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); } elseif ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } elseif ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } return $this; } /** * Postprocess image after rezising image. * * @return $this */ public function postResize() { $this->log("### Post-process after resizing"); // Rotate image if ($this->rotateAfter) { $this->log("Rotating image."); $this->rotate($this->rotateAfter, $this->bgColor); } // Apply filters if (isset($this->filters) && is_array($this->filters)) { foreach ($this->filters as $filter) { $this->log("Applying filter {$filter['type']}."); switch ($filter['argc']) { case 0: imagefilter($this->image, $filter['type']); break; case 1: imagefilter($this->image, $filter['type'], $filter['arg1']); break; case 2: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']); break; case 3: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']); break; case 4: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']); break; } } } // Convert to palette image if ($this->palette) { $this->log("Converting to palette image."); $this->trueColorToPalette(); } // Blur the image if ($this->blur) { $this->log("Blur."); $this->blurImage(); } // Emboss the image if ($this->emboss) { $this->log("Emboss."); $this->embossImage(); } // Sharpen the image if ($this->sharpen) { $this->log("Sharpen."); $this->sharpenImage(); } // Custom convolution if ($this->convolve) { //$this->log("Convolve: " . $this->convolve); $this->imageConvolution(); } return $this; } /** * Rotate image using angle. * * @param float $angle to rotate image. * @param int $anglebgColor to fill image with if needed. * * @return $this */ public function rotate($angle, $bgColor) { $this->log("Rotate image " . $angle . " degrees with filler color."); $color = $this->getBackgroundColor(); $this->image = imagerotate($this->image, $angle, $color); $this->width = imagesx($this->image); $this->height = imagesy($this->image); $this->log("New image dimension width x height: " . $this->width . " x " . $this->height); return $this; } /** * Rotate image using information in EXIF. * * @return $this */ public function rotateExif() { if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) { $this->log("Autorotate ignored, EXIF not supported by this filetype."); return $this; } $exif = exif_read_data($this->pathToImage); if (!empty($exif['Orientation'])) { switch ($exif['Orientation']) { case 3: $this->log("Autorotate 180."); $this->rotate(180, $this->bgColor); break; case 6: $this->log("Autorotate -90."); $this->rotate(-90, $this->bgColor); break; case 8: $this->log("Autorotate 90."); $this->rotate(90, $this->bgColor); break; default: $this->log("Autorotate ignored, unknown value as orientation."); } } else { $this->log("Autorotate ignored, no orientation in EXIF."); } return $this; } /** * Convert true color image to palette image, keeping alpha. * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library * * @return void */ public function trueColorToPalette() { $img = imagecreatetruecolor($this->width, $this->height); $bga = imagecolorallocatealpha($img, 0, 0, 0, 127); imagecolortransparent($img, $bga); imagefill($img, 0, 0, $bga); imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height); imagetruecolortopalette($img, false, 255); imagesavealpha($img, true); if (imageistruecolor($this->image)) { $this->log("Matching colors with true color image."); imagecolormatch($this->image, $img); } $this->image = $img; } /** * Sharpen image using image convolution. * * @return $this */ public function sharpenImage() { $this->imageConvolution('sharpen'); return $this; } /** * Emboss image using image convolution. * * @return $this */ public function embossImage() { $this->imageConvolution('emboss'); return $this; } /** * Blur image using image convolution. * * @return $this */ public function blurImage() { $this->imageConvolution('blur'); return $this; } /** * Create convolve expression and return arguments for image convolution. * * @param string $expression constant string which evaluates to a list of * 11 numbers separated by komma or such a list. * * @return array as $matrix (3x3), $divisor and $offset */ public function createConvolveArguments($expression) { // Check of matching constant if (isset($this->convolves[$expression])) { $expression = $this->convolves[$expression]; } $part = explode(',', $expression); $this->log("Creating convolution expressen: $expression"); // Expect list of 11 numbers, split by , and build up arguments if (count($part) != 11) { throw new Exception( "Missmatch in argument convolve. Expected comma-separated string with 11 float values. Got $expression." ); } array_walk($part, function ($item, $key) { if (!is_numeric($item)) { throw new Exception("Argument to convolve expression should be float but is not."); } }); return array( array( array($part[0], $part[1], $part[2]), array($part[3], $part[4], $part[5]), array($part[6], $part[7], $part[8]), ), $part[9], $part[10], ); } /** * Add custom expressions (or overwrite existing) for image convolution. * * @param array $options Key value array with strings to be converted * to convolution expressions. * * @return $this */ public function addConvolveExpressions($options) { $this->convolves = array_merge($this->convolves, $options); return $this; } /** * Image convolution. * * @param string $options A string with 11 float separated by comma. * * @return $this */ public function imageConvolution($options = null) { // Use incoming options or use $this. $options = $options ? $options : $this->convolve; // Treat incoming as string, split by + $this->log("Convolution with '$options'"); $options = explode(":", $options); // Check each option if it matches constant value foreach ($options as $option) { list($matrix, $divisor, $offset) = $this->createConvolveArguments($option); imageconvolution($this->image, $matrix, $divisor, $offset); } return $this; } /** * Set default background color between 000000-FFFFFF or if using * alpha 00000000-FFFFFF7F. * * @param string $color as hex value. * * @return $this */ public function setDefaultBackgroundColor($color) { $this->log("Setting default background color to '$color'."); if (!(strlen($color) == 6 || strlen($color) == 8)) { throw new Exception( "Background color needs a hex value of 6 or 8 digits. 000000-FFFFFF or 00000000-FFFFFF7F. Current value was: '$color'." ); } $red = hexdec(substr($color, 0, 2)); $green = hexdec(substr($color, 2, 2)); $blue = hexdec(substr($color, 4, 2)); $alpha = (strlen($color) == 8) ? hexdec(substr($color, 6, 2)) : null; if (($red < 0 || $red > 255) || ($green < 0 || $green > 255) || ($blue < 0 || $blue > 255) || ($alpha < 0 || $alpha > 127) ) { throw new Exception( "Background color out of range. Red, green blue should be 00-FF and alpha should be 00-7F. Current value was: '$color'." ); } $this->bgColor = strtolower($color); $this->bgColorDefault = array( 'red' => $red, 'green' => $green, 'blue' => $blue, 'alpha' => $alpha ); return $this; } /** * Get the background color. * * @param resource $img the image to work with or null if using $this->image. * * @return color value or null if no background color is set. */ private function getBackgroundColor($img = null) { $img = isset($img) ? $img : $this->image; if ($this->bgColorDefault) { $red = $this->bgColorDefault['red']; $green = $this->bgColorDefault['green']; $blue = $this->bgColorDefault['blue']; $alpha = $this->bgColorDefault['alpha']; if ($alpha) { $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha); } else { $color = imagecolorallocate($img, $red, $green, $blue); } return $color; } else { return 0; } } /** * Create a image and keep transparency for png and gifs. * * @param int $width of the new image. * @param int $height of the new image. * * @return image resource. */ private function createImageKeepTransparency($width, $height) { $this->log("Creating a new working image width={$width}px, height={$height}px."); $img = imagecreatetruecolor($width, $height); imagealphablending($img, false); imagesavealpha($img, true); $index = $this->image ? imagecolortransparent($this->image) : -1; if ($index != -1) { imagealphablending($img, true); $transparent = imagecolorsforindex($this->image, $index); $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']); imagefill($img, 0, 0, $color); $index = imagecolortransparent($img, $color); $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index"); } elseif ($this->bgColorDefault) { $color = $this->getBackgroundColor($img); imagefill($img, 0, 0, $color); $this->Log("Filling image with background color."); } return $img; } /** * Set optimizing and post-processing options. * * @param array $options with config for postprocessing with external tools. * * @return $this */ public function setPostProcessingOptions($options) { if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) { $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd']; } else { $this->jpegOptimizeCmd = null; } if (array_key_exists("png_lossy", $options) && $options['png_lossy'] !== false) { $this->pngLossy = $options['png_lossy']; $this->pngLossyCmd = $options['png_lossy_cmd']; } else { $this->pngLossyCmd = null; } if (isset($options['png_filter']) && $options['png_filter']) { $this->pngFilterCmd = $options['png_filter_cmd']; } else { $this->pngFilterCmd = null; } if (isset($options['png_deflate']) && $options['png_deflate']) { $this->pngDeflateCmd = $options['png_deflate_cmd']; } else { $this->pngDeflateCmd = null; } return $this; } /** * Find out the type (file extension) for the image to be saved. * * @return string as image extension. */ protected function getTargetImageExtension() { // switch on mimetype if (isset($this->extension)) { return strtolower($this->extension); } elseif ($this->fileType === IMG_WEBP) { return "webp"; } return substr(image_type_to_extension($this->fileType), 1); } /** * Save image. * * @param string $src as target filename. * @param string $base as base directory where to store images. * @param boolean $overwrite or not, default to always overwrite file. * * @return $this or false if no folder is set. */ public function save($src = null, $base = null, $overwrite = true) { if (isset($src)) { $this->setTarget($src, $base); } if ($overwrite === false && is_file($this->cacheFileName)) { $this->Log("Not overwriting file since its already exists and \$overwrite if false."); return; } if (!defined("WINDOWS2WSL")) { is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); } $type = $this->getTargetImageExtension(); $this->Log("Saving image as " . $type); switch($type) { case 'jpeg': case 'jpg': // Set as interlaced progressive JPEG if ($this->interlace) { $this->Log("Set JPEG image to be interlaced."); $res = imageinterlace($this->image, true); } $this->Log("Saving image as JPEG to cache using quality = {$this->quality}."); imagejpeg($this->image, $this->cacheFileName, $this->quality); // Use JPEG optimize if defined if ($this->jpegOptimizeCmd) { if ($this->verbose) { clearstatcache(); $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->log($cmd); $this->log($res); } break; case 'gif': $this->Log("Saving image as GIF to cache."); imagegif($this->image, $this->cacheFileName); break; case 'webp': $this->Log("Saving image as WEBP to cache using quality = {$this->quality}."); imagewebp($this->image, $this->cacheFileName, $this->quality); break; case 'png': default: $this->Log("Saving image as PNG to cache using compression = {$this->compress}."); // Turn off alpha blending and set alpha flag imagealphablending($this->image, false); imagesavealpha($this->image, true); imagepng($this->image, $this->cacheFileName, $this->compress); // Use external program to process lossy PNG, if defined $lossyEnabled = $this->pngLossy === true; $lossySoftEnabled = $this->pngLossy === null; $lossyActiveEnabled = $this->lossy === true; if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) { if ($this->verbose) { clearstatcache(); $this->log("Lossy enabled: $lossyEnabled"); $this->log("Lossy soft enabled: $lossySoftEnabled"); $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } // Use external program to filter PNG, if defined if ($this->pngFilterCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngFilterCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } // Use external program to deflate PNG, if defined if ($this->pngDeflateCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngDeflateCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } break; } if ($this->verbose) { clearstatcache(); $this->log("Saved image to cache."); $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } /** * Convert image from one colorpsace/color profile to sRGB without * color profile. * * @param string $src of image. * @param string $dir as base directory where images are. * @param string $cache as base directory where to store images. * @param string $iccFile filename of colorprofile. * @param boolean $useCache or not, default to always use cache. * * @return string | boolean false if no conversion else the converted * filename. */ public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true) { if ($this->verbose) { $this->log("# Converting image to sRGB colorspace."); } if (!class_exists("Imagick")) { $this->log(" Ignoring since Imagemagick is not installed."); return false; } // Prepare $this->setSaveFolder($cache) ->setSource($src, $dir) ->generateFilename(null, false, 'srgb_'); // Check if the cached version is accurate. if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { $this->log(" Using cached version: " . $this->cacheFileName); return $this->cacheFileName; } } // Only covert if cachedir is writable if (is_writable($this->saveFolder)) { // Load file and check if conversion is needed $image = new Imagick($this->pathToImage); $colorspace = $image->getImageColorspace(); $this->log(" Current colorspace: " . $colorspace); $profiles = $image->getImageProfiles('*', false); $hasICCProfile = (array_search('icc', $profiles) !== false); $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO")); if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) { $this->log(" Converting to sRGB."); $sRGBicc = file_get_contents($iccFile); $image->profileImage('icc', $sRGBicc); $image->transformImageColorspace(Imagick::COLORSPACE_SRGB); $image->writeImage($this->cacheFileName); return $this->cacheFileName; } } return false; } /** * Create a hard link, as an alias, to the cached file. * * @param string $alias where to store the link, * filename without extension. * * @return $this */ public function linkToCacheFile($alias) { if ($alias === null) { $this->log("Ignore creating alias."); return $this; } if (is_readable($alias)) { unlink($alias); } $res = link($this->cacheFileName, $alias); if ($res) { $this->log("Created an alias as: $alias"); } else { $this->log("Failed to create the alias: $alias"); } return $this; } /** * Add HTTP header for output together with image. * * @param string $type the header type such as "Cache-Control" * @param string $value the value to use * * @return void */ public function addHTTPHeader($type, $value) { $this->HTTPHeader[$type] = $value; } /** * Output image to browser using caching. * * @param string $file to read and output, default is to * use $this->cacheFileName * @param string $format set to json to output file as json * object with details * * @return void */ public function output($file = null, $format = null) { if (is_null($file)) { $file = $this->cacheFileName; } if (is_null($format)) { $format = $this->outputFormat; } $this->log("### Output"); $this->log("Output format is: $format"); if (!$this->verbose && $format == 'json') { header('Content-type: application/json'); echo $this->json($file); exit; } elseif ($format == 'ascii') { header('Content-type: text/plain'); echo $this->ascii($file); exit; } $this->log("Outputting image: $file"); // Get image modification time clearstatcache(); $lastModified = filemtime($file); $lastModifiedFormat = "D, d M Y H:i:s"; $gmdate = gmdate($lastModifiedFormat, $lastModified); if (!$this->verbose) { $header = "Last-Modified: $gmdate GMT"; header($header); $this->fastTrackCache->addHeader($header); $this->fastTrackCache->setLastModified($lastModified); } foreach ($this->HTTPHeader as $key => $val) { $header = "$key: $val"; header($header); $this->fastTrackCache->addHeader($header); } if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) { if ($this->verbose) { $this->log("304 not modified"); $this->verboseOutput(); exit; } header("HTTP/1.0 304 Not Modified"); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 304"); } } else { $this->loadImageDetails($file); $mime = $this->getMimeType(); $size = filesize($file); if ($this->verbose) { $this->log("Last-Modified: " . $gmdate . " GMT"); $this->log("Content-type: " . $mime); $this->log("Content-length: " . $size); $this->verboseOutput(); if (is_null($this->verboseFileName)) { exit; } } $header = "Content-type: $mime"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $header = "Content-length: $size"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $this->fastTrackCache->setSource($file); $this->fastTrackCache->writeToCache(); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 200"); } readfile($file); } exit; } /** * Create a JSON object from the image details. * * @param string $file the file to output. * * @return string json-encoded representation of the image. */ public function json($file = null) { $file = $file ? $file : $this->cacheFileName; $details = array(); clearstatcache(); $details['src'] = $this->imageSrc; $lastModified = filemtime($this->pathToImage); $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $details['cache'] = basename($this->cacheFileName ?? ""); $lastModified = filemtime($this->cacheFileName ?? ""); $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $this->load($file); $details['filename'] = basename($file ?? ""); $details['mimeType'] = $this->getMimeType($this->fileType); $details['width'] = $this->width; $details['height'] = $this->height; $details['aspectRatio'] = round($this->width / $this->height, 3); $details['size'] = filesize($file ?? ""); $details['colors'] = $this->colorsTotal($this->image); $details['includedFiles'] = count(get_included_files()); $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ; $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB"; $details['memoryLimit'] = ini_get('memory_limit'); if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s"; } if ($details['mimeType'] == 'image/png') { $details['pngType'] = $this->getPngTypeAsString(null, $file); } $options = null; if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; } return json_encode($details, $options); } /** * Set options for creating ascii version of image. * * @param array $options empty to use default or set options to change. * * @return void. */ public function setAsciiOptions($options = array()) { $this->asciiOptions = $options; } /** * Create an ASCII version from the image details. * * @param string $file the file to output. * * @return string ASCII representation of the image. */ public function ascii($file = null) { $file = $file ? $file : $this->cacheFileName; $asciiArt = new CAsciiArt(); $asciiArt->setOptions($this->asciiOptions); return $asciiArt->createFromFile($file); } /** * Log an event if verbose mode. * * @param string $message to log. * * @return this */ public function log($message) { if ($this->verbose) { $this->log[] = $message; } return $this; } /** * Do verbose output to a file. * * @param string $fileName where to write the verbose output. * * @return void */ public function setVerboseToFile($fileName) { $this->log("Setting verbose output to file."); $this->verboseFileName = $fileName; } /** * Do verbose output and print out the log and the actual images. * * @return void */ private function verboseOutput() { $log = null; $this->log("### Summary of verbose log"); $this->log("As JSON: \n" . $this->json()); $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M"); $this->log("Memory limit: " . ini_get('memory_limit')); $included = get_included_files(); $this->log("Included files: " . count($included)); foreach ($this->log as $val) { if (is_array($val)) { foreach ($val as $val1) { $log .= htmlentities($val1) . '
{$log}
EOD;
}
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
}
================================================
FILE: CRemoteImage.php
================================================
status;
}
/**
* Get JSON details for cache item.
*
* @return array with json details on cache.
*/
public function getDetails()
{
return $this->cache;
}
/**
* Set the path to the cache directory.
*
* @param boolean $use true to use the cache and false to ignore cache.
*
* @return $this
*/
public function setCache($path)
{
$this->saveFolder = rtrim($path, "/") . "/";
return $this;
}
/**
* Check if cache is writable or throw exception.
*
* @return $this
*
* @throws Exception if cahce folder is not writable.
*/
public function isCacheWritable()
{
if (!is_writable($this->saveFolder)) {
throw new Exception("Cache folder is not writable for downloaded files.");
}
return $this;
}
/**
* Decide if the cache should be used or not before trying to download
* a remote file.
*
* @param boolean $use true to use the cache and false to ignore cache.
*
* @return $this
*/
public function useCache($use = true)
{
$this->useCache = $use;
return $this;
}
/**
* Set header fields.
*
* @return $this
*/
public function setHeaderFields()
{
$cimageVersion = "CImage";
if (defined("CIMAGE_USER_AGENT")) {
$cimageVersion = CIMAGE_USER_AGENT;
}
$this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)");
$this->http->setHeader("Accept", "image/jpeg,image/png,image/gif");
if ($this->useCache) {
$this->http->setHeader("Cache-Control", "max-age=0");
} else {
$this->http->setHeader("Cache-Control", "no-cache");
$this->http->setHeader("Pragma", "no-cache");
}
}
/**
* Save downloaded resource to cache.
*
* @return string as path to saved file or false if not saved.
*/
public function save()
{
$this->cache = array();
$date = $this->http->getDate(time());
$maxAge = $this->http->getMaxAge($this->defaultMaxAge);
$lastModified = $this->http->getLastModified();
$type = $this->http->getContentType();
$this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date);
$this->cache['Max-Age'] = $maxAge;
$this->cache['Content-Type'] = $type;
$this->cache['Url'] = $this->url;
if ($lastModified) {
$this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified);
}
// Save only if body is a valid image
$body = $this->http->getBody();
$img = imagecreatefromstring($body);
if ($img !== false) {
file_put_contents($this->fileName, $body);
file_put_contents($this->fileJson, json_encode($this->cache));
return $this->fileName;
}
return false;
}
/**
* Got a 304 and updates cache with new age.
*
* @return string as path to cached file.
*/
public function updateCacheDetails()
{
$date = $this->http->getDate(time());
$maxAge = $this->http->getMaxAge($this->defaultMaxAge);
$lastModified = $this->http->getLastModified();
$this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date);
$this->cache['Max-Age'] = $maxAge;
if ($lastModified) {
$this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified);
}
file_put_contents($this->fileJson, json_encode($this->cache));
return $this->fileName;
}
/**
* Download a remote file and keep a cache of downloaded files.
*
* @param string $url a remote url.
*
* @throws Exception when status code does not match 200 or 304.
*
* @return string as path to downloaded file or false if failed.
*/
public function download($url)
{
$this->http = new CHttpGet();
$this->url = $url;
// First check if the cache is valid and can be used
$this->loadCacheDetails();
if ($this->useCache) {
$src = $this->getCachedSource();
if ($src) {
$this->status = 1;
return $src;
}
}
// Do a HTTP request to download item
$this->setHeaderFields();
$this->http->setUrl($this->url);
$this->http->doGet();
$this->status = $this->http->getStatus();
if ($this->status === 200) {
$this->isCacheWritable();
return $this->save();
} elseif ($this->status === 304) {
$this->isCacheWritable();
return $this->updateCacheDetails();
}
throw new Exception("Unknown statuscode when downloading remote image: " . $this->status);
}
/**
* Get the path to the cached image file if the cache is valid.
*
* @return $this
*/
public function loadCacheDetails()
{
$cacheFile = md5($this->url);
$this->fileName = $this->saveFolder . $cacheFile;
$this->fileJson = $this->fileName . ".json";
if (is_readable($this->fileJson)) {
$this->cache = json_decode(file_get_contents($this->fileJson), true);
}
}
/**
* Get the path to the cached image file if the cache is valid.
*
* @return string as the path ot the image file or false if no cache.
*/
public function getCachedSource()
{
$imageExists = is_readable($this->fileName);
// Is cache valid?
$date = strtotime($this->cache['Date']);
$maxAge = $this->cache['Max-Age'];
$now = time();
if ($imageExists && $date + $maxAge > $now) {
return $this->fileName;
}
// Prepare for a 304 if available
if ($imageExists && isset($this->cache['Last-Modified'])) {
$this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']);
}
return false;
}
}
================================================
FILE: CWhitelist.php
================================================
whitelist = $whitelist;
return $this;
}
/**
* Check if item exists in the whitelist.
*
* @param string $item string to check.
* @param array $whitelist optional with all valid options, default is null.
*
* @return boolean true if item is in whitelist, else false.
*/
public function check($item, $whitelist = null)
{
if ($whitelist !== null) {
$this->set($whitelist);
}
if (empty($item) or empty($this->whitelist)) {
return false;
}
foreach ($this->whitelist as $regexp) {
if (preg_match("#$regexp#", $item)) {
return true;
}
}
return false;
}
}
================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)
Copyright (c) 2012 - 2016 Mikael Roos, https://mikaelroos.se, mos@dbwebb.se
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
Image conversion on the fly using PHP
=====================================
[](https://gitter.im/mosbth/cimage?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
About
-------------------------------------
`CImage` is a PHP class enabling resizing of images through scaling, cropping and filtering effects -- using PHP GD. The script `img.php` uses `CImage` to enable server-side image processing utilizing caching and optimization of the processed images.
Server-side image processing is a most useful tool for any web developer, `img.php` has an easy to use interface and its powerful when you integrate it with your website. Using it might decrease the time and effort for managing images and it might improve your workflow for creating content for websites.
This software is free and open source, licensed according MIT.
Documentation
--------------------------------------
Read full documentation at:
Lets say you have a larger image and you want to make a smaller thumbnail of it with a size of 80x80 pixels. You simply take the image and add constraints on `width`, `height` and you use the resize strategy `crop-to-fit` to crops out the parts of the image that does not fit.
To produce such a thumbnail, create a link like this:
> `img.php?src=kodim04.png&width=80&height=80&crop-to-fit`
### Slightly complexer use case
Perhaps you got an image from a friend. The image was taken with the iPhone and thus rotated.
The original image is looking like this one, scaled down to a width of 250 pixels.
So, you need to rotate it and crop off some parts to make it intresting.
To show it off, I'll auto-rotate the image based on its EXIF-information, I will crop it to a thumbnail of 100x100 pixels and add a filter to make it greyscale finishing up with a sharpen effect. Just for the show I'll rotate the image 25 degrees - do not ask me why.
Lets call this *the URL-Photoshopper*. This is how the magic looks like.
> `img.php?src=issue36/me-270.jpg&w=100&h=100&cf&aro`
> `&rb=-25&a=8,30,30,38&f=grayscale&convolve=sharpen-alt`
For myself, I use `img.php` to put up all images on my website, it gives me the power of affecting the resulting images - without opening up a photo-editing application.
Get going quickly
--------------------------------------
### Check out the test page
Try it out by pointing your browser to the test file `webroot/test/test.php`. It will show some example images and you can review how they are created.
### Process your first image
Try it yourself by opening up an image in your browser. Start with
> `webroot/img.php?src=kodim04.png`
and try to resize it to a thumbnail by adding the options
> `&width=100&height=100&crop-to-fit`
### What does "processing the image" involves?
Add `&verbose` to the link to get a verbose output of what is happens during image processing. This is useful for developers and those who seek a deeper understanding on how it works behind the scene.
### Check your system
Open up `webroot/check_system.php` if you are uncertain that your system has the right extensions loaded.
### How does it work?
Review the settings in `webroot/img_config.php` and check out `webroot/img.php` on how it uses `CImage`.
The programatic flow, just to get you oriented in the environment, is.
1. Start in `img.php`.
2. `img.php` reads configuration details from `img_config.php` (if the config-file is available).
3. `img.php` reads and processes incoming `$_GET` arguments to prepare using `CImage`.
4. `img.php` uses `CImage`.
5. `CImage` processes, caches and outputs the image according to how its used.
Read on to learn more on how to use `img.php`.
Basic usage
--------------------------------------
### Select the source
Open an image through `img.php` by using its `src` attribute.
> `img.php?src=kodim13.png`
It looks like this.
All images are stored in a directory structure and you access them as:
> `?src=dir1/dir2/image.png`
### Resize using constraints on width and height
Create a thumbnail of the image by applying constraints on width and height, or one of them.
| `&width=150` | `&height=150` | `&w=150&h=150` |
|---------------------|---------------------|---------------------|
|
|
|
|
By setting `width`, `height` or both, the image gets resized to be *not larger* than the defined dimensions *and* keeping its original aspect ratio.
Think of the constraints as a imaginary box where the image should fit. With `width=150` and `height=150` the box would have the dimension of 150x150px. A landscape image would fit in that box and its width would be 150px and its height depending on the aspect ratio, but for sure less than 150px. A portrait image would fit with a height of 150px and the width depending on the aspect ratio, but surely less than 150px.
### Resize to fit a certain dimension
Creating a thumbnail with a certain dimension of width and height, usually involves stretching or cropping the image to fit in the selected dimensions. Here is how you create a image that has the exact dimensions of 300x150 pixels, by either *stretching*, *cropping* or *fill to fit*.
| What | The image |
|---------------------|---------------------|
| **Original.** The original image resized with a max width and max height.
|
| **Stretch.** Stretch the image so that the resulting image has the defined width and height.
|
| **Crop to fit.** Keep the aspect ratio and crop out the parts of the image that does not fit.
|
| **Fill to fit.** Keep the aspect ratio and fill then blank space with a background color.
|
Learn to crop your images, creative cropping can make wonderful images from appearingly useless originals.
Stretching might work, like in the above example where you can not really notice that the image is stretched. But usually, stretching is not that a good option since it distorts the ratio. Stretching a face may not turn out particularly well.
Fill to fit is useful when you have some image that must fit in a certain dimension and stretching nor cropping can do it. Carefully choose the background color to make a good resulting image. Choose the same background color as your website and no one will notice.
### List of parameters
`img.php` supports a lot of parameters. Combine the parameters to get the desired behavior and resulting image. For example, take the original image, resize it using width, aspect-ratio and crop-to-fit, apply a sharpen effect, save the image as JPEG using quality 30.
> `img.php?src=kodim13.png&w=600&aspect-ratio=4`
> `&crop-to-fit&sharpen&save-as=jpg&q=30`
Here is a list of all parameters that you can use together with `img.php`, grouped by its basic intent of usage.
#### Mandatory options and debugging
Option `src` is the only mandatory option. The options in this section is useful for debugging or deciding what version of the target image is used.
| Parameter | Explained |
|----------------|----------------------------------------------|
| `src` | Source image to use, mandatory. `src=img.png` or with subdirectory `src=dir/img.png`. |
| `nc, no-cache` | Do not use the cached version, do all image processing and save a new image to cache. |
| `so, skip-original`| Skip using the original image, always process image, create and use a cached version of the original image. |
| `v, verbose` | Do verbose output and print out a log what happens. Good for debugging, analyzing the process and inspecting how the image is being processed. |
| `json` | Output a JSON-representation of the image, useful for testing or optimizing when one wants to know the image dimensions, before using it. |
| `pwd, password` | Use password to protect unauthorized usage. |
#### Options for deciding width and height of target image
These options are all affecting the final dimensions, width and height, of the resulting image.
| Parameter | Explained |
|----------------|----------------------------------------------|
| `h, height` | `h=200` sets the width to be to max 200px. `h=25%` sets the height to max 25% of its original height. |
| `w, width` | `w=200` sets the height to be max 200px. `w=100%` sets the width to max 100% of its original width. |
| `ar, aspect-ratio` | Control target aspect ratio. Use together with either height or width or alone to base calculations on original image dimensions. This setting is used to calculate the resulting dimension for the image. `w=160&aspect-ratio=1.6` results in a height of 100px. Use `ar=!1.6` to inverse the ratio, useful for portrait images, compared to landscape images. |
| `dpr, device-pixel-ratio` | Default value is 1, set to 2 when you are delivering the image to a high density screen, `dpr=2` or `dpr=1.4`. Its a easy way to say the image should have larger dimensions. The resulting image will be twice as large (or 1.4 times), keeping its aspect ratio. |
#### Options for resize strategy
These options affect strategy to use when resizing an image into a target image that has both width and height set.
| Parameter | Explained |
|----------------|----------------------------------------------|
| `nr, no-ratio, stretch` | Do *not* keep aspect ratio when resizing and using both width & height constraints. Results in stretching the image, if needed, to fit in the resulting box. |
| `cf, crop-to-fit` | Set together with both `h` and `w` to make the image fit into dimensions, and crop out the rest of the image. |
| `ff, fill-to-fit` | Set together with both `h` and `w` to make the image fit into dimensions, and fill the rest using a background color. You can optionally supply a background color as this `ff=00ff00`, or `ff=00ff007f` when using the alpha channel. |
| `nu, no-upscale` | Avoid smaller images from being upscaled to larger ones. Combine with `stretch`, `crop-to-fit` or `fill-to-fit` to get the smaller image centered on a larger canvas. The requested dimension for the target image are thereby met. |
#### Options for cropping part of image
These options enable to decide what part of image to crop out.
| Parameter | Explained |
|----------------|----------------------------------------------|
| `c, crop` | Crops an area from the original image, set `width`, `height`, `start_x` and `start_y` to define the area to crop, for example `crop=100,100,10,10` (`crop=width,height,start_x,start_y`). Left top corner is 0, 0. You can use `left`, `right` or `center` when setting `start_x`. You may use `top`, `bottom` or `center` when setting `start_y`. |
| `a, area` | Define the area of the image to work with. Set `area=10,10,10,10` (`top`, `right`, `bottom`, `left`) to crop out the 10% of the outermost area. It works like an offset to define the part of the image you want to process. Its an alternative of using `crop`. |
#### General processing options
These options are general options affecting processing.
| Parameter | Explained |
|----------------|----------------------------------------------|
| `bgc, bg-color` | Set the backgroundcolor to use (if its needed). Use six hex digits as `bgc=00ff00` and 8 digits when using the alpha channel, as this `bgc=00ff007f`. The alpha value can be between 00 and 7f. |
#### Processing of image before resizing
This option are executed *before* the image is resized.
| Parameter | Explained |
|----------------|----------------------------------------------|
| `s, scale` | Scale the image to a size proportional to a percentage of its original size, `scale=25` makes an image 25% of its original size and `size=200` doubles up the image size. Scale is applied before resizing and has no impact of the target width and height. |
| `rb, rotate-before` | Rotate the image before its processed, send the angle as parameter `rb=45`. |
| `aro, auto-rotate` | Auto rotate the image based on EXIF information (useful when using images from smartphones). |
#### Processing of image after resizing
These options are executed *after* the image is resized.
| Parameter | Explained |
|----------------|----------------------------------------------|
| `ra, rotate-after`
This can be most useful for debugging and to understand what actually happen.
The parameter `nc, no-cache` ignores the cached item and will always create a new cached item.
The parameter `so, skip-original` skips the original image, even it that is a best fit. As a result a cached image is created and displayed.
A JSON representation of the image
--------------------------------------
You can ge a JSON representation of the image by adding the option `json`. This can be useful if you need to know the actual dimension of the image.
For example, the following image is created like this:
> `&w=300&save-as=jpg`
Its JSON-representation is retrieved like this:
> `&w=300&save-as=jpg&json`
Which gives the following result.
```php
{
"src":"kodim24.png",
"srcGmdate":"Wed, 12 Feb 2014 13:46:19",
"cache":"_._kodim24_300_200_q60.jpg",
"cacheGmdate":"Sat, 06 Dec 2014 14:09:50",
"filename":"_._kodim24_300_200_q60.jpg",
"width":300,
"height":200,
"aspectRatio":1.5,
"size":11008,
"colors":25751
}
```
I'll use this feature for ease testing of `img.php` and `CImage`. But the feature can also be useful when one really want complete control over the resulting dimension of an image.
Implications and considerations
--------------------------------------
Here are some thoughts when applying `img.php` on a live system.
### Select the proper mode
Select the proper mode for `img.php`. Set it to "strict" or "production" to prevent outsiders to get information about your system. Use only "development" for internal use since its quite verbose in its nature of error reporting.
### Put the installation directory outside web root
Edit the config file to put the installation directory -- and the cache directory -- outside of the web root. Best practice would be to store the installation directory and cache, outside of the web root. The only thing needed in the web root is `img.php` and `img_config.php` (if used) which can be placed, for example, in `/img/img.php` or just as `/img.php`.
### Friendly urls through `.htaccess`
Use `.htaccess`and rewrite rules (Apache) to get friendly image urls. Put `img.php` in the `/img` directory. Put the file `.htaccess` in the web root.
**.htaccess for `img.php`.**
```php
#
# Rewrite to have friendly urls to img.php, edit it to suite your environment.
#
# The example is set up as following.
#
# img A directory where all images are stored
# img/me.jpg Access a image as usually.
# img/img.php This is where I choose to place img.php (and img_config.php).
# image/me.jpg Access a image though img.php using htaccess rewrite.
# image/me.jpg?w=300 Using options to img.php.
#
# Subdirectories also work.
# img/me/me.jpg Direct access to the image.
# image/me/me.jpg Accessed through img.php.
# image/me/me.jpg?w=300 Using options to img.php.
#
RewriteRule ^image/(.*)$ img/img.php?src=$1 [QSA,NC,L]
```
You can now access all images through either `/image/car.jpg` (which uses `img.php`) or as usual through `/img/car.jpg` without passing through `img.php`. You send the arguments as usual.
> `/image/car.jpg?w=300&sharpen`
Or a image that resides in a subdirectory.
> `/image/all-cars/car.jpg?w=300&sharpen`
The result is good readable urls to your images. Its easy for the search engine to track and you can use the directory structure already existing in `/img`. Just like one wants to have it.
### Monitor cache size
There is a utility `cache.bash` included for monitoring the size of the cache-directory. It generates an output like this.
```bash
$ ./cache.bash
Usage: ./cache.bash [cache-dir]
$ ./cache.bash cache
Total size: 27M
Number of files: 225
Top-5 largest files:
1032 cache/_._kodim08_768_512_q60convolvesharpen.png
960 cache/_._kodim08_768_512_q60convolveemboss.png
932 cache/_._kodim08_768_512_q60_rb45.png
932 cache/_._kodim08_768_512_q60_ra45.png
856 cache/_._kodim08_768_512_q60_rb90.png
Last-5 created files:
2014-11-26 16:51 cache/_._kodim08_768_512_q60convolvelighten.png
2014-11-26 16:51 cache/_._kodim08_768_512_q60convolveblur.png
2014-11-26 16:48 cache/_._kodim08_400_267_q60convolvesharpen.png
2014-11-26 16:48 cache/_._kodim08_400_267_q60convolvelighten.png
2014-11-26 16:48 cache/_._kodim08_400_267_q60convolveemboss.png
Last-5 accessed files:
2014-11-27 16:12 _._wider_900_581_q60.jpg
2014-11-27 16:12 _._wider_750_484_q60.jpg
2014-11-27 16:12 _._wider_640_413_q60.jpg
2014-11-27 16:12 _._wider_640_200_c640-200-0-100_q60.jpg
2014-11-27 16:12 _._wider_600_387_q60.jpg
```
Use it as a base if you feel the need to monitor the size och the cache-directory.
### Read-only cache
The cache directory need to be writable for `img.php` to create new files. But its possible to first create all cache-files and then set the directory to be read-only. This will give you a way of shutting of `img.php` from creating new cache files. `img.php` will then continue to work for all images having a cached version but will fail if someone tries to create a new, not previously cached, version of the image.
### Post-processing with external tools
You can use external tools to post-process the images to optimize the file size. This option is available for JPEG and for PNG images. Post-processing is disabled by default, edit `img_config.php` to enable it.
It takes additional time to do post processing, it can take up to a couple of seconds. This is processing to create the cached image, thereafter the cached version will be used and no more post processing needs to be done.
These tools for post processing is not a part of `CImage` and `img.php`, you need to download and install them separately. I use them myself on my system to get an optimal file size.
### Allowing remote download of images
You can allow `img.php` to download remote images. That can be enabled in the config-file. However, before doing so, consider the implications on allowing anyone to download a file, hopefully an image, to your server and then the possibility to access it through the webserver.
That sounds scary. It should.
For my own sake I will use it like this, since I consider it a most useful feature.
* Create a special version of `img.php` that has remote download allowed, hide it from public usage.
* Always use a password.
* Download and process the image and save it as an `alias`.
* Integrate the image into your webpage and use the image in the alias directory.
This is an easy way to quickly download a remote image, process and share it.
So, its a scary feature and I might regret I did put it in. Still, its disabled by default and you enable it on your own risk. I have tried to make it as secure as I can, but I might have missed something. I will run it on my own system so I guess I'll find out how secure it is.
Community
--------------------------------------
There is a Swedish forum where you can ask questions, even in English. The forum is a general forum for education in web development, it is not specific for this software.
Ask questions on `CImage` and `img.php` [in the PHP sub forum]([BASEURL]forum/viewforum.php?f=12).
Or ask it on GitHub by creating an issue -- that would be the best place to ask questions.
Or if you fancy irc.
* `irc://irc.bsnet.se/#db-o-webb`
* `irc://irc.freenode.net/#dbwebb`
Trouble- and feature requests
--------------------------------------
Use [GitHub to report issues](https://github.com/mosbth/cimage/issues). Always include the following.
1. Describe very shortly: What are you trying to achieve, what happens, what did you expect.
2. Parameter list used for `img.php`.
3. The image used.
If you request a feature, describe its usage and argument for why you think it fits into `CImage` and `img.php`.
Feel free to fork, clone and create pull requests.
```
.
..: Copyright 2012-2015 by Mikael Roos (me@mikaelroos.se)
```
================================================
FILE: REVISION.md
================================================
Revision history
=====================================
v0.8.6 (2023-10-27)
-------------------------------------
* Fix deprecation notice on "Creation of dynamic property" for PHP 8.2.
v0.8.5 (2022-11-17)
-------------------------------------
* Enable configuration fix for solving Windows 2 WSL2 issue with is_readable/is_writable #189.
* Update CHttpGet.php for php 8.1 deprecated notice #188.
* Remove build status from README (since it is not up to date).
v0.8.4 (2022-05-30)
-------------------------------------
* Support PHP 8.1 and remove (more) deprecated messages when run in in development mode.
v0.8.3 (2022-05-24)
-------------------------------------
* Support PHP 8.1 and remove deprecated messages when run in in development mode.
* Generate prebuilt all include files for various settings
* Fix deprecated for PHP 8.1
* Fix deprecated for PHP 8.1
* Add php version as output in verbose mode
* Add PHP 81 as test environment
v0.8.2 (2021-10-27)
-------------------------------------
* Remove bad configuration.
v0.8.1 (2020-06-08)
-------------------------------------
* Updated version number in define.php.
v0.8.0 (2020-06-08)
-------------------------------------
* Enable to set JPEG image as interlaced, implement feature #177.
* Add function getValue() to read from querystring.
* Set PHP 7.0 as precondition (to prepare to update the codebase).
v0.7.23 (2020-05-06)
-------------------------------------
* Fix error in composer.json
v0.7.22 (2020-05-06)
-------------------------------------
* Update composer.json and move ext-gd from required to suggested to ease installation where cli does not have all extensions installed.
v0.7.21 (2020-01-15)
-------------------------------------
* Support PHP 7.4, some minor fixes with notices.
v0.7.20 (2017-11-06)
-------------------------------------
* Remove webroot/img/{round8.PNG,wider.JPEG,wider.JPG} to avoid unzip warning message when installing with composer.
* Adding docker-compose.yml #169.
v0.7.19 (2017-03-31)
-------------------------------------
* Move exception handler from functions.php to img.php #166.
* Correct XSS injection in `check_system.php`.
* Composer suggests ext-imagick and ext-curl.
v0.7.18 (2016-08-09)
-------------------------------------
* Made `&lossless` a requirement to not use the original image.
v0.7.17 (2016-08-09)
-------------------------------------
* Made `&lossless` part of the generated cache filename.
v0.7.16 (2016-08-09)
-------------------------------------
* Fix default mode to be production.
* Added pngquant as extra postprocessing utility for PNG-images, #154.
* Bug `&status` wrong variable name for fast track cache.
v0.7.15 (2016-08-09)
-------------------------------------
* Added the [Lenna/Lena sample image](http://www.cs.cmu.edu/~chuck/lennapg/) as tif and created a png, jpeg and webp version using Imagick convert `convert lena.tif lena.{png,jpg,webp}`, #152.
* Limited and basic support for WEBP format, se #132.
v0.7.14 (2016-08-08)
-------------------------------------
* Re-add removed cache directory.
* Make fast track cache disabled by default in the config file.
v0.7.13 (2016-08-08)
-------------------------------------
* Moved functions from img.php to `functions.php`.
* Added function `trace()` to measure speed and memory consumption, only for development.
* Added fast cache #149.
* Added `imgf.php` as shortcut to check for fast cache, before loading `img.php` as usual, adding `imgf_config.php` as symlink to `img_config.php`.
* Created `defines.php` and moved definition av version there.
* Fixed images in README, #148.
* Initiated dependency injection to `CImage`, class names can be set in config file and will be injected to `CImage` from `img.php`. Not implemented for all classes. #151.
* Enabled debug mode to make it easier to trace what actually happens while processing the image, #150.
v0.7.12 (2016-06-01)
-------------------------------------
* Fixed to correctly display image when using a resize strategy without height or width.
* Fixed background color for option `no-upscale`, #144.
v0.7.11 (2016-04-18)
-------------------------------------
* Add option for `skip_original` to config file to always skip original, #118.
v0.7.10 (2016-04-01)
-------------------------------------
* Add backup option for images `src-alt`, #141.
* Add require of ext-gd in composer.json, #133.
* Fix strict mode only reporting 404 when failure, #127.
v0.7.9 (2015-12-07)
-------------------------------------
* Strict mode only reporting 404 when failure, #127.
* Added correct CImage version to remote agent string, #131.
* Adding CCache to improve cache handling of caching for dummy, remote and srgb. #130.
v0.7.8 (2015-12-06)
-------------------------------------
* HTTP error messages now 403, 404 and 500 as in #128 and #127.
* More examples on dealing with cache through bash `bin/cache.bash`, #129.
* Added conversion to sRGB using option `?srgb`. #120.
* Added Gitter badge to README, #126.
* Fix proper download url in README, #125.
* Change path in `webroot/htaccess` to make it work in current environment.
v0.7.7 (2015-10-21)
-------------------------------------
* One can now add a HTTP header for Cache-Control in the config file, #109.
* Added hook in img,php before CImage is called, #123.
* Added configuration for default jpeg quality and png compression in the config file, #107.
* Strip comments and whitespace in imgs.php, #115.
* Bundle imgs.php did not have the correct mode.
* Adding option &status to get an overview of the installed on configured utilities, #116.
* Bug, all files saved as png-files, when not saving as specific file.
* Removed saving filename extension for alias images.
* Added option to decide if resample or resize when copying images internally. `&no-resample` makes resize, instead of resample as is default.
* Verbose now correctly states if transparent color is detected.
* Compare-tool now supports 6 images.
* Added option for dark background in the compare-tool.
* Removed that source png-files, containing less than 255 colors, is always saved as palette images since this migth depend on processing of the image.
* Adding save-as as part of the generated cache filename, #121.
* Add extra fields to json-response, #114.
* Add header for Content-Length, #111.
* Add check for postprocessing tools in path in `webroot/check_system.php`, #104.
v0.7.6 (2015-10-18)
-------------------------------------
* Adding testpage for dummy images `webroot/test/test_issue101-dummy.php`.
* Adding width and height when creating dummy image.
v0.7.5 (2015-10-18)
-------------------------------------
* Adding feature for creating dummy images `src=dummy`, #101.
* Add png compression to generated cache filename, fix #103.
* Removed file prefix from storing images in cache, breaking filenamestructure for cache images.
* Code cleaning in `CImage.php`.
v0.7.4 (2015-09-15)
-------------------------------------
* Add CAsciiArt.php to composer for autoloading, fix #102.
* Generate filename with filters, does not work on Windows, fix #100.
v0.7.3 (2015-09-01)
-------------------------------------
* Support output of ascii images, #67.
v0.7.2 (2015-08-17)
-------------------------------------
* Allow space in remote filenames, fix #98.
v0.7.1 (2015-07-25)
-------------------------------------
* Support for password hashes using `text`, `md5` and `hash`, fix #77.
* Using `CWhitelist` for checking hotlinking to images, fix #88.
* Added mode for `test` which enables logging verbose mode to file, fix #97.
* Improved codestyle and added `phpcs.xml` to start using phpcs to check code style, fix #95.
* Adding `composer.json` for publishing on packagist.
* Add permalink to setup for comparing images with `webroot/compare/compare.php`, fix #92.
* Allow space in filename by using `urlencode()` and allow space as valid filenam character. fix #91.
* Support redirections for remote images, fix #87, fix #90.
* Improving usage of Travis and Scrutinizer.
* Naming cache-file using md5 for remote images, fix #86.
* Loading images without depending on filename extension, fix #85.
* Adding unittest with phpunit #84, fix #13
* Adding support for whitelist of remote hostnames, #84
* Adding phpdoc, fix #48.
* Adding travis, fix #15.
* Adding scrutinizer, fix #57.
v0.7.0 (2015-02-10)
-------------------------------------
* Always use password, setting in img_config.php, fix #78.
* Resize gif keeping transparency #81.
* Now returns statuscode 500 when something fails #55.
* Three different modes: strict, production, development #44.
* Three files for all-in-one `imgs.php`, `imgp.php`, `imgd.php` #73.
* Change name of script all-in-one to `webroot/imgs.php` #73.
* Combine all code into one singel script, `webroot/img_single.php` #73.
* Disallow hotlinking/leeching by configuration #46.
* Alias-name is without extension #47.
* Option `alias` now requires `password` to work #47.
* Support for option `password, pwd` to protect usage of `alias` and remote download.
* Added support for option `alias` that creates a link to a cached version of the image #47.
* Create cache directory for remote download if it does not exists.
* Cleaned up `img_config.php` and introduced default values for almost all options #72.
v0.6.2 (2015-01-14)
-------------------------------------
* Added support for download of remote images #43.
* Added autoloader.
v0.6.1 (2015-01-08)
-------------------------------------
* Adding compare-page for comparing images. Issue #20.
* Added option `no-upscale, nu` as resizing strategy to decline upscaling of smaller images. Fix #61.
* Minor change in `CImage::resize()`, crop now does imagecopy without resamling.
* Correcting internal details for save-as and response json which indicated wrong colors. Fix #62.
* Fixed fill-to-fit that failed when using aspect-ratio. Fix #52.
* JSON returns correct values for resulting image. Fix #58.
* Corrected behaviour for skip-original. Fix #60.
v0.6 (2014-12-06)
-------------------------------------
* Rewrote and added documentation.
* Moved conolution expressesion from `img_config.php` to `CImage`.
* Minor cleaning of properties in `CImage`. Fix #23.
* Adding `webroot/htaccess` to show off how friendly urls can be created for `img.php`. Fix #45.
* Added option `fill-to-fit, ff`. Fix #38.
* Added option `shortcut, sc` to enable configuration of complex expressions. Fix #2.
* Added support for custom convolutions. Fix #49.
* Restructured testprograms. Fix #41.
* Corrected json on PHP 5.3. Fix #42.
* Improving template for tests in `webroot/tests` when testing out #40.
* Adding testcase for #40.
* Adding option `convolve` taking comma-separated list of 11 float-values, wraps and exposes `imageconvoluttion()`. #4
* Adding option `dpr, device-pixel-ratio` which defaults to 1. Set to 2 to get a twice as large image. Useful for Retina displays. Basically a shortcut to enlarge the image.
* Adding utility `cache.bash` to ease gathering stats on cache usage. #21
* Cache-directory can now be readonly and serve all cached files, still failing when need to save files. #5
* Cache now uses same file extension as original image #37.
* Can output image as json format using `json` #11.
v0.5.3 (2014-11-21)
-------------------------------------
* Support filenames of uppercase JPEG, JPG, PNG and GIF, as proposed in #37.
* Changing `CImage::output()` as proposed in #37.
* Adding security check that image filename is always below the path `image_path` as specified in `img_config.php` #37.
* Adding configuration item in `img_config.php` for setting valid characters in image filename.
* Moving `webroot/test*` into directory `webroot/test`.
* `webroot/check_system.php` now outputs if extension for exif is loaded.
* Broke API when `initDimensions()` split into two methods, new `initDimensions()` and `loadImageDetails()`.
* Added `autoRotate, aro` to auto rotate image based on EXIF information.
* Added `bgColor, bgc` to use as backgroundcolor when needing a filler color, for example rotate 45.
* Added `rotateBefore, rb` to rotate image a certain angle before processing.
* Added `rotateAfter, ra` to rotate image a certain angle after processing.
* Cleaned up code formatting, removed trailing spaces.
* Removed @ from opening images, better to display correct warning when failing #34, but put it back again.
* Setting gd.jpeg_ignore_warning to true as default #34.
* `webroot/check_system.php` now outputs version of PHP and GD.
* #32 correctly send 404 header when serving an error message.
* Trying to verify issue #29, but can not.
* Adding structure for testprograms together with, use `webroot/test_issue29.php` as sample.
* Improving code formatting.
* Moving parts of verbose output from img.php to CImage.php.
v0.5.2 (2014-04-01)
-------------------------------------
* Correcting issue #26 providing error message when not using postprocessing.
* Correcting issue #27 warning of default timezone.
* Removed default $config options in `img.php`, was not used, all configuration should be in `img_config.php`.
* Verified known bug - sharpen acts as blur in PHP 5.5.9 and 5.5.10 #28
v0.5.1 (2014-02-12)
-------------------------------------
* Display image in README-file.
* Create an empty `cache` directory as part of repo.
v0.5 (2014-02-12)
-------------------------------------
* Change constant name `CImage::PNG_QUALITY_DEFAULT` to `CImage::PNG_COMPRESSION_DEFAULT`.
* Split JPEG quality and PNG compression, `CImage->quality` and `CImage->compression`
* Changed `img.php` parameter name `d, deflate` to `co, compress`.
* Separating configuration issues from `img.php` to `img_config.php`.
* Format code according to PSR-2.
* Disabled post-processing JPEG and PNG as default.
* This version is supporting PHP 5.3, later versions will require 5.5 or later.
* Using GitHub issue tracking for feature requests and planning.
* Rewrote [the manual](http://dbwebb.se/opensource/cimage).
* Created directory `webroot` and moved some files there.
v0.4.1 (2014-01-27)
-------------------------------------
* Changed => to == on Modified-Since.
* Always send Last-Modified-Header.
* Added `htmlentities()` to verbose output.
* Fixed support for jpeg, not only jpg.
* Fixed crop whole image by setting crop=0,0,0,0
* Use negative values for crop width & height to base calulation on original width/height and withdraw selected amount.
* Correcting jpeg when setting quality.
* Removed obsolete reference to `$newName` in `CImage::__construct()` (issue 1).
v0.4 (2013-10-08)
-------------------------------------
* Improved support for pre-defined sizes.
* Adding grid column size as predefined size, c1-c24 for a 24 column grid. Configure in `img.php`.
* Corrected error on naming cache-files using subdir.
* Corrected calculation error on width & height for crop-to-fit.
* Adding effects for sharpen, emboss and blur through imageconvolution using matrixes.
* crop-to-fit, add parameter for offset x and y to enable to define which area is the, implemented as area.
* Support for resizing opaque images.
* Center of the image from which the crop is done. Improved usage of area to crop.
* Added support for % in width & height.
* Added aspect-ratio.
* Added scale.
* Quality for PNG images is now knows as deflate.
* Added palette to create images with max 256 colors.
* Added usage of all parameters to README.md
* Added documentation here http://dbwebb.se/opensource/cimage
* Adding `.gitignore`
* Re-adding `cache` directory
v0.3 (2012-10-02)
-------------------------------------
* Added crop. Can crop a area (`width`, `height`, `start_x`, `start_y`) from the original
image.
* Corrected to make the 304 Not Modified header work.
* Predefined sizes can be configured for width in `img.php`.
* Corrected to make crop work with width or height in combination with crop-to-fit.
v0.2 (2012-05-09)
-------------------------------------
* Implemented filters as in http://php.net/manual/en/function.imagefilter.php
* Changed `crop` to `crop_to_fit`, works the same way.
* Changed arguments and sends them in array.
* Added quality-setting.
* Added testcases for above.
v0.1.1 (2012-04-27)
-------------------------------------
* Corrected calculation where both width and height were set.
v0.1 (2012-04-25)
-------------------------------------
* Initial release after rewriting some older code doing the same, but not that good and flexible.
================================================
FILE: SECURITY.md
================================================
Security policy
======================
To report security vulnerabilities in the project, send en email to mikael.t.h.roos@gmail.com.
For other security related issues, please open an issue on the project.
================================================
FILE: autoload.php
================================================
$TARGET_P
cat webroot/img_header.php | sed "s|//'mode' => 'production',|'mode' => 'development',|" > $TARGET_D
cat webroot/img_header.php | sed "s|//'mode' => 'production',|'mode' => 'strict',|" > $TARGET_S
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 defines.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 functions.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 CHttpGet.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 CRemoteImage.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 CWhitelist.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 CAsciiArt.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 CImage.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 CCache.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 CFastTrackCache.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
tail -n +2 webroot/img.php | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
$ECHO "$NEWLINES" | tee -a $TARGET_D $TARGET_P $TARGET_S > /dev/null
php -w $TARGET_S > tmp && mv tmp $TARGET_S
$ECHO "\nDone."
$ECHO "\n"
$ECHO "\n"
================================================
FILE: cache/.gitignore
================================================
# Ignore everything in this directory
*
# Except this file
!.gitignore
================================================
FILE: composer.json
================================================
{
"name": "mos/cimage",
"type": "library",
"description": "Process, scale, resize, crop and filter images.",
"keywords": ["image", "imageprocessing", "gd"],
"homepage": "http://dbwebb.se/opensource/cimage",
"license": "MIT",
"authors": [
{
"name": "Mikael Roos",
"email": "me@mikaelroos.se",
"homepage": "http://mikaelroos.se",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/mosbth/cimage/issues",
"docs": "http://dbwebb.se/opensource/cimage"
},
"require": {
"php": ">=7.0"
},
"suggest": {
"ext-curl": "*",
"ext-exif": "*",
"ext-gd": "*",
"ext-imagick": "*"
},
"autoload": {
"files": [
"defines.php",
"functions.php"
],
"classmap": [
"CImage.php",
"CHttpGet.php",
"CRemoteImage.php",
"CWhitelist.php",
"CAsciiArt.php",
"CCache.php",
"CFastTrackCache.php"
]
}
}
================================================
FILE: defines.php
================================================
RemoveHandler .php
ForceType text/plain
================================================
FILE: docs/api/classes/CAsciiArt.html
================================================
Create an ASCII version of an image.
luminanceAreaAverage(string $img, integer $x1, integer $y1, integer $x2, integer $y2) : integer
Get the luminance from a region of an image using average color value.
| string | $img | the image. |
| integer | $x1 | the area to get pixels from. |
| integer | $y1 | the area to get pixels from. |
| integer | $x2 | the area to get pixels from. |
| integer | $y2 | the area to get pixels from. |
$luminance with a value between 0 and 100.
Get a image from a remote server using HTTP GET and If-Modified-Since.
Resize and crop images on the fly, store generated images in a cache.
** File not found : http://dbwebb.se/opensource/cimage **
__construct(string $imageSrc = null, string $imageFolder = null, string $saveFolder = null, string $saveName = null)
Constructor, can take arguments to init the object.
| string | $imageSrc | filename which may contain subdirectory. |
| string | $imageFolder | path to root folder for images. |
| string | $saveFolder | path to folder where to save the new file or null to skip saving. |
| string | $saveName | name of target file when saveing. |
createDummyImage(integer $width = null, integer $height = null) : $this
Create and save a dummy image. Use dimensions as stated in $this->newWidth, or $width or default to 100 (same for height.
| integer | $width | use specified width for image dimension. |
| integer | $height | use specified width for image dimension. |
trueColorToPalette() : void
Convert true color image to palette image, keeping alpha.
http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library
createConvolveArguments(string $expression) : array
Create convolve expression and return arguments for image convolution.
| string | $expression | constant string which evaluates to a list of 11 numbers separated by komma or such a list. |
as $matrix (3x3), $divisor and $offset
save(string $src = null, string $base = null, boolean $overwrite = true) : $this
Save image.
| string | $src | as target filename. |
| string | $base | as base directory where to store images. |
| boolean | $overwrite | or not, default to always overwrite file. |
or false if no folder is set.
A testclass
Get a image from a remote server using HTTP GET and If-Modified-Since.
Act as whitelist (or blacklist).
A testclass
| CHttpGet | Get a image from a remote server using HTTP GET and If-Modified-Since. |
", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } /** * Get HTTP code of response. * * @return integer as HTTP status code or null if not available. */ public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } /** * Get file modification time of response. * * @return int as timestamp. */ public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } /** * Get content type. * * @return string as the content type or null if not existing or invalid. */ public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : null; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } /** * Get file modification time of response. * * @param mixed $default as default value (int seconds) if date is * missing in response header. * * @return int as timestamp or $default if Date is missing in * response header. */ public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } /** * Get max age of cachable item. * * @param mixed $default as default value if date is missing in response * header. * * @return int as timestamp or false if not available. */ public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { // max-age=2592000 $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } /** * Get body of response. * * @return string as body. */ public function getBody() { return $this->response['body']; } } ================================================ FILE: docs/api/files/CImage.html ================================================
{$log}
EOD;
}
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
}
================================================
FILE: docs/api/files/CRemoteImage.html
================================================
| CRemoteImage | Get a image from a remote server using HTTP GET and If-Modified-Since. |
" . $exception->getMessage() . "
"
. $exception->getTraceAsString()
. ""
);
});
/**
* Get input from query string or return default value if not set.
*
* @param mixed $key as string or array of string values to look for in $_GET.
* @param mixed $default value to return when $key is not set in $_GET.
*
* @return mixed value from $_GET or default value.
*/
function get($key, $default = null)
{
if (is_array($key)) {
foreach ($key as $val) {
if (isset($_GET[$val])) {
return $_GET[$val];
}
}
} elseif (isset($_GET[$key])) {
return $_GET[$key];
}
return $default;
}
/**
* Get input from query string and set to $defined if defined or else $undefined.
*
* @param mixed $key as string or array of string values to look for in $_GET.
* @param mixed $defined value to return when $key is set in $_GET.
* @param mixed $undefined value to return when $key is not set in $_GET.
*
* @return mixed value as $defined or $undefined.
*/
function getDefined($key, $defined, $undefined)
{
return get($key) === null ? $undefined : $defined;
}
/**
* Get value from config array or default if key is not set in config array.
*
* @param string $key the key in the config array.
* @param mixed $default value to be default if $key is not set in config.
*
* @return mixed value as $config[$key] or $default.
*/
function getConfig($key, $default)
{
global $config;
return isset($config[$key])
? $config[$key]
: $default;
}
/**
* Log when verbose mode, when used without argument it returns the result.
*
* @param string $msg to log.
*
* @return void or array.
*/
function verbose($msg = null)
{
global $verbose, $verboseFile;
static $log = array();
if (!($verbose || $verboseFile)) {
return;
}
if (is_null($msg)) {
return $log;
}
$log[] = $msg;
}
/**
* Get configuration options from file, if the file exists, else use $config
* if its defined or create an empty $config.
*/
$configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php';
if (is_file($configFile)) {
$config = require $configFile;
} elseif (!isset($config)) {
$config = array();
}
/**
* verbose, v - do a verbose dump of what happens
* vf - do verbose dump to file
*/
$verbose = getDefined(array('verbose', 'v'), true, false);
$verboseFile = getDefined('vf', true, false);
verbose("img.php version = $version");
/**
* status - do a verbose dump of the configuration
*/
$status = getDefined('status', true, false);
/**
* Set mode as strict, production or development.
* Default is production environment.
*/
$mode = getConfig('mode', 'production');
// Settings for any mode
set_time_limit(20);
ini_set('gd.jpeg_ignore_warning', 1);
if (!extension_loaded('gd')) {
errorPage("Extension gd is nod loaded.");
}
// Specific settings for each mode
if ($mode == 'strict') {
error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'production') {
error_reporting(-1);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'development') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
$verboseFile = false;
} elseif ($mode == 'test') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
} else {
errorPage("Unknown mode: $mode");
}
verbose("mode = $mode");
verbose("error log = " . ini_get('error_log'));
/**
* Set default timezone if not set or if its set in the config-file.
*/
$defaultTimezone = getConfig('default_timezone', null);
if ($defaultTimezone) {
date_default_timezone_set($defaultTimezone);
} elseif (!ini_get('default_timezone')) {
date_default_timezone_set('UTC');
}
/**
* Check if passwords are configured, used and match.
* Options decide themself if they require passwords to be used.
*/
$pwdConfig = getConfig('password', false);
$pwdAlways = getConfig('password_always', false);
$pwdType = getConfig('password_type', 'text');
$pwd = get(array('password', 'pwd'), null);
// Check if passwords match, if configured to use passwords
$passwordMatch = null;
if ($pwd) {
switch($pwdType) {
case 'md5':
$passwordMatch = ($pwdConfig === md5($pwd));
break;
case 'hash':
$passwordMatch = password_verify($pwd, $pwdConfig);
break;
case 'text':
$passwordMatch = ($pwdConfig === $pwd);
break;
default:
$passwordMatch = false;
}
}
if ($pwdAlways && $passwordMatch !== true) {
errorPage("Password required and does not match or exists.");
}
verbose("password match = $passwordMatch");
/**
* Prevent hotlinking, leeching, of images by controlling who access them
* from where.
*
*/
$allowHotlinking = getConfig('allow_hotlinking', true);
$hotlinkingWhitelist = getConfig('hotlinking_whitelist', array());
$serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
$refererHost = parse_url($referer, PHP_URL_HOST);
if (!$allowHotlinking) {
if ($passwordMatch) {
; // Always allow when password match
verbose("Hotlinking since passwordmatch");
} elseif ($passwordMatch === false) {
errorPage("Hotlinking/leeching not allowed when password missmatch.");
} elseif (!$referer) {
errorPage("Hotlinking/leeching not allowed and referer is missing.");
} elseif (strcmp($serverName, $refererHost) == 0) {
; // Allow when serverName matches refererHost
verbose("Hotlinking disallowed but serverName matches refererHost.");
} elseif (!empty($hotlinkingWhitelist)) {
$whitelist = new CWhitelist();
$allowedByWhitelist = $whitelist->check($refererHost, $hotlinkingWhitelist);
if ($allowedByWhitelist) {
verbose("Hotlinking/leeching allowed by whitelist.");
} else {
errorPage("Hotlinking/leeching not allowed by whitelist. Referer: $referer.");
}
} else {
errorPage("Hotlinking/leeching not allowed.");
}
}
verbose("allow_hotlinking = $allowHotlinking");
verbose("referer = $referer");
verbose("referer host = $refererHost");
/**
* Get the source files.
*/
$autoloader = getConfig('autoloader', false);
$cimageClass = getConfig('cimage_class', false);
if ($autoloader) {
require $autoloader;
} elseif ($cimageClass) {
require $cimageClass;
}
/**
* Create the class for the image.
*/
$img = new CImage();
$img->setVerbose($verbose || $verboseFile);
/**
* Allow or disallow remote download of images from other servers.
* Passwords apply if used.
*
*/
$allowRemote = getConfig('remote_allow', false);
if ($allowRemote && $passwordMatch !== false) {
$pattern = getConfig('remote_pattern', null);
$img->setRemoteDownload($allowRemote, $pattern);
$whitelist = getConfig('remote_whitelist', null);
$img->setRemoteHostWhitelist($whitelist);
}
/**
* shortcut, sc - extend arguments with a constant value, defined
* in config-file.
*/
$shortcut = get(array('shortcut', 'sc'), null);
$shortcutConfig = getConfig('shortcut', array(
'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
));
verbose("shortcut = $shortcut");
if (isset($shortcut)
&& isset($shortcutConfig[$shortcut])) {
parse_str($shortcutConfig[$shortcut], $get);
verbose("shortcut-constant = {$shortcutConfig[$shortcut]}");
$_GET = array_merge($_GET, $get);
}
/**
* src - the source image file.
*/
$srcImage = urldecode(get('src'))
or errorPage('Must set src-attribute.');
// Check for valid/invalid characters
$imagePath = getConfig('image_path', __DIR__ . '/img/');
$imagePathConstraint = getConfig('image_path_constraint', true);
$validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_ \.:]+$#');
// Dummy image feature
$dummyEnabled = getConfig('dummy_enabled', true);
$dummyFilename = getConfig('dummy_filename', 'dummy');
$dummyImage = false;
preg_match($validFilename, $srcImage)
or errorPage('Filename contains invalid characters.');
if ($dummyEnabled && $srcImage === $dummyFilename) {
// Prepare to create a dummy image and use it as the source image.
$dummyImage = true;
} elseif ($allowRemote && $img->isRemoteSource($srcImage)) {
// If source is a remote file, ignore local file checks.
} elseif ($imagePathConstraint) {
// Check that the image is a file below the directory 'image_path'.
$pathToImage = realpath($imagePath . $srcImage);
$imageDir = realpath($imagePath);
is_file($pathToImage)
or errorPage(
'Source image is not a valid file, check the filename and that a
matching file exists on the filesystem.'
);
substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0
or errorPage(
'Security constraint: Source image is not below the directory "image_path"
as specified in the config file img_config.php.'
);
}
verbose("src = $srcImage");
/**
* Manage size constants from config file, use constants to replace values
* for width and height.
*/
$sizeConstant = getConfig('size_constant', function () {
// Set sizes to map constant to value, easier to use with width or height
$sizes = array(
'w1' => 613,
'w2' => 630,
);
// Add grid column width, useful for use as predefined size for width (or height).
$gridColumnWidth = 30;
$gridGutterWidth = 10;
$gridColumns = 24;
for ($i = 1; $i <= $gridColumns; $i++) {
$sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth;
}
return $sizes;
});
$sizes = call_user_func($sizeConstant);
/**
* width, w - set target width, affecting the resulting image width, height and resize options
*/
$newWidth = get(array('width', 'w'));
$maxWidth = getConfig('max_width', 2000);
// Check to replace predefined size
if (isset($sizes[$newWidth])) {
$newWidth = $sizes[$newWidth];
}
// Support width as % of original width
if ($newWidth[strlen($newWidth)-1] == '%') {
is_numeric(substr($newWidth, 0, -1))
or errorPage('Width % not numeric.');
} else {
is_null($newWidth)
or ($newWidth > 10 && $newWidth <= $maxWidth)
or errorPage('Width out of range.');
}
verbose("new width = $newWidth");
/**
* height, h - set target height, affecting the resulting image width, height and resize options
*/
$newHeight = get(array('height', 'h'));
$maxHeight = getConfig('max_height', 2000);
// Check to replace predefined size
if (isset($sizes[$newHeight])) {
$newHeight = $sizes[$newHeight];
}
// height
if ($newHeight[strlen($newHeight)-1] == '%') {
is_numeric(substr($newHeight, 0, -1))
or errorPage('Height % out of range.');
} else {
is_null($newHeight)
or ($newHeight > 10 && $newHeight <= $maxHeight)
or errorPage('Hight out of range.');
}
verbose("new height = $newHeight");
/**
* aspect-ratio, ar - affecting the resulting image width, height and resize options
*/
$aspectRatio = get(array('aspect-ratio', 'ar'));
$aspectRatioConstant = getConfig('aspect_ratio_constant', function () {
return array(
'3:1' => 3/1,
'3:2' => 3/2,
'4:3' => 4/3,
'8:5' => 8/5,
'16:10' => 16/10,
'16:9' => 16/9,
'golden' => 1.618,
);
});
// Check to replace predefined aspect ratio
$aspectRatios = call_user_func($aspectRatioConstant);
$negateAspectRatio = ($aspectRatio[0] == '!') ? true : false;
$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio;
if (isset($aspectRatios[$aspectRatio])) {
$aspectRatio = $aspectRatios[$aspectRatio];
}
if ($negateAspectRatio) {
$aspectRatio = 1 / $aspectRatio;
}
is_null($aspectRatio)
or is_numeric($aspectRatio)
or errorPage('Aspect ratio out of range');
verbose("aspect ratio = $aspectRatio");
/**
* crop-to-fit, cf - affecting the resulting image width, height and resize options
*/
$cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false);
verbose("crop to fit = $cropToFit");
/**
* Set default background color from config file.
*/
$backgroundColor = getConfig('background_color', null);
if ($backgroundColor) {
$img->setDefaultBackgroundColor($backgroundColor);
verbose("Using default background_color = $backgroundColor");
}
/**
* bgColor - Default background color to use
*/
$bgColor = get(array('bgColor', 'bg-color', 'bgc'), null);
verbose("bgColor = $bgColor");
/**
* Do or do not resample image when resizing.
*/
$resizeStrategy = getDefined(array('no-resample'), true, false);
if ($resizeStrategy) {
$img->setCopyResizeStrategy($img::RESIZE);
verbose("Setting = Resize instead of resample");
}
/**
* fill-to-fit, ff - affecting the resulting image width, height and resize options
*/
$fillToFit = get(array('fill-to-fit', 'ff'), null);
verbose("fill-to-fit = $fillToFit");
if ($fillToFit !== null) {
if (!empty($fillToFit)) {
$bgColor = $fillToFit;
verbose("fillToFit changed bgColor to = $bgColor");
}
$fillToFit = true;
verbose("fill-to-fit (fixed) = $fillToFit");
}
/**
* no-ratio, nr, stretch - affecting the resulting image width, height and resize options
*/
$keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true);
verbose("keep ratio = $keepRatio");
/**
* crop, c - affecting the resulting image width, height and resize options
*/
$crop = get(array('crop', 'c'));
verbose("crop = $crop");
/**
* area, a - affecting the resulting image width, height and resize options
*/
$area = get(array('area', 'a'));
verbose("area = $area");
/**
* skip-original, so - skip the original image and always process a new image
*/
$useOriginal = getDefined(array('skip-original', 'so'), false, true);
verbose("use original = $useOriginal");
/**
* no-cache, nc - skip the cached version and process and create a new version in cache.
*/
$useCache = getDefined(array('no-cache', 'nc'), false, true);
verbose("use cache = $useCache");
/**
* quality, q - set level of quality for jpeg images
*/
$quality = get(array('quality', 'q'));
$qualityDefault = getConfig('jpg_quality', null);
is_null($quality)
or ($quality > 0 and $quality <= 100)
or errorPage('Quality out of range');
if (is_null($quality) && !is_null($qualityDefault)) {
$quality = $qualityDefault;
}
verbose("quality = $quality");
/**
* compress, co - what strategy to use when compressing png images
*/
$compress = get(array('compress', 'co'));
$compressDefault = getConfig('png_compression', null);
is_null($compress)
or ($compress > 0 and $compress <= 9)
or errorPage('Compress out of range');
if (is_null($compress) && !is_null($compressDefault)) {
$compress = $compressDefault;
}
verbose("compress = $compress");
/**
* save-as, sa - what type of image to save
*/
$saveAs = get(array('save-as', 'sa'));
verbose("save as = $saveAs");
/**
* scale, s - Processing option, scale up or down the image prior actual resize
*/
$scale = get(array('scale', 's'));
is_null($scale)
or ($scale >= 0 and $scale <= 400)
or errorPage('Scale out of range');
verbose("scale = $scale");
/**
* palette, p - Processing option, create a palette version of the image
*/
$palette = getDefined(array('palette', 'p'), true, false);
verbose("palette = $palette");
/**
* sharpen - Processing option, post filter for sharpen effect
*/
$sharpen = getDefined('sharpen', true, null);
verbose("sharpen = $sharpen");
/**
* emboss - Processing option, post filter for emboss effect
*/
$emboss = getDefined('emboss', true, null);
verbose("emboss = $emboss");
/**
* blur - Processing option, post filter for blur effect
*/
$blur = getDefined('blur', true, null);
verbose("blur = $blur");
/**
* rotateBefore - Rotate the image with an angle, before processing
*/
$rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb'));
is_null($rotateBefore)
or ($rotateBefore >= -360 and $rotateBefore <= 360)
or errorPage('RotateBefore out of range');
verbose("rotateBefore = $rotateBefore");
/**
* rotateAfter - Rotate the image with an angle, before processing
*/
$rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r'));
is_null($rotateAfter)
or ($rotateAfter >= -360 and $rotateAfter <= 360)
or errorPage('RotateBefore out of range');
verbose("rotateAfter = $rotateAfter");
/**
* autoRotate - Auto rotate based on EXIF information
*/
$autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false);
verbose("autoRotate = $autoRotate");
/**
* filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter()
*/
$filters = array();
$filter = get(array('filter', 'f'));
if ($filter) {
$filters[] = $filter;
}
for ($i = 0; $i < 10; $i++) {
$filter = get(array("filter{$i}", "f{$i}"));
if ($filter) {
$filters[] = $filter;
}
}
verbose("filters = " . print_r($filters, 1));
/**
* json - output the image as a JSON object with details on the image.
* ascii - output the image as ASCII art.
*/
$outputFormat = getDefined('json', 'json', null);
$outputFormat = getDefined('ascii', 'ascii', $outputFormat);
verbose("outputformat = $outputFormat");
if ($outputFormat == 'ascii') {
$defaultOptions = getConfig(
'ascii-options',
array(
"characterSet" => 'two',
"scale" => 14,
"luminanceStrategy" => 3,
"customCharacterSet" => null,
)
);
$options = get('ascii');
$options = explode(',', $options);
if (isset($options[0]) && !empty($options[0])) {
$defaultOptions['characterSet'] = $options[0];
}
if (isset($options[1]) && !empty($options[1])) {
$defaultOptions['scale'] = $options[1];
}
if (isset($options[2]) && !empty($options[2])) {
$defaultOptions['luminanceStrategy'] = $options[2];
}
if (count($options) > 3) {
// Last option is custom character string
unset($options[0]);
unset($options[1]);
unset($options[2]);
$characterString = implode($options);
$defaultOptions['customCharacterSet'] = $characterString;
}
$img->setAsciiOptions($defaultOptions);
}
/**
* dpr - change to get larger image to easier support larger dpr, such as retina.
*/
$dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1);
verbose("dpr = $dpr");
/**
* convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php
*/
$convolve = get('convolve', null);
$convolutionConstant = getConfig('convolution_constant', array());
// Check if the convolve is matching an existing constant
if ($convolve && isset($convolutionConstant)) {
$img->addConvolveExpressions($convolutionConstant);
verbose("convolve constant = " . print_r($convolutionConstant, 1));
}
verbose("convolve = " . print_r($convolve, 1));
/**
* no-upscale, nu - Do not upscale smaller image to larger dimension.
*/
$upscale = getDefined(array('no-upscale', 'nu'), false, true);
verbose("upscale = $upscale");
/**
* Get details for post processing
*/
$postProcessing = getConfig('postprocessing', array(
'png_filter' => false,
'png_filter_cmd' => '/usr/local/bin/optipng -q',
'png_deflate' => false,
'png_deflate_cmd' => '/usr/local/bin/pngout -q',
'jpeg_optimize' => false,
'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize',
));
/**
* alias - Save resulting image to another alias name.
* Password always apply, must be defined.
*/
$alias = get('alias', null);
$aliasPath = getConfig('alias_path', null);
$validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#');
$aliasTarget = null;
if ($alias && $aliasPath && $passwordMatch) {
$aliasTarget = $aliasPath . $alias;
$useCache = false;
is_writable($aliasPath)
or errorPage("Directory for alias is not writable.");
preg_match($validAliasname, $alias)
or errorPage('Filename for alias contains invalid characters. Do not add extension.');
} elseif ($alias) {
errorPage('Alias is not enabled in the config file or password not matching.');
}
verbose("alias = $alias");
/**
* Get the cachepath from config.
*/
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');
/**
* Get the cachepath from config.
*/
$cacheControl = getConfig('cache_control', null);
if ($cacheControl) {
verbose("cacheControl = $cacheControl");
$img->addHTTPHeader("Cache-Control", $cacheControl);
}
/**
* Prepare a dummy image and use it as source image.
*/
$dummyDir = getConfig('dummy_dir', $cachePath. "/" . $dummyFilename);
if ($dummyImage === true) {
is_writable($dummyDir)
or verbose("dummy dir not writable = $dummyDir");
$img->setSaveFolder($dummyDir)
->setSource($dummyFilename, $dummyDir)
->setOptions(
array(
'newWidth' => $newWidth,
'newHeight' => $newHeight,
'bgColor' => $bgColor,
)
)
->setJpegQuality($quality)
->setPngCompression($compress)
->createDummyImage()
->generateFilename(null, false)
->save(null, null, false);
$srcImage = $img->getTarget();
$imagePath = null;
verbose("src (updated) = $srcImage");
}
/**
* Display status
*/
if ($status) {
$text = "img.php version = $version\n";
$text .= "PHP version = " . PHP_VERSION . "\n";
$text .= "Running on: " . $_SERVER['SERVER_SOFTWARE'] . "\n";
$text .= "Allow remote images = $allowRemote\n";
$text .= "Cache writable = " . is_writable($cachePath) . "\n";
$text .= "Cache dummy writable = " . is_writable($dummyDir) . "\n";
$text .= "Alias path writable = " . is_writable($aliasPath) . "\n";
$no = extension_loaded('exif') ? null : 'NOT';
$text .= "Extension exif is $no loaded.$textEOD; exit; } /** * Log verbose details to file */ if ($verboseFile) { $img->setVerboseToFile("$cachePath/log.txt"); } /** * Hook after img.php configuration and before processing with CImage */ $hookBeforeCImage = getConfig('hook_before_CImage', null); if (is_callable($hookBeforeCImage)) { verbose("hookBeforeCImage activated"); $allConfig = $hookBeforeCImage($img, array( // Options for calculate dimensions 'newWidth' => $newWidth, 'newHeight' => $newHeight, 'aspectRatio' => $aspectRatio, 'keepRatio' => $keepRatio, 'cropToFit' => $cropToFit, 'fillToFit' => $fillToFit, 'crop' => $crop, 'area' => $area, 'upscale' => $upscale, // Pre-processing, before resizing is done 'scale' => $scale, 'rotateBefore' => $rotateBefore, 'autoRotate' => $autoRotate, // General processing options 'bgColor' => $bgColor, // Post-processing, after resizing is done 'palette' => $palette, 'filters' => $filters, 'sharpen' => $sharpen, 'emboss' => $emboss, 'blur' => $blur, 'convolve' => $convolve, 'rotateAfter' => $rotateAfter, // Output format 'outputFormat' => $outputFormat, 'dpr' => $dpr, // Other 'postProcessing' => $postProcessing, )); verbose(print_r($allConfig, 1)); extract($allConfig); } /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1", var_dump(gd_info()), ""; } ================================================ FILE: docs/api/files/webroot%2Fcompare%2Fcompare-test.php.txt ================================================
Add link to images and visually compare them. Change the link och press return to load the image. Read more...
Image 1Image 2Image 3Image 4" . $exception->getMessage() . "
" . $exception->getTraceAsString(), ""); }); /** * Get input from query string or return default value if not set. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $default value to return when $key is not set in $_GET. * * @return mixed value from $_GET or default value. */ function get($key, $default = null) { if (is_array($key)) { foreach ($key as $val) { if (isset($_GET[$val])) { return $_GET[$val]; } } } elseif (isset($_GET[$key])) { return $_GET[$key]; } return $default; } /** * Get input from query string and set to $defined if defined or else $undefined. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $defined value to return when $key is set in $_GET. * @param mixed $undefined value to return when $key is not set in $_GET. * * @return mixed value as $defined or $undefined. */ function getDefined($key, $defined, $undefined) { return get($key) === null ? $undefined : $defined; } /** * Get value from config array or default if key is not set in config array. * * @param string $key the key in the config array. * @param mixed $default value to be default if $key is not set in config. * * @return mixed value as $config[$key] or $default. */ function getConfig($key, $default) { global $config; return isset($config[$key]) ? $config[$key] : $default; } /** * Log when verbose mode, when used without argument it returns the result. * * @param string $msg to log. * * @return void or array. */ function verbose($msg = null) { global $verbose; static $log = array(); if (!$verbose) { return; } if (is_null($msg)) { return $log; } $log[] = $msg; } /** * Get configuration options from file, if the file exists, else use $config * if its defined or create an empty $config. */ $configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php'; if (is_file($configFile)) { $config = require $configFile; } else if (!isset($config)) { $config = array(); } /** * verbose, v - do a verbose dump of what happens */ $verbose = getDefined(array('verbose', 'v'), true, false); verbose("img.php version = $version"); /** * Set mode as strict, production or development. * Default is production environment. */ $mode = getConfig('mode', 'production'); // Settings for any mode set_time_limit(20); ini_set('gd.jpeg_ignore_warning', 1); if (!extension_loaded('gd')) { errorPage("Extension gd is nod loaded."); } // Specific settings for each mode if ($mode == 'strict') { error_reporting(0); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'production') { error_reporting(-1); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'development') { error_reporting(-1); ini_set('display_errors', 1); ini_set('log_errors', 0); } else { errorPage("Unknown mode: $mode"); } verbose("mode = $mode"); verbose("error log = " . ini_get('error_log')); /** * Set default timezone if not set or if its set in the config-file. */ $defaultTimezone = getConfig('default_timezone', null); if ($defaultTimezone) { date_default_timezone_set($defaultTimezone); } else if (!ini_get('default_timezone')) { date_default_timezone_set('UTC'); } /** * Check if passwords are configured, used and match. * Options decide themself if they require passwords to be used. */ $pwdConfig = getConfig('password', false); $pwdAlways = getConfig('password_always', false); $pwd = get(array('password', 'pwd'), null); // Check if passwords match, if configured to use passwords $passwordMatch = null; if ($pwdAlways) { $passwordMatch = ($pwdConfig === $pwd); if (!$passwordMatch) { errorPage("Password required and does not match or exists."); } } elseif ($pwdConfig && $pwd) { $passwordMatch = ($pwdConfig === $pwd); } verbose("password match = $passwordMatch"); /** * Prevent hotlinking, leeching, of images by controlling who access them * from where. * */ $allowHotlinking = getConfig('allow_hotlinking', true); $hotlinkingWhitelist = getConfig('hotlinking_whitelist', array()); $serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $refererHost = parse_url($referer, PHP_URL_HOST); if (!$allowHotlinking) { if ($passwordMatch) { ; // Always allow when password match } else if ($passwordMatch === false) { errorPage("Hotlinking/leeching not allowed when password missmatch."); } else if (!$referer) { errorPage("Hotlinking/leeching not allowed and referer is missing."); } else if (strcmp($serverName, $refererHost) == 0) { ; // Allow when serverName matches refererHost } else if (!empty($hotlinkingWhitelist)) { $allowedByWhitelist = false; foreach ($hotlinkingWhitelist as $val) { if (preg_match($val, $refererHost)) { $allowedByWhitelist = true; } } if (!$allowedByWhitelist) { errorPage("Hotlinking/leeching not allowed by whitelist."); } } else { errorPage("Hotlinking/leeching not allowed."); } } verbose("allow_hotlinking = $allowHotlinking"); verbose("referer = $referer"); verbose("referer host = $refererHost"); /** * Get the source files. */ $autoloader = getConfig('autoloader', false); $cimageClass = getConfig('cimage_class', false); if ($autoloader) { require $autoloader; } else if ($cimageClass) { require $cimageClass; } /** * Create the class for the image. */ $img = new CImage(); $img->setVerbose($verbose); /** * Allow or disallow remote download of images from other servers. * Passwords apply if used. * */ $allowRemote = getConfig('remote_allow', false); if ($allowRemote && $passwordMatch !== false) { $pattern = getConfig('remote_pattern', null); $img->setRemoteDownload($allowRemote, $pattern); $whitelist = getConfig('remote_whitelist', null); $img->setRemoteHostWhitelist($whitelist); } /** * shortcut, sc - extend arguments with a constant value, defined * in config-file. */ $shortcut = get(array('shortcut', 'sc'), null); $shortcutConfig = getConfig('shortcut', array( 'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen", )); verbose("shortcut = $shortcut"); if (isset($shortcut) && isset($shortcutConfig[$shortcut])) { parse_str($shortcutConfig[$shortcut], $get); verbose("shortcut-constant = {$shortcutConfig[$shortcut]}"); $_GET = array_merge($_GET, $get); } /** * src - the source image file. */ $srcImage = get('src') or errorPage('Must set src-attribute.'); // Check for valid/invalid characters $imagePath = getConfig('image_path', __DIR__ . '/img/'); $imagePathConstraint = getConfig('image_path_constraint', true); $validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_\.:]+$#'); preg_match($validFilename, $srcImage) or errorPage('Filename contains invalid characters.'); if ($allowRemote && $img->isRemoteSource($srcImage)) { // If source is a remote file, ignore local file checks. } else if ($imagePathConstraint) { // Check that the image is a file below the directory 'image_path'. $pathToImage = realpath($imagePath . $srcImage); $imageDir = realpath($imagePath); is_file($pathToImage) or errorPage( 'Source image is not a valid file, check the filename and that a matching file exists on the filesystem.' ); substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0 or errorPage( 'Security constraint: Source image is not below the directory "image_path" as specified in the config file img_config.php.' ); } verbose("src = $srcImage"); /** * Manage size constants from config file, use constants to replace values * for width and height. */ $sizeConstant = getConfig('size_constant', function () { // Set sizes to map constant to value, easier to use with width or height $sizes = array( 'w1' => 613, 'w2' => 630, ); // Add grid column width, useful for use as predefined size for width (or height). $gridColumnWidth = 30; $gridGutterWidth = 10; $gridColumns = 24; for ($i = 1; $i <= $gridColumns; $i++) { $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth; } return $sizes; }); $sizes = call_user_func($sizeConstant); /** * width, w - set target width, affecting the resulting image width, height and resize options */ $newWidth = get(array('width', 'w')); $maxWidth = getConfig('max_width', 2000); // Check to replace predefined size if (isset($sizes[$newWidth])) { $newWidth = $sizes[$newWidth]; } // Support width as % of original width if ($newWidth[strlen($newWidth)-1] == '%') { is_numeric(substr($newWidth, 0, -1)) or errorPage('Width % not numeric.'); } else { is_null($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('Width out of range.'); } verbose("new width = $newWidth"); /** * height, h - set target height, affecting the resulting image width, height and resize options */ $newHeight = get(array('height', 'h')); $maxHeight = getConfig('max_height', 2000); // Check to replace predefined size if (isset($sizes[$newHeight])) { $newHeight = $sizes[$newHeight]; } // height if ($newHeight[strlen($newHeight)-1] == '%') { is_numeric(substr($newHeight, 0, -1)) or errorPage('Height % out of range.'); } else { is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Hight out of range.'); } verbose("new height = $newHeight"); /** * aspect-ratio, ar - affecting the resulting image width, height and resize options */ $aspectRatio = get(array('aspect-ratio', 'ar')); $aspectRatioConstant = getConfig('aspect_ratio_constant', function () { return array( '3:1' => 3/1, '3:2' => 3/2, '4:3' => 4/3, '8:5' => 8/5, '16:10' => 16/10, '16:9' => 16/9, 'golden' => 1.618, ); }); // Check to replace predefined aspect ratio $aspectRatios = call_user_func($aspectRatioConstant); $negateAspectRatio = ($aspectRatio[0] == '!') ? true : false; $aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio; if (isset($aspectRatios[$aspectRatio])) { $aspectRatio = $aspectRatios[$aspectRatio]; } if ($negateAspectRatio) { $aspectRatio = 1 / $aspectRatio; } is_null($aspectRatio) or is_numeric($aspectRatio) or errorPage('Aspect ratio out of range'); verbose("aspect ratio = $aspectRatio"); /** * crop-to-fit, cf - affecting the resulting image width, height and resize options */ $cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false); verbose("crop to fit = $cropToFit"); /** * Set default background color from config file. */ $backgroundColor = getConfig('background_color', null); if ($backgroundColor) { $img->setDefaultBackgroundColor($backgroundColor); verbose("Using default background_color = $backgroundColor"); } /** * bgColor - Default background color to use */ $bgColor = get(array('bgColor', 'bg-color', 'bgc'), null); verbose("bgColor = $bgColor"); /** * fill-to-fit, ff - affecting the resulting image width, height and resize options */ $fillToFit = get(array('fill-to-fit', 'ff'), null); verbose("fill-to-fit = $fillToFit"); if ($fillToFit !== null) { if (!empty($fillToFit)) { $bgColor = $fillToFit; verbose("fillToFit changed bgColor to = $bgColor"); } $fillToFit = true; verbose("fill-to-fit (fixed) = $fillToFit"); } /** * no-ratio, nr, stretch - affecting the resulting image width, height and resize options */ $keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true); verbose("keep ratio = $keepRatio"); /** * crop, c - affecting the resulting image width, height and resize options */ $crop = get(array('crop', 'c')); verbose("crop = $crop"); /** * area, a - affecting the resulting image width, height and resize options */ $area = get(array('area', 'a')); verbose("area = $area"); /** * skip-original, so - skip the original image and always process a new image */ $useOriginal = getDefined(array('skip-original', 'so'), false, true); verbose("use original = $useOriginal"); /** * no-cache, nc - skip the cached version and process and create a new version in cache. */ $useCache = getDefined(array('no-cache', 'nc'), false, true); verbose("use cache = $useCache"); /** * quality, q - set level of quality for jpeg images */ $quality = get(array('quality', 'q')); is_null($quality) or ($quality > 0 and $quality <= 100) or errorPage('Quality out of range'); verbose("quality = $quality"); /** * compress, co - what strategy to use when compressing png images */ $compress = get(array('compress', 'co')); is_null($compress) or ($compress > 0 and $compress <= 9) or errorPage('Compress out of range'); verbose("compress = $compress"); /** * save-as, sa - what type of image to save */ $saveAs = get(array('save-as', 'sa')); verbose("save as = $saveAs"); /** * scale, s - Processing option, scale up or down the image prior actual resize */ $scale = get(array('scale', 's')); is_null($scale) or ($scale >= 0 and $scale <= 400) or errorPage('Scale out of range'); verbose("scale = $scale"); /** * palette, p - Processing option, create a palette version of the image */ $palette = getDefined(array('palette', 'p'), true, false); verbose("palette = $palette"); /** * sharpen - Processing option, post filter for sharpen effect */ $sharpen = getDefined('sharpen', true, null); verbose("sharpen = $sharpen"); /** * emboss - Processing option, post filter for emboss effect */ $emboss = getDefined('emboss', true, null); verbose("emboss = $emboss"); /** * blur - Processing option, post filter for blur effect */ $blur = getDefined('blur', true, null); verbose("blur = $blur"); /** * rotateBefore - Rotate the image with an angle, before processing */ $rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb')); is_null($rotateBefore) or ($rotateBefore >= -360 and $rotateBefore <= 360) or errorPage('RotateBefore out of range'); verbose("rotateBefore = $rotateBefore"); /** * rotateAfter - Rotate the image with an angle, before processing */ $rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r')); is_null($rotateAfter) or ($rotateAfter >= -360 and $rotateAfter <= 360) or errorPage('RotateBefore out of range'); verbose("rotateAfter = $rotateAfter"); /** * autoRotate - Auto rotate based on EXIF information */ $autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false); verbose("autoRotate = $autoRotate"); /** * filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter() */ $filters = array(); $filter = get(array('filter', 'f')); if ($filter) { $filters[] = $filter; } for ($i = 0; $i < 10; $i++) { $filter = get(array("filter{$i}", "f{$i}")); if ($filter) { $filters[] = $filter; } } verbose("filters = " . print_r($filters, 1)); /** * json - output the image as a JSON object with details on the image. */ $outputFormat = getDefined('json', 'json', null); verbose("json = $outputFormat"); /** * dpr - change to get larger image to easier support larger dpr, such as retina. */ $dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1); verbose("dpr = $dpr"); /** * convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php */ $convolve = get('convolve', null); $convolutionConstant = getConfig('convolution_constant', array()); // Check if the convolve is matching an existing constant if ($convolve && isset($convolutionConstant)) { $img->addConvolveExpressions($convolutionConstant); verbose("convolve constant = " . print_r($convolutionConstant, 1)); } verbose("convolve = " . print_r($convolve, 1)); /** * no-upscale, nu - Do not upscale smaller image to larger dimension. */ $upscale = getDefined(array('no-upscale', 'nu'), false, true); verbose("upscale = $upscale"); /** * Get details for post processing */ $postProcessing = getConfig('postprocessing', array( 'png_filter' => false, 'png_filter_cmd' => '/usr/local/bin/optipng -q', 'png_deflate' => false, 'png_deflate_cmd' => '/usr/local/bin/pngout -q', 'jpeg_optimize' => false, 'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize', )); /** * alias - Save resulting image to another alias name. * Password always apply, must be defined. */ $alias = get('alias', null); $aliasPath = getConfig('alias_path', null); $validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#'); $aliasTarget = null; if ($alias && $aliasPath && $passwordMatch) { $aliasTarget = $aliasPath . $alias; $useCache = false; is_writable($aliasPath) or errorPage("Directory for alias is not writable."); preg_match($validAliasname, $alias) or errorPage('Filename for alias contains invalid characters. Do not add extension.'); } else if ($alias) { errorPage('Alias is not enabled in the config file or password not matching.'); } verbose("alias = $alias"); /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } /** * Get HTTP code of response. * * @return integer as HTTP status code or null if not available. */ public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } /** * Get file modification time of response. * * @return int as timestamp. */ public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } /** * Get content type. * * @return string as the content type or null if not existing or invalid. */ public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : null; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } /** * Get file modification time of response. * * @param mixed $default as default value (int seconds) if date is * missing in response header. * * @return int as timestamp or $default if Date is missing in * response header. */ public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } /** * Get max age of cachable item. * * @param mixed $default as default value if date is missing in response * header. * * @return int as timestamp or false if not available. */ public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { // max-age=2592000 $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } /** * Get body of response. * * @return string as body. */ public function getBody() { return $this->response['body']; } } /** * Get a image from a remote server using HTTP GET and If-Modified-Since. * */ class CRemoteImage { /** * Path to cache files. */ private $saveFolder = null; /** * Use cache or not. */ private $useCache = true; /** * HTTP object to aid in download file. */ private $http; /** * Status of the HTTP request. */ private $status; /** * Defalt age for cached items 60*60*24*7. */ private $defaultMaxAge = 604800; /** * Url of downloaded item. */ private $url; /** * Base name of cache file for downloaded item. */ private $fileName; /** * Filename for json-file with details of cached item. */ private $fileJson; /** * Filename for image-file. */ private $fileImage; /** * Cache details loaded from file. */ private $cache; /** * Constructor * */ public function __construct() { ; } /** * Get status of last HTTP request. * * @return int as status */ public function getStatus() { return $this->status; } /** * Get JSON details for cache item. * * @return array with json details on cache. */ public function getDetails() { return $this->cache; } /** * Set the path to the cache directory. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function setCache($path) { $this->saveFolder = $path; return $this; } /** * Check if cache is writable or throw exception. * * @return $this * * @throws Exception if cahce folder is not writable. */ public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } /** * Decide if the cache should be used or not before trying to download * a remote file. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Translate a content type to a file extension. * * @param string $type a valid content type. * * @return string as file extension or false if no match. */ function contentTypeToFileExtension($type) { $extension = array( 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', ); return isset($extension[$type]) ? $extension[$type] : false; } /** * Set header fields. * * @return $this */ function setHeaderFields() { $this->http->setHeader("User-Agent", "CImage/0.6 (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } /** * Save downloaded resource to cache. * * @return string as path to saved file or false if not saved. */ function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $extension = $this->contentTypeToFileExtension($type); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['File-Extension'] = $extension; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } if ($extension) { $this->fileImage = $this->fileName . "." . $extension; // Save only if body is a valid image $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileImage, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileImage; } } return false; } /** * Got a 304 and updates cache with new age. * * @return string as path to cached file. */ function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileImage; } /** * Download a remote file and keep a cache of downloaded files. * * @param string $url a remote url. * * @return string as path to downloaded file or false if failed. */ function download($url) { $this->http = new CHttpGet(); $this->url = $url; // First check if the cache is valid and can be used $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } // Do a HTTP request to download item $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } else if ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } return false; } /** * Get the path to the cached image file if the cache is valid. * * @return $this */ public function loadCacheDetails() { $cacheFile = str_replace(array("/", ":", "#", ".", "?"), "-", $this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } /** * Get the path to the cached image file if the cache is valid. * * @return string as the path ot the image file or false if no cache. */ public function getCachedSource() { $this->fileImage = $this->fileName . "." . $this->cache['File-Extension']; $imageExists = is_readable($this->fileImage); // Is cache valid? $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileImage; } // Prepare for a 304 if available if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } /** * Resize and crop images on the fly, store generated images in a cache. * * @author Mikael Roos mos@dbwebb.se * @example http://dbwebb.se/opensource/cimage * @link https://github.com/mosbth/cimage */ class CImage { /** * Constants type of PNG image */ const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; /** * Constant for default image quality when not set */ const JPEG_QUALITY_DEFAULT = 60; /** * Quality level for JPEG images. */ private $quality; /** * Is the quality level set from external use (true) or is it default (false)? */ private $useQuality = false; /** * Constant for default image quality when not set */ const PNG_COMPRESSION_DEFAULT = -1; /** * Compression level for PNG images. */ private $compress; /** * Is the compress level set from external use (true) or is it default (false)? */ private $useCompress = false; /** * Default background color, red, green, blue, alpha. * * @todo remake when upgrading to PHP 5.5 */ /* const BACKGROUND_COLOR = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, );*/ /** * Default background color to use. * * @todo remake when upgrading to PHP 5.5 */ //private $bgColorDefault = self::BACKGROUND_COLOR; private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); /** * Background color to use, specified as part of options. */ private $bgColor; /** * Where to save the target file. */ private $saveFolder; /** * The working image object. */ private $image; /** * The root folder of images (only used in constructor to create $pathToImage?). */ private $imageFolder; /** * Image filename, may include subdirectory, relative from $imageFolder */ private $imageSrc; /** * Actual path to the image, $imageFolder . '/' . $imageSrc */ private $pathToImage; /** * Original file extension */ private $fileExtension; /** * File extension to use when saving image. */ private $extension; /** * Output format, supports null (image) or json. */ private $outputFormat = null; /** * Verbose mode to print out a trace and display the created image */ private $verbose = false; /** * Keep a log/trace on what happens */ private $log = array(); /** * Handle image as palette image */ private $palette; /** * Target filename, with path, to save resulting image in. */ private $cacheFileName; /** * Set a format to save image as, or null to use original format. */ private $saveAs; /** * Path to command for filter optimize, for example optipng or null. */ private $pngFilter; /** * Path to command for deflate optimize, for example pngout or null. */ private $pngDeflate; /** * Path to command to optimize jpeg images, for example jpegtran or null. */ private $jpegOptimize; /** * Image dimensions, calculated from loaded image. */ private $width; // Calculated from source image private $height; // Calculated from source image /** * New image dimensions, incoming as argument or calculated. */ private $newWidth; private $newWidthOrig; // Save original value private $newHeight; private $newHeightOrig; // Save original value /** * Change target height & width when different dpr, dpr 2 means double image dimensions. */ private $dpr = 1; /** * Always upscale images, even if they are smaller than target image. */ const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; /** * Array with details on how to crop, incoming as argument and calculated. */ public $crop; public $cropOrig; // Save original value /** * String with details on how to do image convolution. String * should map a key in the $convolvs array or be a string of * 11 float values separated by comma. The first nine builds * up the matrix, then divisor and last offset. */ private $convolve; /** * Custom convolution expressions, matrix 3x3, divisor and offset. */ private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); /** * Resize strategy to fill extra area with background color. * True or false. */ private $fillToFit; /** * Used with option area to set which parts of the image to use. */ private $offset; /** * Calculate target dimension for image when using fill-to-fit resize strategy. */ private $fillWidth; private $fillHeight; /** * Allow remote file download, default is to disallow remote file download. */ private $allowRemote = false; /** * Pattern to recognize a remote file. */ //private $remotePattern = '#^[http|https]://#'; private $remotePattern = '#^https?://#'; /** * Use the cache if true, set to false to ignore the cached file. */ private $useCache = true; /** * Properties, the class is mutable and the method setOptions() * decides (partly) what properties are created. * * @todo Clean up these and check if and how they are used */ public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $type; // Calculated from source image private $attr; // Calculated from source image private $useOriginal; // Use original image if possible /** * Constructor, can take arguments to init the object. * * @param string $imageSrc filename which may contain subdirectory. * @param string $imageFolder path to root folder for images. * @param string $saveFolder path to folder where to save the new file or null to skip saving. * @param string $saveName name of target file when saveing. */ public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } /** * Set verbose mode. * * @param boolean $mode true or false to enable and disable verbose mode, * default is true. * * @return $this */ public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } /** * Set save folder, base folder for saving cache files. * * @todo clean up how $this->saveFolder is used in other methods. * * @param string $path where to store cached files. * * @return $this */ public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } /** * Use cache or not. * * @todo clean up how $this->noCache is used in other methods. * * @param string $use true or false to use cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Allow or disallow remote image download. * * @param boolean $allow true or false to enable and disable. * @param string $pattern to use to detect if its a remote file. * * @return $this */ public function setRemoteDownload($allow, $pattern = null) { $this->allowRemote = $allow; $this->remotePattern = $pattern ? $pattern : $this->remotePattern; $this->log("Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern); return $this; } /** * Check if the image resource is a remote file or not. * * @param string $src check if src is remote. * * @return boolean true if $src is a remote file, else false. */ public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return $remote; } /** * Check if file extension is valid as a file extension. * * @param string $extension of image file. * * @return $this */ private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } /** * Download a remote image and return path to its local copy. * * @param string $src remote path to image. * * @return string as path to downloaded remote source. */ public function downloadRemoteSource($src) { $remote = new CRemoteImage(); $cache = $this->saveFolder . "/remote/"; if (!is_dir($cache)) { if (!is_writable($this->saveFolder)) { throw new Exception("Can not create remote cache, cachefolder not writable."); } mkdir($cache); $this->log("The remote cache does not exists, creating it."); } if (!is_writable($cache)) { $this->log("The remote cache is not writable."); } $remote->setCache($cache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item has local cached file: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } /** * Set src file. * * @param string $src of image. * @param string $dir as base directory where images are. * * @return $this */ public function setSource($src, $dir = null) { if (!isset($src)) { return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $this->imageFolder = rtrim($dir, '/'); $this->pathToImage = $this->imageFolder . '/' . $this->imageSrc; $this->fileExtension = strtolower(pathinfo($this->pathToImage, PATHINFO_EXTENSION)); //$this->extension = $this->fileExtension; $this->checkFileExtension($this->fileExtension); return $this; } /** * Set target file. * * @param string $src of target image. * @param string $dir as base directory where images are stored. * * @return $this */ public function setTarget($src = null, $dir = null) { if (!(isset($src) && isset($dir))) { return $this; } $this->saveFolder = $dir; $this->cacheFileName = $dir . '/' . $src; /* Allow readonly cache is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); */ // Sanitize filename $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } /** * Set options to use when processing image. * * @param array $args used when processing image. * * @return $this */ public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( // Options for calculate dimensions 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), 'area' => null, //'0,0,0,0', 'upscale' => self::UPSCALE_DEFAULT, // Options for caching or using original 'useCache' => true, 'useOriginal' => true, // Pre-processing, before resizing is done 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, // General options 'bgColor' => null, // Post-processing, after resizing is done 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, // Output format 'outputFormat' => null, 'dpr' => 1, // Options for saving //'quality' => null, //'compress' => null, //'saveAs' => null, ); // Convert crop settings from string to array if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } // Convert area settings from string to array if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } // Convert filter settings from array of string to array of array if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } // Merge default arguments with incoming and set properties. //$args = array_merge_recursive($defaults, $args); $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } // Save original values to enable re-calculating $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } /** * Map filter name to PHP filter and id. * * @param string $name the name of the filter. * * @return array with filter settings * @throws Exception */ private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } /** * Load image details from original image file. * * @param string $file the file to load or null to use $this->pathToImage. * * @return $this * @throws Exception */ public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; is_readable($file) or $this->raiseError('Image file does not exist.'); // Get details on image $info = list($this->width, $this->height, $this->type, $this->attr) = getimagesize($file); !empty($info) or $this->raiseError("The file doesn't seem to be an image."); if ($this->verbose) { $this->log("Image file: {$file}"); $this->log("Image width x height (type): {$this->width} x {$this->height} ({$this->type})."); $this->log("Image filesize: " . filesize($file) . " bytes."); } return $this; } /** * Init new width and height and do some sanity checks on constraints, before any * processing can be done. * * @return $this * @throws Exception */ public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // width as % if ($this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } // height as % if ($this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); // width & height from aspect ratio if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } // Change width & height based on dpr if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } // Check values to be within domain is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Calculate new width and height of image, based on settings. * * @return $this */ public function calculateNewWidthAndHeight() { // Crop, use cropped width and height as base for calulations $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // Check if there is an area to crop off if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; // Check if crop is set if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } // Calculate new width and height if keeping aspect-ratio. if ($this->keepRatio) { $this->log("Keep aspect ratio."); // Crop-to-fit and both new width and height are set. if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { // Use newWidth and newHeigh as width/height, image should fit in box. $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { // Both new width and height are set. // Use newWidth and newHeigh as max width/height, image should not be larger. $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { // Use new width as max-width $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { // Use new height as max-hight $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } // Get image dimensions for pre-resize image. if ($this->cropToFit || $this->fillToFit) { // Get relations of original & target image $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } else if ($this->fillToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } // Crop, ensure to set new width and height if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } // Fill to fit, ensure to set new width and height /*if ($this->fillToFit) { $this->log("FillToFit."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); }*/ // No new height or width is set, use existing measures. $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Re-calculate image dimensions when original image dimension has changed. * * @return $this */ public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } /** * Set extension for filename to save as. * * @param string $saveas extension to save image as * * @return $this */ public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image using as: " . $this->extension); return $this; } /** * Set JPEG quality to use when saving image * * @param int $quality as the quality to set. * * @return $this */ public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } /** * Set PNG compressen algorithm to use when saving image * * @param int $compress as the algorithm to use. * * @return $this */ public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } /** * Use original image if possible, check options which affects image processing. * * @param boolean $useOrig default is to use original if possible, else set to false. * * @return $this */ public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } /** * Generate filename to save file in cache. * * @param string $base as basepath for storing file. * * @return $this */ public function generateFilename($base) { $parts = pathinfo($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $width = $this->newWidth; $height = $this->newHeight; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= ":".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $this->extension = isset($this->extension) ? $this->extension : $parts['extension']; $optimize = null; if ($this->extension == 'jpeg' || $this->extension == 'jpg') { $optimize = $this->jpegOptimize ? 'o' : null; } elseif ($this->extension == 'png') { $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; } $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $file = $subdir . '_' . $parts['filename'] . '_' . $width . '_' . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . '.' . $this->extension; return $this->setTarget($file, $base); } /** * Use cached version of image, if possible. * * @param boolean $useCache is default true, set to false to avoid using cached object. * * @return $this */ public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } /** * Error message when failing to load somehow corrupt image. * * @return void * */ public function failedToLoad() { header("HTTP/1.0 404 Not Found"); echo("CImage.php says 404: Fatal error when opening image.
{$log}
EOD;
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
}
/**
* Resize and crop images on the fly, store generated images in a cache.
*
* @author Mikael Roos mos@dbwebb.se
* @example http://dbwebb.se/opensource/cimage
* @link https://github.com/mosbth/cimage
*
*/
$version = "v0.7.0 (2015-02-10)";
/**
* Default configuration options, can be overridden in own config-file.
*
* @param string $msg to display.
*
* @return void
*/
function errorPage($msg)
{
global $mode;
header("HTTP/1.0 500 Internal Server Error");
if ($mode == 'development') {
die("[img.php] $msg");
} else {
error_log("[img.php] $msg");
die("HTTP/1.0 500 Internal Server Error");
}
}
/**
* Custom exception handler.
*/
set_exception_handler(function ($exception) {
errorPage("img.php: Uncaught exception:
" . $exception->getMessage() . "
" . $exception->getTraceAsString(), ""); }); /** * Get input from query string or return default value if not set. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $default value to return when $key is not set in $_GET. * * @return mixed value from $_GET or default value. */ function get($key, $default = null) { if (is_array($key)) { foreach ($key as $val) { if (isset($_GET[$val])) { return $_GET[$val]; } } } elseif (isset($_GET[$key])) { return $_GET[$key]; } return $default; } /** * Get input from query string and set to $defined if defined or else $undefined. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $defined value to return when $key is set in $_GET. * @param mixed $undefined value to return when $key is not set in $_GET. * * @return mixed value as $defined or $undefined. */ function getDefined($key, $defined, $undefined) { return get($key) === null ? $undefined : $defined; } /** * Get value from config array or default if key is not set in config array. * * @param string $key the key in the config array. * @param mixed $default value to be default if $key is not set in config. * * @return mixed value as $config[$key] or $default. */ function getConfig($key, $default) { global $config; return isset($config[$key]) ? $config[$key] : $default; } /** * Log when verbose mode, when used without argument it returns the result. * * @param string $msg to log. * * @return void or array. */ function verbose($msg = null) { global $verbose; static $log = array(); if (!$verbose) { return; } if (is_null($msg)) { return $log; } $log[] = $msg; } /** * Get configuration options from file, if the file exists, else use $config * if its defined or create an empty $config. */ $configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php'; if (is_file($configFile)) { $config = require $configFile; } else if (!isset($config)) { $config = array(); } /** * verbose, v - do a verbose dump of what happens */ $verbose = getDefined(array('verbose', 'v'), true, false); verbose("img.php version = $version"); /** * Set mode as strict, production or development. * Default is production environment. */ $mode = getConfig('mode', 'production'); // Settings for any mode set_time_limit(20); ini_set('gd.jpeg_ignore_warning', 1); if (!extension_loaded('gd')) { errorPage("Extension gd is nod loaded."); } // Specific settings for each mode if ($mode == 'strict') { error_reporting(0); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'production') { error_reporting(-1); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'development') { error_reporting(-1); ini_set('display_errors', 1); ini_set('log_errors', 0); } else { errorPage("Unknown mode: $mode"); } verbose("mode = $mode"); verbose("error log = " . ini_get('error_log')); /** * Set default timezone if not set or if its set in the config-file. */ $defaultTimezone = getConfig('default_timezone', null); if ($defaultTimezone) { date_default_timezone_set($defaultTimezone); } else if (!ini_get('default_timezone')) { date_default_timezone_set('UTC'); } /** * Check if passwords are configured, used and match. * Options decide themself if they require passwords to be used. */ $pwdConfig = getConfig('password', false); $pwdAlways = getConfig('password_always', false); $pwd = get(array('password', 'pwd'), null); // Check if passwords match, if configured to use passwords $passwordMatch = null; if ($pwdAlways) { $passwordMatch = ($pwdConfig === $pwd); if (!$passwordMatch) { errorPage("Password required and does not match or exists."); } } elseif ($pwdConfig && $pwd) { $passwordMatch = ($pwdConfig === $pwd); } verbose("password match = $passwordMatch"); /** * Prevent hotlinking, leeching, of images by controlling who access them * from where. * */ $allowHotlinking = getConfig('allow_hotlinking', true); $hotlinkingWhitelist = getConfig('hotlinking_whitelist', array()); $serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $refererHost = parse_url($referer, PHP_URL_HOST); if (!$allowHotlinking) { if ($passwordMatch) { ; // Always allow when password match } else if ($passwordMatch === false) { errorPage("Hotlinking/leeching not allowed when password missmatch."); } else if (!$referer) { errorPage("Hotlinking/leeching not allowed and referer is missing."); } else if (strcmp($serverName, $refererHost) == 0) { ; // Allow when serverName matches refererHost } else if (!empty($hotlinkingWhitelist)) { $allowedByWhitelist = false; foreach ($hotlinkingWhitelist as $val) { if (preg_match($val, $refererHost)) { $allowedByWhitelist = true; } } if (!$allowedByWhitelist) { errorPage("Hotlinking/leeching not allowed by whitelist."); } } else { errorPage("Hotlinking/leeching not allowed."); } } verbose("allow_hotlinking = $allowHotlinking"); verbose("referer = $referer"); verbose("referer host = $refererHost"); /** * Get the source files. */ $autoloader = getConfig('autoloader', false); $cimageClass = getConfig('cimage_class', false); if ($autoloader) { require $autoloader; } else if ($cimageClass) { require $cimageClass; } /** * Create the class for the image. */ $img = new CImage(); $img->setVerbose($verbose); /** * Allow or disallow remote download of images from other servers. * Passwords apply if used. * */ $allowRemote = getConfig('remote_allow', false); if ($allowRemote && $passwordMatch !== false) { $pattern = getConfig('remote_pattern', null); $img->setRemoteDownload($allowRemote, $pattern); } /** * shortcut, sc - extend arguments with a constant value, defined * in config-file. */ $shortcut = get(array('shortcut', 'sc'), null); $shortcutConfig = getConfig('shortcut', array( 'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen", )); verbose("shortcut = $shortcut"); if (isset($shortcut) && isset($shortcutConfig[$shortcut])) { parse_str($shortcutConfig[$shortcut], $get); verbose("shortcut-constant = {$shortcutConfig[$shortcut]}"); $_GET = array_merge($_GET, $get); } /** * src - the source image file. */ $srcImage = get('src') or errorPage('Must set src-attribute.'); // Check for valid/invalid characters $imagePath = getConfig('image_path', __DIR__ . '/img/'); $imagePathConstraint = getConfig('image_path_constraint', true); $validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_\.:]+$#'); preg_match($validFilename, $srcImage) or errorPage('Filename contains invalid characters.'); if ($allowRemote && $img->isRemoteSource($srcImage)) { // If source is a remote file, ignore local file checks. } else if ($imagePathConstraint) { // Check that the image is a file below the directory 'image_path'. $pathToImage = realpath($imagePath . $srcImage); $imageDir = realpath($imagePath); is_file($pathToImage) or errorPage( 'Source image is not a valid file, check the filename and that a matching file exists on the filesystem.' ); substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0 or errorPage( 'Security constraint: Source image is not below the directory "image_path" as specified in the config file img_config.php.' ); } verbose("src = $srcImage"); /** * Manage size constants from config file, use constants to replace values * for width and height. */ $sizeConstant = getConfig('size_constant', function () { // Set sizes to map constant to value, easier to use with width or height $sizes = array( 'w1' => 613, 'w2' => 630, ); // Add grid column width, useful for use as predefined size for width (or height). $gridColumnWidth = 30; $gridGutterWidth = 10; $gridColumns = 24; for ($i = 1; $i <= $gridColumns; $i++) { $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth; } return $sizes; }); $sizes = call_user_func($sizeConstant); /** * width, w - set target width, affecting the resulting image width, height and resize options */ $newWidth = get(array('width', 'w')); $maxWidth = getConfig('max_width', 2000); // Check to replace predefined size if (isset($sizes[$newWidth])) { $newWidth = $sizes[$newWidth]; } // Support width as % of original width if ($newWidth[strlen($newWidth)-1] == '%') { is_numeric(substr($newWidth, 0, -1)) or errorPage('Width % not numeric.'); } else { is_null($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('Width out of range.'); } verbose("new width = $newWidth"); /** * height, h - set target height, affecting the resulting image width, height and resize options */ $newHeight = get(array('height', 'h')); $maxHeight = getConfig('max_height', 2000); // Check to replace predefined size if (isset($sizes[$newHeight])) { $newHeight = $sizes[$newHeight]; } // height if ($newHeight[strlen($newHeight)-1] == '%') { is_numeric(substr($newHeight, 0, -1)) or errorPage('Height % out of range.'); } else { is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Hight out of range.'); } verbose("new height = $newHeight"); /** * aspect-ratio, ar - affecting the resulting image width, height and resize options */ $aspectRatio = get(array('aspect-ratio', 'ar')); $aspectRatioConstant = getConfig('aspect_ratio_constant', function () { return array( '3:1' => 3/1, '3:2' => 3/2, '4:3' => 4/3, '8:5' => 8/5, '16:10' => 16/10, '16:9' => 16/9, 'golden' => 1.618, ); }); // Check to replace predefined aspect ratio $aspectRatios = call_user_func($aspectRatioConstant); $negateAspectRatio = ($aspectRatio[0] == '!') ? true : false; $aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio; if (isset($aspectRatios[$aspectRatio])) { $aspectRatio = $aspectRatios[$aspectRatio]; } if ($negateAspectRatio) { $aspectRatio = 1 / $aspectRatio; } is_null($aspectRatio) or is_numeric($aspectRatio) or errorPage('Aspect ratio out of range'); verbose("aspect ratio = $aspectRatio"); /** * crop-to-fit, cf - affecting the resulting image width, height and resize options */ $cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false); verbose("crop to fit = $cropToFit"); /** * Set default background color from config file. */ $backgroundColor = getConfig('background_color', null); if ($backgroundColor) { $img->setDefaultBackgroundColor($backgroundColor); verbose("Using default background_color = $backgroundColor"); } /** * bgColor - Default background color to use */ $bgColor = get(array('bgColor', 'bg-color', 'bgc'), null); verbose("bgColor = $bgColor"); /** * fill-to-fit, ff - affecting the resulting image width, height and resize options */ $fillToFit = get(array('fill-to-fit', 'ff'), null); verbose("fill-to-fit = $fillToFit"); if ($fillToFit !== null) { if (!empty($fillToFit)) { $bgColor = $fillToFit; verbose("fillToFit changed bgColor to = $bgColor"); } $fillToFit = true; verbose("fill-to-fit (fixed) = $fillToFit"); } /** * no-ratio, nr, stretch - affecting the resulting image width, height and resize options */ $keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true); verbose("keep ratio = $keepRatio"); /** * crop, c - affecting the resulting image width, height and resize options */ $crop = get(array('crop', 'c')); verbose("crop = $crop"); /** * area, a - affecting the resulting image width, height and resize options */ $area = get(array('area', 'a')); verbose("area = $area"); /** * skip-original, so - skip the original image and always process a new image */ $useOriginal = getDefined(array('skip-original', 'so'), false, true); verbose("use original = $useOriginal"); /** * no-cache, nc - skip the cached version and process and create a new version in cache. */ $useCache = getDefined(array('no-cache', 'nc'), false, true); verbose("use cache = $useCache"); /** * quality, q - set level of quality for jpeg images */ $quality = get(array('quality', 'q')); is_null($quality) or ($quality > 0 and $quality <= 100) or errorPage('Quality out of range'); verbose("quality = $quality"); /** * compress, co - what strategy to use when compressing png images */ $compress = get(array('compress', 'co')); is_null($compress) or ($compress > 0 and $compress <= 9) or errorPage('Compress out of range'); verbose("compress = $compress"); /** * save-as, sa - what type of image to save */ $saveAs = get(array('save-as', 'sa')); verbose("save as = $saveAs"); /** * scale, s - Processing option, scale up or down the image prior actual resize */ $scale = get(array('scale', 's')); is_null($scale) or ($scale >= 0 and $scale <= 400) or errorPage('Scale out of range'); verbose("scale = $scale"); /** * palette, p - Processing option, create a palette version of the image */ $palette = getDefined(array('palette', 'p'), true, false); verbose("palette = $palette"); /** * sharpen - Processing option, post filter for sharpen effect */ $sharpen = getDefined('sharpen', true, null); verbose("sharpen = $sharpen"); /** * emboss - Processing option, post filter for emboss effect */ $emboss = getDefined('emboss', true, null); verbose("emboss = $emboss"); /** * blur - Processing option, post filter for blur effect */ $blur = getDefined('blur', true, null); verbose("blur = $blur"); /** * rotateBefore - Rotate the image with an angle, before processing */ $rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb')); is_null($rotateBefore) or ($rotateBefore >= -360 and $rotateBefore <= 360) or errorPage('RotateBefore out of range'); verbose("rotateBefore = $rotateBefore"); /** * rotateAfter - Rotate the image with an angle, before processing */ $rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r')); is_null($rotateAfter) or ($rotateAfter >= -360 and $rotateAfter <= 360) or errorPage('RotateBefore out of range'); verbose("rotateAfter = $rotateAfter"); /** * autoRotate - Auto rotate based on EXIF information */ $autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false); verbose("autoRotate = $autoRotate"); /** * filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter() */ $filters = array(); $filter = get(array('filter', 'f')); if ($filter) { $filters[] = $filter; } for ($i = 0; $i < 10; $i++) { $filter = get(array("filter{$i}", "f{$i}")); if ($filter) { $filters[] = $filter; } } verbose("filters = " . print_r($filters, 1)); /** * json - output the image as a JSON object with details on the image. */ $outputFormat = getDefined('json', 'json', null); verbose("json = $outputFormat"); /** * dpr - change to get larger image to easier support larger dpr, such as retina. */ $dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1); verbose("dpr = $dpr"); /** * convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php */ $convolve = get('convolve', null); $convolutionConstant = getConfig('convolution_constant', array()); // Check if the convolve is matching an existing constant if ($convolve && isset($convolutionConstant)) { $img->addConvolveExpressions($convolutionConstant); verbose("convolve constant = " . print_r($convolutionConstant, 1)); } verbose("convolve = " . print_r($convolve, 1)); /** * no-upscale, nu - Do not upscale smaller image to larger dimension. */ $upscale = getDefined(array('no-upscale', 'nu'), false, true); verbose("upscale = $upscale"); /** * Get details for post processing */ $postProcessing = getConfig('postprocessing', array( 'png_filter' => false, 'png_filter_cmd' => '/usr/local/bin/optipng -q', 'png_deflate' => false, 'png_deflate_cmd' => '/usr/local/bin/pngout -q', 'jpeg_optimize' => false, 'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize', )); /** * alias - Save resulting image to another alias name. * Password always apply, must be defined. */ $alias = get('alias', null); $aliasPath = getConfig('alias_path', null); $validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#'); $aliasTarget = null; if ($alias && $aliasPath && $passwordMatch) { $aliasTarget = $aliasPath . $alias; $useCache = false; is_writable($aliasPath) or errorPage("Directory for alias is not writable."); preg_match($validAliasname, $alias) or errorPage('Filename for alias contains invalid characters. Do not add extension.'); } else if ($alias) { errorPage('Alias is not enabled in the config file or password not matching.'); } verbose("alias = $alias"); /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } /** * Get HTTP code of response. * * @return integer as HTTP status code or null if not available. */ public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } /** * Get file modification time of response. * * @return int as timestamp. */ public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } /** * Get content type. * * @return string as the content type or null if not existing or invalid. */ public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : null; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } /** * Get file modification time of response. * * @param mixed $default as default value (int seconds) if date is * missing in response header. * * @return int as timestamp or $default if Date is missing in * response header. */ public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } /** * Get max age of cachable item. * * @param mixed $default as default value if date is missing in response * header. * * @return int as timestamp or false if not available. */ public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { // max-age=2592000 $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } /** * Get body of response. * * @return string as body. */ public function getBody() { return $this->response['body']; } } /** * Get a image from a remote server using HTTP GET and If-Modified-Since. * */ class CRemoteImage { /** * Path to cache files. */ private $saveFolder = null; /** * Use cache or not. */ private $useCache = true; /** * HTTP object to aid in download file. */ private $http; /** * Status of the HTTP request. */ private $status; /** * Defalt age for cached items 60*60*24*7. */ private $defaultMaxAge = 604800; /** * Url of downloaded item. */ private $url; /** * Base name of cache file for downloaded item. */ private $fileName; /** * Filename for json-file with details of cached item. */ private $fileJson; /** * Filename for image-file. */ private $fileImage; /** * Cache details loaded from file. */ private $cache; /** * Constructor * */ public function __construct() { ; } /** * Get status of last HTTP request. * * @return int as status */ public function getStatus() { return $this->status; } /** * Get JSON details for cache item. * * @return array with json details on cache. */ public function getDetails() { return $this->cache; } /** * Set the path to the cache directory. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function setCache($path) { $this->saveFolder = $path; return $this; } /** * Check if cache is writable or throw exception. * * @return $this * * @throws Exception if cahce folder is not writable. */ public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } /** * Decide if the cache should be used or not before trying to download * a remote file. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Translate a content type to a file extension. * * @param string $type a valid content type. * * @return string as file extension or false if no match. */ function contentTypeToFileExtension($type) { $extension = array( 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', ); return isset($extension[$type]) ? $extension[$type] : false; } /** * Set header fields. * * @return $this */ function setHeaderFields() { $this->http->setHeader("User-Agent", "CImage/0.6 (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } /** * Save downloaded resource to cache. * * @return string as path to saved file or false if not saved. */ function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $extension = $this->contentTypeToFileExtension($type); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['File-Extension'] = $extension; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } if ($extension) { $this->fileImage = $this->fileName . "." . $extension; // Save only if body is a valid image $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileImage, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileImage; } } return false; } /** * Got a 304 and updates cache with new age. * * @return string as path to cached file. */ function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileImage; } /** * Download a remote file and keep a cache of downloaded files. * * @param string $url a remote url. * * @return string as path to downloaded file or false if failed. */ function download($url) { $this->http = new CHttpGet(); $this->url = $url; // First check if the cache is valid and can be used $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } // Do a HTTP request to download item $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } else if ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } return false; } /** * Get the path to the cached image file if the cache is valid. * * @return $this */ public function loadCacheDetails() { $cacheFile = str_replace(array("/", ":", "#", ".", "?"), "-", $this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } /** * Get the path to the cached image file if the cache is valid. * * @return string as the path ot the image file or false if no cache. */ public function getCachedSource() { $this->fileImage = $this->fileName . "." . $this->cache['File-Extension']; $imageExists = is_readable($this->fileImage); // Is cache valid? $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileImage; } // Prepare for a 304 if available if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } /** * Resize and crop images on the fly, store generated images in a cache. * * @author Mikael Roos mos@dbwebb.se * @example http://dbwebb.se/opensource/cimage * @link https://github.com/mosbth/cimage */ class CImage { /** * Constants type of PNG image */ const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; /** * Constant for default image quality when not set */ const JPEG_QUALITY_DEFAULT = 60; /** * Quality level for JPEG images. */ private $quality; /** * Is the quality level set from external use (true) or is it default (false)? */ private $useQuality = false; /** * Constant for default image quality when not set */ const PNG_COMPRESSION_DEFAULT = -1; /** * Compression level for PNG images. */ private $compress; /** * Is the compress level set from external use (true) or is it default (false)? */ private $useCompress = false; /** * Default background color, red, green, blue, alpha. * * @todo remake when upgrading to PHP 5.5 */ /* const BACKGROUND_COLOR = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, );*/ /** * Default background color to use. * * @todo remake when upgrading to PHP 5.5 */ //private $bgColorDefault = self::BACKGROUND_COLOR; private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); /** * Background color to use, specified as part of options. */ private $bgColor; /** * Where to save the target file. */ private $saveFolder; /** * The working image object. */ private $image; /** * The root folder of images (only used in constructor to create $pathToImage?). */ private $imageFolder; /** * Image filename, may include subdirectory, relative from $imageFolder */ private $imageSrc; /** * Actual path to the image, $imageFolder . '/' . $imageSrc */ private $pathToImage; /** * Original file extension */ private $fileExtension; /** * File extension to use when saving image. */ private $extension; /** * Output format, supports null (image) or json. */ private $outputFormat = null; /** * Verbose mode to print out a trace and display the created image */ private $verbose = false; /** * Keep a log/trace on what happens */ private $log = array(); /** * Handle image as palette image */ private $palette; /** * Target filename, with path, to save resulting image in. */ private $cacheFileName; /** * Set a format to save image as, or null to use original format. */ private $saveAs; /** * Path to command for filter optimize, for example optipng or null. */ private $pngFilter; /** * Path to command for deflate optimize, for example pngout or null. */ private $pngDeflate; /** * Path to command to optimize jpeg images, for example jpegtran or null. */ private $jpegOptimize; /** * Image dimensions, calculated from loaded image. */ private $width; // Calculated from source image private $height; // Calculated from source image /** * New image dimensions, incoming as argument or calculated. */ private $newWidth; private $newWidthOrig; // Save original value private $newHeight; private $newHeightOrig; // Save original value /** * Change target height & width when different dpr, dpr 2 means double image dimensions. */ private $dpr = 1; /** * Always upscale images, even if they are smaller than target image. */ const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; /** * Array with details on how to crop, incoming as argument and calculated. */ public $crop; public $cropOrig; // Save original value /** * String with details on how to do image convolution. String * should map a key in the $convolvs array or be a string of * 11 float values separated by comma. The first nine builds * up the matrix, then divisor and last offset. */ private $convolve; /** * Custom convolution expressions, matrix 3x3, divisor and offset. */ private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); /** * Resize strategy to fill extra area with background color. * True or false. */ private $fillToFit; /** * Used with option area to set which parts of the image to use. */ private $offset; /** * Calculate target dimension for image when using fill-to-fit resize strategy. */ private $fillWidth; private $fillHeight; /** * Allow remote file download, default is to disallow remote file download. */ private $allowRemote = false; /** * Pattern to recognize a remote file. */ //private $remotePattern = '#^[http|https]://#'; private $remotePattern = '#^https?://#'; /** * Use the cache if true, set to false to ignore the cached file. */ private $useCache = true; /** * Properties, the class is mutable and the method setOptions() * decides (partly) what properties are created. * * @todo Clean up these and check if and how they are used */ public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $type; // Calculated from source image private $attr; // Calculated from source image private $useOriginal; // Use original image if possible /** * Constructor, can take arguments to init the object. * * @param string $imageSrc filename which may contain subdirectory. * @param string $imageFolder path to root folder for images. * @param string $saveFolder path to folder where to save the new file or null to skip saving. * @param string $saveName name of target file when saveing. */ public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } /** * Set verbose mode. * * @param boolean $mode true or false to enable and disable verbose mode, * default is true. * * @return $this */ public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } /** * Set save folder, base folder for saving cache files. * * @todo clean up how $this->saveFolder is used in other methods. * * @param string $path where to store cached files. * * @return $this */ public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } /** * Use cache or not. * * @todo clean up how $this->noCache is used in other methods. * * @param string $use true or false to use cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Allow or disallow remote image download. * * @param boolean $allow true or false to enable and disable. * @param string $pattern to use to detect if its a remote file. * * @return $this */ public function setRemoteDownload($allow, $pattern = null) { $this->allowRemote = $allow; $this->remotePattern = $pattern ? $pattern : $this->remotePattern; $this->log("Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern); return $this; } /** * Check if the image resource is a remote file or not. * * @param string $src check if src is remote. * * @return boolean true if $src is a remote file, else false. */ public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return $remote; } /** * Check if file extension is valid as a file extension. * * @param string $extension of image file. * * @return $this */ private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } /** * Download a remote image and return path to its local copy. * * @param string $src remote path to image. * * @return string as path to downloaded remote source. */ public function downloadRemoteSource($src) { $remote = new CRemoteImage(); $cache = $this->saveFolder . "/remote/"; if (!is_dir($cache)) { if (!is_writable($this->saveFolder)) { throw new Exception("Can not create remote cache, cachefolder not writable."); } mkdir($cache); $this->log("The remote cache does not exists, creating it."); } if (!is_writable($cache)) { $this->log("The remote cache is not writable."); } $remote->setCache($cache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item has local cached file: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } /** * Set src file. * * @param string $src of image. * @param string $dir as base directory where images are. * * @return $this */ public function setSource($src, $dir = null) { if (!isset($src)) { return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $this->imageFolder = rtrim($dir, '/'); $this->pathToImage = $this->imageFolder . '/' . $this->imageSrc; $this->fileExtension = strtolower(pathinfo($this->pathToImage, PATHINFO_EXTENSION)); //$this->extension = $this->fileExtension; $this->checkFileExtension($this->fileExtension); return $this; } /** * Set target file. * * @param string $src of target image. * @param string $dir as base directory where images are stored. * * @return $this */ public function setTarget($src = null, $dir = null) { if (!(isset($src) && isset($dir))) { return $this; } $this->saveFolder = $dir; $this->cacheFileName = $dir . '/' . $src; /* Allow readonly cache is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); */ // Sanitize filename $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } /** * Set options to use when processing image. * * @param array $args used when processing image. * * @return $this */ public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( // Options for calculate dimensions 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), 'area' => null, //'0,0,0,0', 'upscale' => self::UPSCALE_DEFAULT, // Options for caching or using original 'useCache' => true, 'useOriginal' => true, // Pre-processing, before resizing is done 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, // General options 'bgColor' => null, // Post-processing, after resizing is done 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, // Output format 'outputFormat' => null, 'dpr' => 1, // Options for saving //'quality' => null, //'compress' => null, //'saveAs' => null, ); // Convert crop settings from string to array if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } // Convert area settings from string to array if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } // Convert filter settings from array of string to array of array if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } // Merge default arguments with incoming and set properties. //$args = array_merge_recursive($defaults, $args); $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } // Save original values to enable re-calculating $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } /** * Map filter name to PHP filter and id. * * @param string $name the name of the filter. * * @return array with filter settings * @throws Exception */ private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } /** * Load image details from original image file. * * @param string $file the file to load or null to use $this->pathToImage. * * @return $this * @throws Exception */ public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; is_readable($file) or $this->raiseError('Image file does not exist.'); // Get details on image $info = list($this->width, $this->height, $this->type, $this->attr) = getimagesize($file); !empty($info) or $this->raiseError("The file doesn't seem to be an image."); if ($this->verbose) { $this->log("Image file: {$file}"); $this->log("Image width x height (type): {$this->width} x {$this->height} ({$this->type})."); $this->log("Image filesize: " . filesize($file) . " bytes."); } return $this; } /** * Init new width and height and do some sanity checks on constraints, before any * processing can be done. * * @return $this * @throws Exception */ public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // width as % if ($this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } // height as % if ($this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); // width & height from aspect ratio if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } // Change width & height based on dpr if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } // Check values to be within domain is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Calculate new width and height of image, based on settings. * * @return $this */ public function calculateNewWidthAndHeight() { // Crop, use cropped width and height as base for calulations $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // Check if there is an area to crop off if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; // Check if crop is set if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } // Calculate new width and height if keeping aspect-ratio. if ($this->keepRatio) { $this->log("Keep aspect ratio."); // Crop-to-fit and both new width and height are set. if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { // Use newWidth and newHeigh as width/height, image should fit in box. $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { // Both new width and height are set. // Use newWidth and newHeigh as max width/height, image should not be larger. $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { // Use new width as max-width $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { // Use new height as max-hight $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } // Get image dimensions for pre-resize image. if ($this->cropToFit || $this->fillToFit) { // Get relations of original & target image $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } else if ($this->fillToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } // Crop, ensure to set new width and height if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } // Fill to fit, ensure to set new width and height /*if ($this->fillToFit) { $this->log("FillToFit."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); }*/ // No new height or width is set, use existing measures. $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Re-calculate image dimensions when original image dimension has changed. * * @return $this */ public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } /** * Set extension for filename to save as. * * @param string $saveas extension to save image as * * @return $this */ public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image using as: " . $this->extension); return $this; } /** * Set JPEG quality to use when saving image * * @param int $quality as the quality to set. * * @return $this */ public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } /** * Set PNG compressen algorithm to use when saving image * * @param int $compress as the algorithm to use. * * @return $this */ public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } /** * Use original image if possible, check options which affects image processing. * * @param boolean $useOrig default is to use original if possible, else set to false. * * @return $this */ public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } /** * Generate filename to save file in cache. * * @param string $base as basepath for storing file. * * @return $this */ public function generateFilename($base) { $parts = pathinfo($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $width = $this->newWidth; $height = $this->newHeight; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= ":".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $this->extension = isset($this->extension) ? $this->extension : $parts['extension']; $optimize = null; if ($this->extension == 'jpeg' || $this->extension == 'jpg') { $optimize = $this->jpegOptimize ? 'o' : null; } elseif ($this->extension == 'png') { $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; } $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $file = $subdir . '_' . $parts['filename'] . '_' . $width . '_' . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . '.' . $this->extension; return $this->setTarget($file, $base); } /** * Use cached version of image, if possible. * * @param boolean $useCache is default true, set to false to avoid using cached object. * * @return $this */ public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } /** * Error message when failing to load somehow corrupt image. * * @return void * */ public function failedToLoad() { header("HTTP/1.0 404 Not Found"); echo("CImage.php says 404: Fatal error when opening image.
{$log}
EOD;
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
}
/**
* Resize and crop images on the fly, store generated images in a cache.
*
* @author Mikael Roos mos@dbwebb.se
* @example http://dbwebb.se/opensource/cimage
* @link https://github.com/mosbth/cimage
*
*/
$version = "v0.7.0 (2015-02-10)";
/**
* Default configuration options, can be overridden in own config-file.
*
* @param string $msg to display.
*
* @return void
*/
function errorPage($msg)
{
global $mode;
header("HTTP/1.0 500 Internal Server Error");
if ($mode == 'development') {
die("[img.php] $msg");
} else {
error_log("[img.php] $msg");
die("HTTP/1.0 500 Internal Server Error");
}
}
/**
* Custom exception handler.
*/
set_exception_handler(function ($exception) {
errorPage("img.php: Uncaught exception:
" . $exception->getMessage() . "
" . $exception->getTraceAsString(), ""); }); /** * Get input from query string or return default value if not set. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $default value to return when $key is not set in $_GET. * * @return mixed value from $_GET or default value. */ function get($key, $default = null) { if (is_array($key)) { foreach ($key as $val) { if (isset($_GET[$val])) { return $_GET[$val]; } } } elseif (isset($_GET[$key])) { return $_GET[$key]; } return $default; } /** * Get input from query string and set to $defined if defined or else $undefined. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $defined value to return when $key is set in $_GET. * @param mixed $undefined value to return when $key is not set in $_GET. * * @return mixed value as $defined or $undefined. */ function getDefined($key, $defined, $undefined) { return get($key) === null ? $undefined : $defined; } /** * Get value from config array or default if key is not set in config array. * * @param string $key the key in the config array. * @param mixed $default value to be default if $key is not set in config. * * @return mixed value as $config[$key] or $default. */ function getConfig($key, $default) { global $config; return isset($config[$key]) ? $config[$key] : $default; } /** * Log when verbose mode, when used without argument it returns the result. * * @param string $msg to log. * * @return void or array. */ function verbose($msg = null) { global $verbose; static $log = array(); if (!$verbose) { return; } if (is_null($msg)) { return $log; } $log[] = $msg; } /** * Get configuration options from file, if the file exists, else use $config * if its defined or create an empty $config. */ $configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php'; if (is_file($configFile)) { $config = require $configFile; } else if (!isset($config)) { $config = array(); } /** * verbose, v - do a verbose dump of what happens */ $verbose = getDefined(array('verbose', 'v'), true, false); verbose("img.php version = $version"); /** * Set mode as strict, production or development. * Default is production environment. */ $mode = getConfig('mode', 'production'); // Settings for any mode set_time_limit(20); ini_set('gd.jpeg_ignore_warning', 1); if (!extension_loaded('gd')) { errorPage("Extension gd is nod loaded."); } // Specific settings for each mode if ($mode == 'strict') { error_reporting(0); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'production') { error_reporting(-1); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'development') { error_reporting(-1); ini_set('display_errors', 1); ini_set('log_errors', 0); } else { errorPage("Unknown mode: $mode"); } verbose("mode = $mode"); verbose("error log = " . ini_get('error_log')); /** * Set default timezone if not set or if its set in the config-file. */ $defaultTimezone = getConfig('default_timezone', null); if ($defaultTimezone) { date_default_timezone_set($defaultTimezone); } else if (!ini_get('default_timezone')) { date_default_timezone_set('UTC'); } /** * Check if passwords are configured, used and match. * Options decide themself if they require passwords to be used. */ $pwdConfig = getConfig('password', false); $pwdAlways = getConfig('password_always', false); $pwd = get(array('password', 'pwd'), null); // Check if passwords match, if configured to use passwords $passwordMatch = null; if ($pwdAlways) { $passwordMatch = ($pwdConfig === $pwd); if (!$passwordMatch) { errorPage("Password required and does not match or exists."); } } elseif ($pwdConfig && $pwd) { $passwordMatch = ($pwdConfig === $pwd); } verbose("password match = $passwordMatch"); /** * Prevent hotlinking, leeching, of images by controlling who access them * from where. * */ $allowHotlinking = getConfig('allow_hotlinking', true); $hotlinkingWhitelist = getConfig('hotlinking_whitelist', array()); $serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $refererHost = parse_url($referer, PHP_URL_HOST); if (!$allowHotlinking) { if ($passwordMatch) { ; // Always allow when password match } else if ($passwordMatch === false) { errorPage("Hotlinking/leeching not allowed when password missmatch."); } else if (!$referer) { errorPage("Hotlinking/leeching not allowed and referer is missing."); } else if (strcmp($serverName, $refererHost) == 0) { ; // Allow when serverName matches refererHost } else if (!empty($hotlinkingWhitelist)) { $allowedByWhitelist = false; foreach ($hotlinkingWhitelist as $val) { if (preg_match($val, $refererHost)) { $allowedByWhitelist = true; } } if (!$allowedByWhitelist) { errorPage("Hotlinking/leeching not allowed by whitelist."); } } else { errorPage("Hotlinking/leeching not allowed."); } } verbose("allow_hotlinking = $allowHotlinking"); verbose("referer = $referer"); verbose("referer host = $refererHost"); /** * Get the source files. */ $autoloader = getConfig('autoloader', false); $cimageClass = getConfig('cimage_class', false); if ($autoloader) { require $autoloader; } else if ($cimageClass) { require $cimageClass; } /** * Create the class for the image. */ $img = new CImage(); $img->setVerbose($verbose); /** * Allow or disallow remote download of images from other servers. * Passwords apply if used. * */ $allowRemote = getConfig('remote_allow', false); if ($allowRemote && $passwordMatch !== false) { $pattern = getConfig('remote_pattern', null); $img->setRemoteDownload($allowRemote, $pattern); } /** * shortcut, sc - extend arguments with a constant value, defined * in config-file. */ $shortcut = get(array('shortcut', 'sc'), null); $shortcutConfig = getConfig('shortcut', array( 'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen", )); verbose("shortcut = $shortcut"); if (isset($shortcut) && isset($shortcutConfig[$shortcut])) { parse_str($shortcutConfig[$shortcut], $get); verbose("shortcut-constant = {$shortcutConfig[$shortcut]}"); $_GET = array_merge($_GET, $get); } /** * src - the source image file. */ $srcImage = get('src') or errorPage('Must set src-attribute.'); // Check for valid/invalid characters $imagePath = getConfig('image_path', __DIR__ . '/img/'); $imagePathConstraint = getConfig('image_path_constraint', true); $validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_\.:]+$#'); preg_match($validFilename, $srcImage) or errorPage('Filename contains invalid characters.'); if ($allowRemote && $img->isRemoteSource($srcImage)) { // If source is a remote file, ignore local file checks. } else if ($imagePathConstraint) { // Check that the image is a file below the directory 'image_path'. $pathToImage = realpath($imagePath . $srcImage); $imageDir = realpath($imagePath); is_file($pathToImage) or errorPage( 'Source image is not a valid file, check the filename and that a matching file exists on the filesystem.' ); substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0 or errorPage( 'Security constraint: Source image is not below the directory "image_path" as specified in the config file img_config.php.' ); } verbose("src = $srcImage"); /** * Manage size constants from config file, use constants to replace values * for width and height. */ $sizeConstant = getConfig('size_constant', function () { // Set sizes to map constant to value, easier to use with width or height $sizes = array( 'w1' => 613, 'w2' => 630, ); // Add grid column width, useful for use as predefined size for width (or height). $gridColumnWidth = 30; $gridGutterWidth = 10; $gridColumns = 24; for ($i = 1; $i <= $gridColumns; $i++) { $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth; } return $sizes; }); $sizes = call_user_func($sizeConstant); /** * width, w - set target width, affecting the resulting image width, height and resize options */ $newWidth = get(array('width', 'w')); $maxWidth = getConfig('max_width', 2000); // Check to replace predefined size if (isset($sizes[$newWidth])) { $newWidth = $sizes[$newWidth]; } // Support width as % of original width if ($newWidth[strlen($newWidth)-1] == '%') { is_numeric(substr($newWidth, 0, -1)) or errorPage('Width % not numeric.'); } else { is_null($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('Width out of range.'); } verbose("new width = $newWidth"); /** * height, h - set target height, affecting the resulting image width, height and resize options */ $newHeight = get(array('height', 'h')); $maxHeight = getConfig('max_height', 2000); // Check to replace predefined size if (isset($sizes[$newHeight])) { $newHeight = $sizes[$newHeight]; } // height if ($newHeight[strlen($newHeight)-1] == '%') { is_numeric(substr($newHeight, 0, -1)) or errorPage('Height % out of range.'); } else { is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Hight out of range.'); } verbose("new height = $newHeight"); /** * aspect-ratio, ar - affecting the resulting image width, height and resize options */ $aspectRatio = get(array('aspect-ratio', 'ar')); $aspectRatioConstant = getConfig('aspect_ratio_constant', function () { return array( '3:1' => 3/1, '3:2' => 3/2, '4:3' => 4/3, '8:5' => 8/5, '16:10' => 16/10, '16:9' => 16/9, 'golden' => 1.618, ); }); // Check to replace predefined aspect ratio $aspectRatios = call_user_func($aspectRatioConstant); $negateAspectRatio = ($aspectRatio[0] == '!') ? true : false; $aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio; if (isset($aspectRatios[$aspectRatio])) { $aspectRatio = $aspectRatios[$aspectRatio]; } if ($negateAspectRatio) { $aspectRatio = 1 / $aspectRatio; } is_null($aspectRatio) or is_numeric($aspectRatio) or errorPage('Aspect ratio out of range'); verbose("aspect ratio = $aspectRatio"); /** * crop-to-fit, cf - affecting the resulting image width, height and resize options */ $cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false); verbose("crop to fit = $cropToFit"); /** * Set default background color from config file. */ $backgroundColor = getConfig('background_color', null); if ($backgroundColor) { $img->setDefaultBackgroundColor($backgroundColor); verbose("Using default background_color = $backgroundColor"); } /** * bgColor - Default background color to use */ $bgColor = get(array('bgColor', 'bg-color', 'bgc'), null); verbose("bgColor = $bgColor"); /** * fill-to-fit, ff - affecting the resulting image width, height and resize options */ $fillToFit = get(array('fill-to-fit', 'ff'), null); verbose("fill-to-fit = $fillToFit"); if ($fillToFit !== null) { if (!empty($fillToFit)) { $bgColor = $fillToFit; verbose("fillToFit changed bgColor to = $bgColor"); } $fillToFit = true; verbose("fill-to-fit (fixed) = $fillToFit"); } /** * no-ratio, nr, stretch - affecting the resulting image width, height and resize options */ $keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true); verbose("keep ratio = $keepRatio"); /** * crop, c - affecting the resulting image width, height and resize options */ $crop = get(array('crop', 'c')); verbose("crop = $crop"); /** * area, a - affecting the resulting image width, height and resize options */ $area = get(array('area', 'a')); verbose("area = $area"); /** * skip-original, so - skip the original image and always process a new image */ $useOriginal = getDefined(array('skip-original', 'so'), false, true); verbose("use original = $useOriginal"); /** * no-cache, nc - skip the cached version and process and create a new version in cache. */ $useCache = getDefined(array('no-cache', 'nc'), false, true); verbose("use cache = $useCache"); /** * quality, q - set level of quality for jpeg images */ $quality = get(array('quality', 'q')); is_null($quality) or ($quality > 0 and $quality <= 100) or errorPage('Quality out of range'); verbose("quality = $quality"); /** * compress, co - what strategy to use when compressing png images */ $compress = get(array('compress', 'co')); is_null($compress) or ($compress > 0 and $compress <= 9) or errorPage('Compress out of range'); verbose("compress = $compress"); /** * save-as, sa - what type of image to save */ $saveAs = get(array('save-as', 'sa')); verbose("save as = $saveAs"); /** * scale, s - Processing option, scale up or down the image prior actual resize */ $scale = get(array('scale', 's')); is_null($scale) or ($scale >= 0 and $scale <= 400) or errorPage('Scale out of range'); verbose("scale = $scale"); /** * palette, p - Processing option, create a palette version of the image */ $palette = getDefined(array('palette', 'p'), true, false); verbose("palette = $palette"); /** * sharpen - Processing option, post filter for sharpen effect */ $sharpen = getDefined('sharpen', true, null); verbose("sharpen = $sharpen"); /** * emboss - Processing option, post filter for emboss effect */ $emboss = getDefined('emboss', true, null); verbose("emboss = $emboss"); /** * blur - Processing option, post filter for blur effect */ $blur = getDefined('blur', true, null); verbose("blur = $blur"); /** * rotateBefore - Rotate the image with an angle, before processing */ $rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb')); is_null($rotateBefore) or ($rotateBefore >= -360 and $rotateBefore <= 360) or errorPage('RotateBefore out of range'); verbose("rotateBefore = $rotateBefore"); /** * rotateAfter - Rotate the image with an angle, before processing */ $rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r')); is_null($rotateAfter) or ($rotateAfter >= -360 and $rotateAfter <= 360) or errorPage('RotateBefore out of range'); verbose("rotateAfter = $rotateAfter"); /** * autoRotate - Auto rotate based on EXIF information */ $autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false); verbose("autoRotate = $autoRotate"); /** * filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter() */ $filters = array(); $filter = get(array('filter', 'f')); if ($filter) { $filters[] = $filter; } for ($i = 0; $i < 10; $i++) { $filter = get(array("filter{$i}", "f{$i}")); if ($filter) { $filters[] = $filter; } } verbose("filters = " . print_r($filters, 1)); /** * json - output the image as a JSON object with details on the image. */ $outputFormat = getDefined('json', 'json', null); verbose("json = $outputFormat"); /** * dpr - change to get larger image to easier support larger dpr, such as retina. */ $dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1); verbose("dpr = $dpr"); /** * convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php */ $convolve = get('convolve', null); $convolutionConstant = getConfig('convolution_constant', array()); // Check if the convolve is matching an existing constant if ($convolve && isset($convolutionConstant)) { $img->addConvolveExpressions($convolutionConstant); verbose("convolve constant = " . print_r($convolutionConstant, 1)); } verbose("convolve = " . print_r($convolve, 1)); /** * no-upscale, nu - Do not upscale smaller image to larger dimension. */ $upscale = getDefined(array('no-upscale', 'nu'), false, true); verbose("upscale = $upscale"); /** * Get details for post processing */ $postProcessing = getConfig('postprocessing', array( 'png_filter' => false, 'png_filter_cmd' => '/usr/local/bin/optipng -q', 'png_deflate' => false, 'png_deflate_cmd' => '/usr/local/bin/pngout -q', 'jpeg_optimize' => false, 'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize', )); /** * alias - Save resulting image to another alias name. * Password always apply, must be defined. */ $alias = get('alias', null); $aliasPath = getConfig('alias_path', null); $validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#'); $aliasTarget = null; if ($alias && $aliasPath && $passwordMatch) { $aliasTarget = $aliasPath . $alias; $useCache = false; is_writable($aliasPath) or errorPage("Directory for alias is not writable."); preg_match($validAliasname, $alias) or errorPage('Filename for alias contains invalid characters. Do not add extension.'); } else if ($alias) { errorPage('Alias is not enabled in the config file or password not matching.'); } verbose("alias = $alias"); /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } /** * Get HTTP code of response. * * @return integer as HTTP status code or null if not available. */ public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } /** * Get file modification time of response. * * @return int as timestamp. */ public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } /** * Get content type. * * @return string as the content type or null if not existing or invalid. */ public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : null; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } /** * Get file modification time of response. * * @param mixed $default as default value (int seconds) if date is * missing in response header. * * @return int as timestamp or $default if Date is missing in * response header. */ public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } /** * Get max age of cachable item. * * @param mixed $default as default value if date is missing in response * header. * * @return int as timestamp or false if not available. */ public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { // max-age=2592000 $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } /** * Get body of response. * * @return string as body. */ public function getBody() { return $this->response['body']; } } /** * Get a image from a remote server using HTTP GET and If-Modified-Since. * */ class CRemoteImage { /** * Path to cache files. */ private $saveFolder = null; /** * Use cache or not. */ private $useCache = true; /** * HTTP object to aid in download file. */ private $http; /** * Status of the HTTP request. */ private $status; /** * Defalt age for cached items 60*60*24*7. */ private $defaultMaxAge = 604800; /** * Url of downloaded item. */ private $url; /** * Base name of cache file for downloaded item. */ private $fileName; /** * Filename for json-file with details of cached item. */ private $fileJson; /** * Filename for image-file. */ private $fileImage; /** * Cache details loaded from file. */ private $cache; /** * Constructor * */ public function __construct() { ; } /** * Get status of last HTTP request. * * @return int as status */ public function getStatus() { return $this->status; } /** * Get JSON details for cache item. * * @return array with json details on cache. */ public function getDetails() { return $this->cache; } /** * Set the path to the cache directory. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function setCache($path) { $this->saveFolder = $path; return $this; } /** * Check if cache is writable or throw exception. * * @return $this * * @throws Exception if cahce folder is not writable. */ public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } /** * Decide if the cache should be used or not before trying to download * a remote file. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Translate a content type to a file extension. * * @param string $type a valid content type. * * @return string as file extension or false if no match. */ function contentTypeToFileExtension($type) { $extension = array( 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', ); return isset($extension[$type]) ? $extension[$type] : false; } /** * Set header fields. * * @return $this */ function setHeaderFields() { $this->http->setHeader("User-Agent", "CImage/0.6 (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } /** * Save downloaded resource to cache. * * @return string as path to saved file or false if not saved. */ function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $extension = $this->contentTypeToFileExtension($type); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['File-Extension'] = $extension; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } if ($extension) { $this->fileImage = $this->fileName . "." . $extension; // Save only if body is a valid image $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileImage, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileImage; } } return false; } /** * Got a 304 and updates cache with new age. * * @return string as path to cached file. */ function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileImage; } /** * Download a remote file and keep a cache of downloaded files. * * @param string $url a remote url. * * @return string as path to downloaded file or false if failed. */ function download($url) { $this->http = new CHttpGet(); $this->url = $url; // First check if the cache is valid and can be used $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } // Do a HTTP request to download item $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } else if ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } return false; } /** * Get the path to the cached image file if the cache is valid. * * @return $this */ public function loadCacheDetails() { $cacheFile = str_replace(array("/", ":", "#", ".", "?"), "-", $this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } /** * Get the path to the cached image file if the cache is valid. * * @return string as the path ot the image file or false if no cache. */ public function getCachedSource() { $this->fileImage = $this->fileName . "." . $this->cache['File-Extension']; $imageExists = is_readable($this->fileImage); // Is cache valid? $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileImage; } // Prepare for a 304 if available if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } /** * Resize and crop images on the fly, store generated images in a cache. * * @author Mikael Roos mos@dbwebb.se * @example http://dbwebb.se/opensource/cimage * @link https://github.com/mosbth/cimage */ class CImage { /** * Constants type of PNG image */ const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; /** * Constant for default image quality when not set */ const JPEG_QUALITY_DEFAULT = 60; /** * Quality level for JPEG images. */ private $quality; /** * Is the quality level set from external use (true) or is it default (false)? */ private $useQuality = false; /** * Constant for default image quality when not set */ const PNG_COMPRESSION_DEFAULT = -1; /** * Compression level for PNG images. */ private $compress; /** * Is the compress level set from external use (true) or is it default (false)? */ private $useCompress = false; /** * Default background color, red, green, blue, alpha. * * @todo remake when upgrading to PHP 5.5 */ /* const BACKGROUND_COLOR = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, );*/ /** * Default background color to use. * * @todo remake when upgrading to PHP 5.5 */ //private $bgColorDefault = self::BACKGROUND_COLOR; private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); /** * Background color to use, specified as part of options. */ private $bgColor; /** * Where to save the target file. */ private $saveFolder; /** * The working image object. */ private $image; /** * The root folder of images (only used in constructor to create $pathToImage?). */ private $imageFolder; /** * Image filename, may include subdirectory, relative from $imageFolder */ private $imageSrc; /** * Actual path to the image, $imageFolder . '/' . $imageSrc */ private $pathToImage; /** * Original file extension */ private $fileExtension; /** * File extension to use when saving image. */ private $extension; /** * Output format, supports null (image) or json. */ private $outputFormat = null; /** * Verbose mode to print out a trace and display the created image */ private $verbose = false; /** * Keep a log/trace on what happens */ private $log = array(); /** * Handle image as palette image */ private $palette; /** * Target filename, with path, to save resulting image in. */ private $cacheFileName; /** * Set a format to save image as, or null to use original format. */ private $saveAs; /** * Path to command for filter optimize, for example optipng or null. */ private $pngFilter; /** * Path to command for deflate optimize, for example pngout or null. */ private $pngDeflate; /** * Path to command to optimize jpeg images, for example jpegtran or null. */ private $jpegOptimize; /** * Image dimensions, calculated from loaded image. */ private $width; // Calculated from source image private $height; // Calculated from source image /** * New image dimensions, incoming as argument or calculated. */ private $newWidth; private $newWidthOrig; // Save original value private $newHeight; private $newHeightOrig; // Save original value /** * Change target height & width when different dpr, dpr 2 means double image dimensions. */ private $dpr = 1; /** * Always upscale images, even if they are smaller than target image. */ const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; /** * Array with details on how to crop, incoming as argument and calculated. */ public $crop; public $cropOrig; // Save original value /** * String with details on how to do image convolution. String * should map a key in the $convolvs array or be a string of * 11 float values separated by comma. The first nine builds * up the matrix, then divisor and last offset. */ private $convolve; /** * Custom convolution expressions, matrix 3x3, divisor and offset. */ private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); /** * Resize strategy to fill extra area with background color. * True or false. */ private $fillToFit; /** * Used with option area to set which parts of the image to use. */ private $offset; /** * Calculate target dimension for image when using fill-to-fit resize strategy. */ private $fillWidth; private $fillHeight; /** * Allow remote file download, default is to disallow remote file download. */ private $allowRemote = false; /** * Pattern to recognize a remote file. */ //private $remotePattern = '#^[http|https]://#'; private $remotePattern = '#^https?://#'; /** * Use the cache if true, set to false to ignore the cached file. */ private $useCache = true; /** * Properties, the class is mutable and the method setOptions() * decides (partly) what properties are created. * * @todo Clean up these and check if and how they are used */ public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $type; // Calculated from source image private $attr; // Calculated from source image private $useOriginal; // Use original image if possible /** * Constructor, can take arguments to init the object. * * @param string $imageSrc filename which may contain subdirectory. * @param string $imageFolder path to root folder for images. * @param string $saveFolder path to folder where to save the new file or null to skip saving. * @param string $saveName name of target file when saveing. */ public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } /** * Set verbose mode. * * @param boolean $mode true or false to enable and disable verbose mode, * default is true. * * @return $this */ public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } /** * Set save folder, base folder for saving cache files. * * @todo clean up how $this->saveFolder is used in other methods. * * @param string $path where to store cached files. * * @return $this */ public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } /** * Use cache or not. * * @todo clean up how $this->noCache is used in other methods. * * @param string $use true or false to use cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Allow or disallow remote image download. * * @param boolean $allow true or false to enable and disable. * @param string $pattern to use to detect if its a remote file. * * @return $this */ public function setRemoteDownload($allow, $pattern = null) { $this->allowRemote = $allow; $this->remotePattern = $pattern ? $pattern : $this->remotePattern; $this->log("Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern); return $this; } /** * Check if the image resource is a remote file or not. * * @param string $src check if src is remote. * * @return boolean true if $src is a remote file, else false. */ public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return $remote; } /** * Check if file extension is valid as a file extension. * * @param string $extension of image file. * * @return $this */ private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } /** * Download a remote image and return path to its local copy. * * @param string $src remote path to image. * * @return string as path to downloaded remote source. */ public function downloadRemoteSource($src) { $remote = new CRemoteImage(); $cache = $this->saveFolder . "/remote/"; if (!is_dir($cache)) { if (!is_writable($this->saveFolder)) { throw new Exception("Can not create remote cache, cachefolder not writable."); } mkdir($cache); $this->log("The remote cache does not exists, creating it."); } if (!is_writable($cache)) { $this->log("The remote cache is not writable."); } $remote->setCache($cache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item has local cached file: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } /** * Set src file. * * @param string $src of image. * @param string $dir as base directory where images are. * * @return $this */ public function setSource($src, $dir = null) { if (!isset($src)) { return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $this->imageFolder = rtrim($dir, '/'); $this->pathToImage = $this->imageFolder . '/' . $this->imageSrc; $this->fileExtension = strtolower(pathinfo($this->pathToImage, PATHINFO_EXTENSION)); //$this->extension = $this->fileExtension; $this->checkFileExtension($this->fileExtension); return $this; } /** * Set target file. * * @param string $src of target image. * @param string $dir as base directory where images are stored. * * @return $this */ public function setTarget($src = null, $dir = null) { if (!(isset($src) && isset($dir))) { return $this; } $this->saveFolder = $dir; $this->cacheFileName = $dir . '/' . $src; /* Allow readonly cache is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); */ // Sanitize filename $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } /** * Set options to use when processing image. * * @param array $args used when processing image. * * @return $this */ public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( // Options for calculate dimensions 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), 'area' => null, //'0,0,0,0', 'upscale' => self::UPSCALE_DEFAULT, // Options for caching or using original 'useCache' => true, 'useOriginal' => true, // Pre-processing, before resizing is done 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, // General options 'bgColor' => null, // Post-processing, after resizing is done 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, // Output format 'outputFormat' => null, 'dpr' => 1, // Options for saving //'quality' => null, //'compress' => null, //'saveAs' => null, ); // Convert crop settings from string to array if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } // Convert area settings from string to array if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } // Convert filter settings from array of string to array of array if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } // Merge default arguments with incoming and set properties. //$args = array_merge_recursive($defaults, $args); $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } // Save original values to enable re-calculating $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } /** * Map filter name to PHP filter and id. * * @param string $name the name of the filter. * * @return array with filter settings * @throws Exception */ private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } /** * Load image details from original image file. * * @param string $file the file to load or null to use $this->pathToImage. * * @return $this * @throws Exception */ public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; is_readable($file) or $this->raiseError('Image file does not exist.'); // Get details on image $info = list($this->width, $this->height, $this->type, $this->attr) = getimagesize($file); !empty($info) or $this->raiseError("The file doesn't seem to be an image."); if ($this->verbose) { $this->log("Image file: {$file}"); $this->log("Image width x height (type): {$this->width} x {$this->height} ({$this->type})."); $this->log("Image filesize: " . filesize($file) . " bytes."); } return $this; } /** * Init new width and height and do some sanity checks on constraints, before any * processing can be done. * * @return $this * @throws Exception */ public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // width as % if ($this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } // height as % if ($this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); // width & height from aspect ratio if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } // Change width & height based on dpr if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } // Check values to be within domain is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Calculate new width and height of image, based on settings. * * @return $this */ public function calculateNewWidthAndHeight() { // Crop, use cropped width and height as base for calulations $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // Check if there is an area to crop off if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; // Check if crop is set if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } // Calculate new width and height if keeping aspect-ratio. if ($this->keepRatio) { $this->log("Keep aspect ratio."); // Crop-to-fit and both new width and height are set. if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { // Use newWidth and newHeigh as width/height, image should fit in box. $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { // Both new width and height are set. // Use newWidth and newHeigh as max width/height, image should not be larger. $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { // Use new width as max-width $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { // Use new height as max-hight $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } // Get image dimensions for pre-resize image. if ($this->cropToFit || $this->fillToFit) { // Get relations of original & target image $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } else if ($this->fillToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } // Crop, ensure to set new width and height if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } // Fill to fit, ensure to set new width and height /*if ($this->fillToFit) { $this->log("FillToFit."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); }*/ // No new height or width is set, use existing measures. $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Re-calculate image dimensions when original image dimension has changed. * * @return $this */ public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } /** * Set extension for filename to save as. * * @param string $saveas extension to save image as * * @return $this */ public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image using as: " . $this->extension); return $this; } /** * Set JPEG quality to use when saving image * * @param int $quality as the quality to set. * * @return $this */ public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } /** * Set PNG compressen algorithm to use when saving image * * @param int $compress as the algorithm to use. * * @return $this */ public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } /** * Use original image if possible, check options which affects image processing. * * @param boolean $useOrig default is to use original if possible, else set to false. * * @return $this */ public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } /** * Generate filename to save file in cache. * * @param string $base as basepath for storing file. * * @return $this */ public function generateFilename($base) { $parts = pathinfo($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $width = $this->newWidth; $height = $this->newHeight; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= ":".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $this->extension = isset($this->extension) ? $this->extension : $parts['extension']; $optimize = null; if ($this->extension == 'jpeg' || $this->extension == 'jpg') { $optimize = $this->jpegOptimize ? 'o' : null; } elseif ($this->extension == 'png') { $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; } $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $file = $subdir . '_' . $parts['filename'] . '_' . $width . '_' . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . '.' . $this->extension; return $this->setTarget($file, $base); } /** * Use cached version of image, if possible. * * @param boolean $useCache is default true, set to false to avoid using cached object. * * @return $this */ public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } /** * Error message when failing to load somehow corrupt image. * * @return void * */ public function failedToLoad() { header("HTTP/1.0 404 Not Found"); echo("CImage.php says 404: Fatal error when opening image.
{$log}
EOD;
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
}
/**
* Resize and crop images on the fly, store generated images in a cache.
*
* @author Mikael Roos mos@dbwebb.se
* @example http://dbwebb.se/opensource/cimage
* @link https://github.com/mosbth/cimage
*
*/
$version = "v0.7.0 (2015-02-10)";
/**
* Default configuration options, can be overridden in own config-file.
*
* @param string $msg to display.
*
* @return void
*/
function errorPage($msg)
{
global $mode;
header("HTTP/1.0 500 Internal Server Error");
if ($mode == 'development') {
die("[img.php] $msg");
} else {
error_log("[img.php] $msg");
die("HTTP/1.0 500 Internal Server Error");
}
}
/**
* Custom exception handler.
*/
set_exception_handler(function ($exception) {
errorPage("img.php: Uncaught exception:
" . $exception->getMessage() . "
" . $exception->getTraceAsString(), ""); }); /** * Get input from query string or return default value if not set. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $default value to return when $key is not set in $_GET. * * @return mixed value from $_GET or default value. */ function get($key, $default = null) { if (is_array($key)) { foreach ($key as $val) { if (isset($_GET[$val])) { return $_GET[$val]; } } } elseif (isset($_GET[$key])) { return $_GET[$key]; } return $default; } /** * Get input from query string and set to $defined if defined or else $undefined. * * @param mixed $key as string or array of string values to look for in $_GET. * @param mixed $defined value to return when $key is set in $_GET. * @param mixed $undefined value to return when $key is not set in $_GET. * * @return mixed value as $defined or $undefined. */ function getDefined($key, $defined, $undefined) { return get($key) === null ? $undefined : $defined; } /** * Get value from config array or default if key is not set in config array. * * @param string $key the key in the config array. * @param mixed $default value to be default if $key is not set in config. * * @return mixed value as $config[$key] or $default. */ function getConfig($key, $default) { global $config; return isset($config[$key]) ? $config[$key] : $default; } /** * Log when verbose mode, when used without argument it returns the result. * * @param string $msg to log. * * @return void or array. */ function verbose($msg = null) { global $verbose; static $log = array(); if (!$verbose) { return; } if (is_null($msg)) { return $log; } $log[] = $msg; } /** * Get configuration options from file, if the file exists, else use $config * if its defined or create an empty $config. */ $configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php'; if (is_file($configFile)) { $config = require $configFile; } else if (!isset($config)) { $config = array(); } /** * verbose, v - do a verbose dump of what happens */ $verbose = getDefined(array('verbose', 'v'), true, false); verbose("img.php version = $version"); /** * Set mode as strict, production or development. * Default is production environment. */ $mode = getConfig('mode', 'production'); // Settings for any mode set_time_limit(20); ini_set('gd.jpeg_ignore_warning', 1); if (!extension_loaded('gd')) { errorPage("Extension gd is nod loaded."); } // Specific settings for each mode if ($mode == 'strict') { error_reporting(0); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'production') { error_reporting(-1); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; } else if ($mode == 'development') { error_reporting(-1); ini_set('display_errors', 1); ini_set('log_errors', 0); } else { errorPage("Unknown mode: $mode"); } verbose("mode = $mode"); verbose("error log = " . ini_get('error_log')); /** * Set default timezone if not set or if its set in the config-file. */ $defaultTimezone = getConfig('default_timezone', null); if ($defaultTimezone) { date_default_timezone_set($defaultTimezone); } else if (!ini_get('default_timezone')) { date_default_timezone_set('UTC'); } /** * Check if passwords are configured, used and match. * Options decide themself if they require passwords to be used. */ $pwdConfig = getConfig('password', false); $pwdAlways = getConfig('password_always', false); $pwd = get(array('password', 'pwd'), null); // Check if passwords match, if configured to use passwords $passwordMatch = null; if ($pwdAlways) { $passwordMatch = ($pwdConfig === $pwd); if (!$passwordMatch) { errorPage("Password required and does not match or exists."); } } elseif ($pwdConfig && $pwd) { $passwordMatch = ($pwdConfig === $pwd); } verbose("password match = $passwordMatch"); /** * Prevent hotlinking, leeching, of images by controlling who access them * from where. * */ $allowHotlinking = getConfig('allow_hotlinking', true); $hotlinkingWhitelist = getConfig('hotlinking_whitelist', array()); $serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $refererHost = parse_url($referer, PHP_URL_HOST); if (!$allowHotlinking) { if ($passwordMatch) { ; // Always allow when password match } else if ($passwordMatch === false) { errorPage("Hotlinking/leeching not allowed when password missmatch."); } else if (!$referer) { errorPage("Hotlinking/leeching not allowed and referer is missing."); } else if (strcmp($serverName, $refererHost) == 0) { ; // Allow when serverName matches refererHost } else if (!empty($hotlinkingWhitelist)) { $allowedByWhitelist = false; foreach ($hotlinkingWhitelist as $val) { if (preg_match($val, $refererHost)) { $allowedByWhitelist = true; } } if (!$allowedByWhitelist) { errorPage("Hotlinking/leeching not allowed by whitelist."); } } else { errorPage("Hotlinking/leeching not allowed."); } } verbose("allow_hotlinking = $allowHotlinking"); verbose("referer = $referer"); verbose("referer host = $refererHost"); /** * Get the source files. */ $autoloader = getConfig('autoloader', false); $cimageClass = getConfig('cimage_class', false); if ($autoloader) { require $autoloader; } else if ($cimageClass) { require $cimageClass; } /** * Create the class for the image. */ $img = new CImage(); $img->setVerbose($verbose); /** * Allow or disallow remote download of images from other servers. * Passwords apply if used. * */ $allowRemote = getConfig('remote_allow', false); if ($allowRemote && $passwordMatch !== false) { $pattern = getConfig('remote_pattern', null); $img->setRemoteDownload($allowRemote, $pattern); } /** * shortcut, sc - extend arguments with a constant value, defined * in config-file. */ $shortcut = get(array('shortcut', 'sc'), null); $shortcutConfig = getConfig('shortcut', array( 'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen", )); verbose("shortcut = $shortcut"); if (isset($shortcut) && isset($shortcutConfig[$shortcut])) { parse_str($shortcutConfig[$shortcut], $get); verbose("shortcut-constant = {$shortcutConfig[$shortcut]}"); $_GET = array_merge($_GET, $get); } /** * src - the source image file. */ $srcImage = get('src') or errorPage('Must set src-attribute.'); // Check for valid/invalid characters $imagePath = getConfig('image_path', __DIR__ . '/img/'); $imagePathConstraint = getConfig('image_path_constraint', true); $validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_\.:]+$#'); preg_match($validFilename, $srcImage) or errorPage('Filename contains invalid characters.'); if ($allowRemote && $img->isRemoteSource($srcImage)) { // If source is a remote file, ignore local file checks. } else if ($imagePathConstraint) { // Check that the image is a file below the directory 'image_path'. $pathToImage = realpath($imagePath . $srcImage); $imageDir = realpath($imagePath); is_file($pathToImage) or errorPage( 'Source image is not a valid file, check the filename and that a matching file exists on the filesystem.' ); substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0 or errorPage( 'Security constraint: Source image is not below the directory "image_path" as specified in the config file img_config.php.' ); } verbose("src = $srcImage"); /** * Manage size constants from config file, use constants to replace values * for width and height. */ $sizeConstant = getConfig('size_constant', function () { // Set sizes to map constant to value, easier to use with width or height $sizes = array( 'w1' => 613, 'w2' => 630, ); // Add grid column width, useful for use as predefined size for width (or height). $gridColumnWidth = 30; $gridGutterWidth = 10; $gridColumns = 24; for ($i = 1; $i <= $gridColumns; $i++) { $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth; } return $sizes; }); $sizes = call_user_func($sizeConstant); /** * width, w - set target width, affecting the resulting image width, height and resize options */ $newWidth = get(array('width', 'w')); $maxWidth = getConfig('max_width', 2000); // Check to replace predefined size if (isset($sizes[$newWidth])) { $newWidth = $sizes[$newWidth]; } // Support width as % of original width if ($newWidth[strlen($newWidth)-1] == '%') { is_numeric(substr($newWidth, 0, -1)) or errorPage('Width % not numeric.'); } else { is_null($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('Width out of range.'); } verbose("new width = $newWidth"); /** * height, h - set target height, affecting the resulting image width, height and resize options */ $newHeight = get(array('height', 'h')); $maxHeight = getConfig('max_height', 2000); // Check to replace predefined size if (isset($sizes[$newHeight])) { $newHeight = $sizes[$newHeight]; } // height if ($newHeight[strlen($newHeight)-1] == '%') { is_numeric(substr($newHeight, 0, -1)) or errorPage('Height % out of range.'); } else { is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Hight out of range.'); } verbose("new height = $newHeight"); /** * aspect-ratio, ar - affecting the resulting image width, height and resize options */ $aspectRatio = get(array('aspect-ratio', 'ar')); $aspectRatioConstant = getConfig('aspect_ratio_constant', function () { return array( '3:1' => 3/1, '3:2' => 3/2, '4:3' => 4/3, '8:5' => 8/5, '16:10' => 16/10, '16:9' => 16/9, 'golden' => 1.618, ); }); // Check to replace predefined aspect ratio $aspectRatios = call_user_func($aspectRatioConstant); $negateAspectRatio = ($aspectRatio[0] == '!') ? true : false; $aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio; if (isset($aspectRatios[$aspectRatio])) { $aspectRatio = $aspectRatios[$aspectRatio]; } if ($negateAspectRatio) { $aspectRatio = 1 / $aspectRatio; } is_null($aspectRatio) or is_numeric($aspectRatio) or errorPage('Aspect ratio out of range'); verbose("aspect ratio = $aspectRatio"); /** * crop-to-fit, cf - affecting the resulting image width, height and resize options */ $cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false); verbose("crop to fit = $cropToFit"); /** * Set default background color from config file. */ $backgroundColor = getConfig('background_color', null); if ($backgroundColor) { $img->setDefaultBackgroundColor($backgroundColor); verbose("Using default background_color = $backgroundColor"); } /** * bgColor - Default background color to use */ $bgColor = get(array('bgColor', 'bg-color', 'bgc'), null); verbose("bgColor = $bgColor"); /** * fill-to-fit, ff - affecting the resulting image width, height and resize options */ $fillToFit = get(array('fill-to-fit', 'ff'), null); verbose("fill-to-fit = $fillToFit"); if ($fillToFit !== null) { if (!empty($fillToFit)) { $bgColor = $fillToFit; verbose("fillToFit changed bgColor to = $bgColor"); } $fillToFit = true; verbose("fill-to-fit (fixed) = $fillToFit"); } /** * no-ratio, nr, stretch - affecting the resulting image width, height and resize options */ $keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true); verbose("keep ratio = $keepRatio"); /** * crop, c - affecting the resulting image width, height and resize options */ $crop = get(array('crop', 'c')); verbose("crop = $crop"); /** * area, a - affecting the resulting image width, height and resize options */ $area = get(array('area', 'a')); verbose("area = $area"); /** * skip-original, so - skip the original image and always process a new image */ $useOriginal = getDefined(array('skip-original', 'so'), false, true); verbose("use original = $useOriginal"); /** * no-cache, nc - skip the cached version and process and create a new version in cache. */ $useCache = getDefined(array('no-cache', 'nc'), false, true); verbose("use cache = $useCache"); /** * quality, q - set level of quality for jpeg images */ $quality = get(array('quality', 'q')); is_null($quality) or ($quality > 0 and $quality <= 100) or errorPage('Quality out of range'); verbose("quality = $quality"); /** * compress, co - what strategy to use when compressing png images */ $compress = get(array('compress', 'co')); is_null($compress) or ($compress > 0 and $compress <= 9) or errorPage('Compress out of range'); verbose("compress = $compress"); /** * save-as, sa - what type of image to save */ $saveAs = get(array('save-as', 'sa')); verbose("save as = $saveAs"); /** * scale, s - Processing option, scale up or down the image prior actual resize */ $scale = get(array('scale', 's')); is_null($scale) or ($scale >= 0 and $scale <= 400) or errorPage('Scale out of range'); verbose("scale = $scale"); /** * palette, p - Processing option, create a palette version of the image */ $palette = getDefined(array('palette', 'p'), true, false); verbose("palette = $palette"); /** * sharpen - Processing option, post filter for sharpen effect */ $sharpen = getDefined('sharpen', true, null); verbose("sharpen = $sharpen"); /** * emboss - Processing option, post filter for emboss effect */ $emboss = getDefined('emboss', true, null); verbose("emboss = $emboss"); /** * blur - Processing option, post filter for blur effect */ $blur = getDefined('blur', true, null); verbose("blur = $blur"); /** * rotateBefore - Rotate the image with an angle, before processing */ $rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb')); is_null($rotateBefore) or ($rotateBefore >= -360 and $rotateBefore <= 360) or errorPage('RotateBefore out of range'); verbose("rotateBefore = $rotateBefore"); /** * rotateAfter - Rotate the image with an angle, before processing */ $rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r')); is_null($rotateAfter) or ($rotateAfter >= -360 and $rotateAfter <= 360) or errorPage('RotateBefore out of range'); verbose("rotateAfter = $rotateAfter"); /** * autoRotate - Auto rotate based on EXIF information */ $autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false); verbose("autoRotate = $autoRotate"); /** * filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter() */ $filters = array(); $filter = get(array('filter', 'f')); if ($filter) { $filters[] = $filter; } for ($i = 0; $i < 10; $i++) { $filter = get(array("filter{$i}", "f{$i}")); if ($filter) { $filters[] = $filter; } } verbose("filters = " . print_r($filters, 1)); /** * json - output the image as a JSON object with details on the image. */ $outputFormat = getDefined('json', 'json', null); verbose("json = $outputFormat"); /** * dpr - change to get larger image to easier support larger dpr, such as retina. */ $dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1); verbose("dpr = $dpr"); /** * convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php */ $convolve = get('convolve', null); $convolutionConstant = getConfig('convolution_constant', array()); // Check if the convolve is matching an existing constant if ($convolve && isset($convolutionConstant)) { $img->addConvolveExpressions($convolutionConstant); verbose("convolve constant = " . print_r($convolutionConstant, 1)); } verbose("convolve = " . print_r($convolve, 1)); /** * no-upscale, nu - Do not upscale smaller image to larger dimension. */ $upscale = getDefined(array('no-upscale', 'nu'), false, true); verbose("upscale = $upscale"); /** * Get details for post processing */ $postProcessing = getConfig('postprocessing', array( 'png_filter' => false, 'png_filter_cmd' => '/usr/local/bin/optipng -q', 'png_deflate' => false, 'png_deflate_cmd' => '/usr/local/bin/pngout -q', 'jpeg_optimize' => false, 'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize', )); /** * alias - Save resulting image to another alias name. * Password always apply, must be defined. */ $alias = get('alias', null); $aliasPath = getConfig('alias_path', null); $validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#'); $aliasTarget = null; if ($alias && $aliasPath && $passwordMatch) { $aliasTarget = $aliasPath . $alias; $useCache = false; is_writable($aliasPath) or errorPage("Directory for alias is not writable."); preg_match($validAliasname, $alias) or errorPage('Filename for alias contains invalid characters. Do not add extension.'); } else if ($alias) { errorPage('Alias is not enabled in the config file or password not matching.'); } verbose("alias = $alias"); /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1=$description?>
The following images are used for this test.
The following testcases are used for each image.
=$tc?>
=$image . $tc?>
(json)
(verbose)
CImage.php through img.phpwider.jpg| Testcase: | Result: |
|---|---|
$key{$val['text']}".htmlentities($url)." |
higher.jpg| Testcase: | Result: |
|---|---|
$key{$val['text']}".htmlentities($url)." |
get(mixed $key, mixed $default = null) : mixed
Get input from query string or return default value if not set.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $default | value to return when $key is not set in $_GET. |
value from $_GET or default value.
getDefined(mixed $key, mixed $defined, mixed $undefined) : mixed
Get input from query string and set to $defined if defined or else $undefined.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $defined | value to return when $key is set in $_GET. |
| mixed | $undefined | value to return when $key is not set in $_GET. |
value as $defined or $undefined.
getConfig(string $key, mixed $default) : mixed
Get value from config array or default if key is not set in config array.
| string | $key | the key in the config array. |
| mixed | $default | value to be default if $key is not set in config. |
value as $config[$key] or $default.
Resize and crop images on the fly, store generated images in a cache.
This version is a all-in-one version of img.php, it is not dependant an any other file so you can simply copy it to any place you want it.
| CHttpGet | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CRemoteImage | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CImage | Resize and crop images on the fly, store generated images in a cache. |
get(mixed $key, mixed $default = null) : mixed
Get input from query string or return default value if not set.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $default | value to return when $key is not set in $_GET. |
value from $_GET or default value.
getDefined(mixed $key, mixed $defined, mixed $undefined) : mixed
Get input from query string and set to $defined if defined or else $undefined.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $defined | value to return when $key is set in $_GET. |
| mixed | $undefined | value to return when $key is not set in $_GET. |
value as $defined or $undefined.
getConfig(string $key, mixed $default) : mixed
Get value from config array or default if key is not set in config array.
| string | $key | the key in the config array. |
| mixed | $default | value to be default if $key is not set in config. |
value as $config[$key] or $default.
Resize and crop images on the fly, store generated images in a cache.
This version is a all-in-one version of img.php, it is not dependant an any other file so you can simply copy it to any place you want it.
| CHttpGet | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CRemoteImage | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CImage | Resize and crop images on the fly, store generated images in a cache. |
get(mixed $key, mixed $default = null) : mixed
Get input from query string or return default value if not set.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $default | value to return when $key is not set in $_GET. |
value from $_GET or default value.
getDefined(mixed $key, mixed $defined, mixed $undefined) : mixed
Get input from query string and set to $defined if defined or else $undefined.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $defined | value to return when $key is set in $_GET. |
| mixed | $undefined | value to return when $key is not set in $_GET. |
value as $defined or $undefined.
getConfig(string $key, mixed $default) : mixed
Get value from config array or default if key is not set in config array.
| string | $key | the key in the config array. |
| mixed | $default | value to be default if $key is not set in config. |
value as $config[$key] or $default.
Resize and crop images on the fly, store generated images in a cache.
This version is a all-in-one version of img.php, it is not dependant an any other file so you can simply copy it to any place you want it.
| CHttpGet | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CRemoteImage | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CImage | Resize and crop images on the fly, store generated images in a cache. |
get(mixed $key, mixed $default = null) : mixed
Get input from query string or return default value if not set.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $default | value to return when $key is not set in $_GET. |
value from $_GET or default value.
getDefined(mixed $key, mixed $defined, mixed $undefined) : mixed
Get input from query string and set to $defined if defined or else $undefined.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $defined | value to return when $key is set in $_GET. |
| mixed | $undefined | value to return when $key is not set in $_GET. |
value as $defined or $undefined.
getConfig(string $key, mixed $default) : mixed
Get value from config array or default if key is not set in config array.
| string | $key | the key in the config array. |
| mixed | $default | value to be default if $key is not set in config. |
value as $config[$key] or $default.
| CAsciiArt | Create an ASCII version of an image. |
| CHttpGet | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CImage | Resize and crop images on the fly, store generated images in a cache. |
| CRemoteImage | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CWhitelist | Act as whitelist (or blacklist). |
get(mixed $key, mixed $default = null) : mixed
Get input from query string or return default value if not set.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $default | value to return when $key is not set in $_GET. |
value from $_GET or default value.
getConfig(string $key, mixed $default) : mixed
Get value from config array or default if key is not set in config array.
| string | $key | the key in the config array. |
| mixed | $default | value to be default if $key is not set in config. |
value as $config[$key] or $default.
getDefined(mixed $key, mixed $defined, mixed $undefined) : mixed
Get input from query string and set to $defined if defined or else $undefined.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $defined | value to return when $key is set in $_GET. |
| mixed | $undefined | value to return when $key is not set in $_GET. |
value as $defined or $undefined.
| CAsciiArt | Create an ASCII version of an image. |
| CHttpGet | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CImage | Resize and crop images on the fly, store generated images in a cache. |
| CRemoteImage | Get a image from a remote server using HTTP GET and If-Modified-Since. |
| CWhitelist | Act as whitelist (or blacklist). |
get(mixed $key, mixed $default = null) : mixed
Get input from query string or return default value if not set.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $default | value to return when $key is not set in $_GET. |
value from $_GET or default value.
getConfig(string $key, mixed $default) : mixed
Get value from config array or default if key is not set in config array.
| string | $key | the key in the config array. |
| mixed | $default | value to be default if $key is not set in config. |
value as $config[$key] or $default.
getDefined(mixed $key, mixed $defined, mixed $undefined) : mixed
Get input from query string and set to $defined if defined or else $undefined.
| mixed | $key | as string or array of string values to look for in $_GET. |
| mixed | $defined | value to return when $key is set in $_GET. |
| mixed | $undefined | value to return when $key is not set in $_GET. |
value as $defined or $undefined.
| Type | Line | Description |
|---|---|---|
| error | 0 | No summary was found for this file |
| error | 1641 | Argument $dst_image is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $src_image is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $dst_x is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $dst_y is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $src_x is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $src_y is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $dst_w is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $dst_h is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $src_w is missing from the Docblock of imageCopyResampled |
| error | 1641 | Argument $src_h is missing from the Docblock of imageCopyResampled |
| error | 94 | No summary for property $bgColorDefault |
| error | 196 | No summary for property $pngFilterCmd |
| error | 204 | No summary for property $pngDeflateCmd |
| error | 212 | No summary for property $jpegOptimizeCmd |
| error | 220 | No summary for property $height |
| error | 227 | No summary for property $newWidthOrig |
| error | 228 | No summary for property $newHeight |
| error | 229 | No summary for property $newHeightOrig |
| error | 242 | No summary for property $upscale |
| error | 250 | No summary for property $cropOrig |
| error | 350 | No summary for property $fillHeight |
| error | 365 | No summary for property $remotePattern |
| error | 380 | No summary for property $remoteHostWhitelist |
| error | 387 | No summary for property $verboseFileName |
| error | 394 | No summary for property $asciiOptions |
| error | 403 | No summary for property $copyStrategy |
| error | 415 | No summary for property $cropToFit |
| error | 416 | No summary for property $cropWidth |
| error | 417 | No summary for property $cropHeight |
| error | 418 | No summary for property $crop_x |
| error | 419 | No summary for property $crop_y |
| error | 420 | No summary for property $filters |
| error | 421 | No summary for property $attr |
| Type | Line | Description |
|---|---|---|
| error | 0 | No summary was found for this file |
| error | 8 | No summary for property $request |
| error | 9 | No summary for property $response |
", var_dump(gd_info()), ""; } echo "Checking path for postprocessing tools"; echo "
Add link to images and visually compare them. Change the link och press return to load the image. Add &black to the querystring to get a black background. Read more...
Image 1Image 2Image 3Image 4Image 5Image 6" . $exception->getMessage() . "
"
. $exception->getTraceAsString()
. "",
500
);
});
/**
* Get configuration options from file, if the file exists, else use $config
* if its defined or create an empty $config.
*/
$configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php';
if (is_file($configFile)) {
$config = require $configFile;
} elseif (!isset($config)) {
$config = array();
}
// Make CIMAGE_DEBUG false by default, if not already defined
if (!defined("CIMAGE_DEBUG")) {
define("CIMAGE_DEBUG", false);
}
/**
* Setup the autoloader, but not when using a bundle.
*/
if (!defined("CIMAGE_BUNDLE")) {
if (!isset($config["autoloader"])) {
die("CImage: Missing autoloader.");
}
require $config["autoloader"];
}
/**
* verbose, v - do a verbose dump of what happens
* vf - do verbose dump to file
*/
$verbose = getDefined(array('verbose', 'v'), true, false);
$verboseFile = getDefined('vf', true, false);
verbose("img.php version = " . CIMAGE_VERSION);
/**
* status - do a verbose dump of the configuration
*/
$status = getDefined('status', true, false);
/**
* Set mode as strict, production or development.
* Default is production environment.
*/
$mode = getConfig('mode', 'production');
// Settings for any mode
set_time_limit(20);
ini_set('gd.jpeg_ignore_warning', 1);
if (!extension_loaded('gd')) {
errorPage("Extension gd is not loaded.", 500);
}
// Specific settings for each mode
if ($mode == 'strict') {
error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'production') {
error_reporting(-1);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'development') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
$verboseFile = false;
} elseif ($mode == 'test') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
} else {
errorPage("Unknown mode: $mode", 500);
}
verbose("mode = $mode");
verbose("error log = " . ini_get('error_log'));
/**
* Set default timezone if not set or if its set in the config-file.
*/
$defaultTimezone = getConfig('default_timezone', null);
if ($defaultTimezone) {
date_default_timezone_set($defaultTimezone);
} elseif (!ini_get('default_timezone')) {
date_default_timezone_set('UTC');
}
/**
* Check if passwords are configured, used and match.
* Options decide themself if they require passwords to be used.
*/
$pwdConfig = getConfig('password', false);
$pwdAlways = getConfig('password_always', false);
$pwdType = getConfig('password_type', 'text');
$pwd = get(array('password', 'pwd'), null);
// Check if passwords match, if configured to use passwords
$passwordMatch = null;
if ($pwd) {
switch ($pwdType) {
case 'md5':
$passwordMatch = ($pwdConfig === md5($pwd));
break;
case 'hash':
$passwordMatch = password_verify($pwd, $pwdConfig);
break;
case 'text':
$passwordMatch = ($pwdConfig === $pwd);
break;
default:
$passwordMatch = false;
}
}
if ($pwdAlways && $passwordMatch !== true) {
errorPage("Password required and does not match or exists.", 403);
}
verbose("password match = $passwordMatch");
/**
* Prevent hotlinking, leeching, of images by controlling who access them
* from where.
*
*/
$allowHotlinking = getConfig('allow_hotlinking', true);
$hotlinkingWhitelist = getConfig('hotlinking_whitelist', array());
$serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
$refererHost = parse_url($referer ?? "", PHP_URL_HOST);
if (!$allowHotlinking) {
if ($passwordMatch) {
; // Always allow when password match
verbose("Hotlinking since passwordmatch");
} elseif ($passwordMatch === false) {
errorPage("Hotlinking/leeching not allowed when password missmatch.", 403);
} elseif (!$referer) {
errorPage("Hotlinking/leeching not allowed and referer is missing.", 403);
} elseif (strcmp($serverName, $refererHost) == 0) {
; // Allow when serverName matches refererHost
verbose("Hotlinking disallowed but serverName matches refererHost.");
} elseif (!empty($hotlinkingWhitelist)) {
$whitelist = new CWhitelist();
$allowedByWhitelist = $whitelist->check($refererHost, $hotlinkingWhitelist);
if ($allowedByWhitelist) {
verbose("Hotlinking/leeching allowed by whitelist.");
} else {
errorPage("Hotlinking/leeching not allowed by whitelist. Referer: $referer.", 403);
}
} else {
errorPage("Hotlinking/leeching not allowed.", 403);
}
}
verbose("allow_hotlinking = $allowHotlinking");
verbose("referer = $referer");
verbose("referer host = $refererHost");
/**
* Create the class for the image.
*/
$CImage = getConfig('CImage', 'CImage');
$img = new $CImage();
$img->setVerbose($verbose || $verboseFile);
/**
* Get the cachepath from config.
*/
$CCache = getConfig('CCache', 'CCache');
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');
$cache = new $CCache();
$cache->setDir($cachePath);
/**
* no-cache, nc - skip the cached version and process and create a new version in cache.
*/
$useCache = getDefined(array('no-cache', 'nc'), false, true);
verbose("use cache = $useCache");
/**
* Prepare fast track cache for swriting cache items.
*/
$fastTrackCache = "fasttrack";
$allowFastTrackCache = getConfig('fast_track_allow', false);
$CFastTrackCache = getConfig('CFastTrackCache', 'CFastTrackCache');
$ftc = new $CFastTrackCache();
$ftc->setCacheDir($cache->getPathToSubdir($fastTrackCache))
->enable($allowFastTrackCache)
->setFilename(array('no-cache', 'nc'));
$img->injectDependency("fastTrackCache", $ftc);
/**
* Load and output images from fast track cache, if items are available
* in cache.
*/
if ($useCache && $allowFastTrackCache) {
if (CIMAGE_DEBUG) {
trace("img.php fast track cache enabled and used");
}
$ftc->output();
}
/**
* Allow or disallow remote download of images from other servers.
* Passwords apply if used.
*
*/
$allowRemote = getConfig('remote_allow', false);
if ($allowRemote && $passwordMatch !== false) {
$cacheRemote = $cache->getPathToSubdir("remote");
$pattern = getConfig('remote_pattern', null);
$img->setRemoteDownload($allowRemote, $cacheRemote, $pattern);
$whitelist = getConfig('remote_whitelist', null);
$img->setRemoteHostWhitelist($whitelist);
}
/**
* shortcut, sc - extend arguments with a constant value, defined
* in config-file.
*/
$shortcut = get(array('shortcut', 'sc'), null);
$shortcutConfig = getConfig('shortcut', array(
'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
));
verbose("shortcut = $shortcut");
if (isset($shortcut)
&& isset($shortcutConfig[$shortcut])) {
parse_str($shortcutConfig[$shortcut], $get);
verbose("shortcut-constant = {$shortcutConfig[$shortcut]}");
$_GET = array_merge($_GET, $get);
}
/**
* src - the source image file.
*/
$srcImage = urldecode(get('src', ""))
or errorPage('Must set src-attribute.', 404);
// Get settings for src-alt as backup image
$srcAltImage = urldecode(get('src-alt', ""));
$srcAltConfig = getConfig('src_alt', null);
if (empty($srcAltImage)) {
$srcAltImage = $srcAltConfig;
}
// Check for valid/invalid characters
$imagePath = getConfig('image_path', __DIR__ . '/img/');
$imagePathConstraint = getConfig('image_path_constraint', true);
$validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_ \.:]+$#');
// Source is remote
$remoteSource = false;
// Dummy image feature
$dummyEnabled = getConfig('dummy_enabled', true);
$dummyFilename = getConfig('dummy_filename', 'dummy');
$dummyImage = false;
preg_match($validFilename, $srcImage)
or errorPage('Source filename contains invalid characters.', 404);
if ($dummyEnabled && $srcImage === $dummyFilename) {
// Prepare to create a dummy image and use it as the source image.
$dummyImage = true;
} elseif ($allowRemote && $img->isRemoteSource($srcImage)) {
// If source is a remote file, ignore local file checks.
$remoteSource = true;
} else {
// Check if file exists on disk or try using src-alt
$pathToImage = realpath($imagePath . $srcImage);
if (!is_file($pathToImage) && !empty($srcAltImage)) {
// Try using the src-alt instead
$srcImage = $srcAltImage;
$pathToImage = realpath($imagePath . $srcImage);
preg_match($validFilename, $srcImage)
or errorPage('Source (alt) filename contains invalid characters.', 404);
if ($dummyEnabled && $srcImage === $dummyFilename) {
// Check if src-alt is the dummy image
$dummyImage = true;
}
}
if (!$dummyImage) {
is_file($pathToImage)
or errorPage(
'Source image is not a valid file, check the filename and that a
matching file exists on the filesystem.',
404
);
}
}
if ($imagePathConstraint && !$dummyImage && !$remoteSource) {
// Check that the image is a file below the directory 'image_path'.
$imageDir = realpath($imagePath);
substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0
or errorPage(
'Security constraint: Source image is not below the directory "image_path"
as specified in the config file img_config.php.',
404
);
}
verbose("src = $srcImage");
/**
* Manage size constants from config file, use constants to replace values
* for width and height.
*/
$sizeConstant = getConfig('size_constant', function () {
// Set sizes to map constant to value, easier to use with width or height
$sizes = array(
'w1' => 613,
'w2' => 630,
);
// Add grid column width, useful for use as predefined size for width (or height).
$gridColumnWidth = 30;
$gridGutterWidth = 10;
$gridColumns = 24;
for ($i = 1; $i <= $gridColumns; $i++) {
$sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth;
}
return $sizes;
});
$sizes = call_user_func($sizeConstant);
/**
* width, w - set target width, affecting the resulting image width, height and resize options
*/
$newWidth = get(array('width', 'w'));
$maxWidth = getConfig('max_width', 2000);
// Check to replace predefined size
if (isset($sizes[$newWidth])) {
$newWidth = $sizes[$newWidth];
}
// Support width as % of original width
if ($newWidth && $newWidth[strlen($newWidth)-1] == '%') {
is_numeric(substr($newWidth, 0, -1))
or errorPage('Width % not numeric.', 404);
} else {
is_null($newWidth)
or ($newWidth > 10 && $newWidth <= $maxWidth)
or errorPage('Width out of range.', 404);
}
verbose("new width = $newWidth");
/**
* height, h - set target height, affecting the resulting image width, height and resize options
*/
$newHeight = get(array('height', 'h'));
$maxHeight = getConfig('max_height', 2000);
// Check to replace predefined size
if (isset($sizes[$newHeight])) {
$newHeight = $sizes[$newHeight];
}
// height
if ($newHeight && $newHeight[strlen($newHeight)-1] == '%') {
is_numeric(substr($newHeight, 0, -1))
or errorPage('Height % out of range.', 404);
} else {
is_null($newHeight)
or ($newHeight > 10 && $newHeight <= $maxHeight)
or errorPage('Height out of range.', 404);
}
verbose("new height = $newHeight");
/**
* aspect-ratio, ar - affecting the resulting image width, height and resize options
*/
$aspectRatio = get(array('aspect-ratio', 'ar'));
$aspectRatioConstant = getConfig('aspect_ratio_constant', function () {
return array(
'3:1' => 3/1,
'3:2' => 3/2,
'4:3' => 4/3,
'8:5' => 8/5,
'16:10' => 16/10,
'16:9' => 16/9,
'golden' => 1.618,
);
});
// Check to replace predefined aspect ratio
$aspectRatios = call_user_func($aspectRatioConstant);
$negateAspectRatio = ($aspectRatio && $aspectRatio[0] == '!') ? true : false;
$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio;
if (isset($aspectRatios[$aspectRatio])) {
$aspectRatio = $aspectRatios[$aspectRatio];
}
if ($negateAspectRatio) {
$aspectRatio = 1 / $aspectRatio;
}
is_null($aspectRatio)
or is_numeric($aspectRatio)
or errorPage('Aspect ratio out of range', 404);
verbose("aspect ratio = $aspectRatio");
/**
* crop-to-fit, cf - affecting the resulting image width, height and resize options
*/
$cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false);
verbose("crop to fit = $cropToFit");
/**
* Set default background color from config file.
*/
$backgroundColor = getConfig('background_color', null);
if ($backgroundColor) {
$img->setDefaultBackgroundColor($backgroundColor);
verbose("Using default background_color = $backgroundColor");
}
/**
* bgColor - Default background color to use
*/
$bgColor = get(array('bgColor', 'bg-color', 'bgc'), null);
verbose("bgColor = $bgColor");
/**
* Do or do not resample image when resizing.
*/
$resizeStrategy = getDefined(array('no-resample'), true, false);
if ($resizeStrategy) {
$img->setCopyResizeStrategy($img::RESIZE);
verbose("Setting = Resize instead of resample");
}
/**
* fill-to-fit, ff - affecting the resulting image width, height and resize options
*/
$fillToFit = get(array('fill-to-fit', 'ff'), null);
verbose("fill-to-fit = $fillToFit");
if ($fillToFit !== null) {
if (!empty($fillToFit)) {
$bgColor = $fillToFit;
verbose("fillToFit changed bgColor to = $bgColor");
}
$fillToFit = true;
verbose("fill-to-fit (fixed) = $fillToFit");
}
/**
* no-ratio, nr, stretch - affecting the resulting image width, height and resize options
*/
$keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true);
verbose("keep ratio = $keepRatio");
/**
* crop, c - affecting the resulting image width, height and resize options
*/
$crop = get(array('crop', 'c'));
verbose("crop = $crop");
/**
* area, a - affecting the resulting image width, height and resize options
*/
$area = get(array('area', 'a'));
verbose("area = $area");
/**
* skip-original, so - skip the original image and always process a new image
*/
$useOriginal = getDefined(array('skip-original', 'so'), false, true);
$useOriginalDefault = getConfig('skip_original', false);
if ($useOriginalDefault === true) {
verbose("skip original is default ON");
$useOriginal = false;
}
verbose("use original = $useOriginal");
/**
* quality, q - set level of quality for jpeg images
*/
$quality = get(array('quality', 'q'));
$qualityDefault = getConfig('jpg_quality', null);
is_null($quality)
or ($quality > 0 and $quality <= 100)
or errorPage('Quality out of range', 404);
if (is_null($quality) && !is_null($qualityDefault)) {
$quality = $qualityDefault;
}
verbose("quality = $quality");
/**
* compress, co - what strategy to use when compressing png images
*/
$compress = get(array('compress', 'co'));
$compressDefault = getConfig('png_compression', null);
is_null($compress)
or ($compress > 0 and $compress <= 9)
or errorPage('Compress out of range', 404);
if (is_null($compress) && !is_null($compressDefault)) {
$compress = $compressDefault;
}
verbose("compress = $compress");
/**
* save-as, sa - what type of image to save
*/
$saveAs = get(array('save-as', 'sa'));
verbose("save as = $saveAs");
/**
* scale, s - Processing option, scale up or down the image prior actual resize
*/
$scale = get(array('scale', 's'));
is_null($scale)
or ($scale >= 0 and $scale <= 400)
or errorPage('Scale out of range', 404);
verbose("scale = $scale");
/**
* palette, p - Processing option, create a palette version of the image
*/
$palette = getDefined(array('palette', 'p'), true, false);
verbose("palette = $palette");
/**
* sharpen - Processing option, post filter for sharpen effect
*/
$sharpen = getDefined('sharpen', true, null);
verbose("sharpen = $sharpen");
/**
* emboss - Processing option, post filter for emboss effect
*/
$emboss = getDefined('emboss', true, null);
verbose("emboss = $emboss");
/**
* blur - Processing option, post filter for blur effect
*/
$blur = getDefined('blur', true, null);
verbose("blur = $blur");
/**
* rotateBefore - Rotate the image with an angle, before processing
*/
$rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb'));
is_null($rotateBefore)
or ($rotateBefore >= -360 and $rotateBefore <= 360)
or errorPage('RotateBefore out of range', 404);
verbose("rotateBefore = $rotateBefore");
/**
* rotateAfter - Rotate the image with an angle, before processing
*/
$rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r'));
is_null($rotateAfter)
or ($rotateAfter >= -360 and $rotateAfter <= 360)
or errorPage('RotateBefore out of range', 404);
verbose("rotateAfter = $rotateAfter");
/**
* autoRotate - Auto rotate based on EXIF information
*/
$autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false);
verbose("autoRotate = $autoRotate");
/**
* filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter()
*/
$filters = array();
$filter = get(array('filter', 'f'));
if ($filter) {
$filters[] = $filter;
}
for ($i = 0; $i < 10; $i++) {
$filter = get(array("filter{$i}", "f{$i}"));
if ($filter) {
$filters[] = $filter;
}
}
verbose("filters = " . print_r($filters, 1));
/**
* json - output the image as a JSON object with details on the image.
* ascii - output the image as ASCII art.
*/
$outputFormat = getDefined('json', 'json', null);
$outputFormat = getDefined('ascii', 'ascii', $outputFormat);
verbose("outputformat = $outputFormat");
if ($outputFormat == 'ascii') {
$defaultOptions = getConfig(
'ascii-options',
array(
"characterSet" => 'two',
"scale" => 14,
"luminanceStrategy" => 3,
"customCharacterSet" => null,
)
);
$options = get('ascii');
$options = explode(',', $options);
if (isset($options[0]) && !empty($options[0])) {
$defaultOptions['characterSet'] = $options[0];
}
if (isset($options[1]) && !empty($options[1])) {
$defaultOptions['scale'] = $options[1];
}
if (isset($options[2]) && !empty($options[2])) {
$defaultOptions['luminanceStrategy'] = $options[2];
}
if (count($options) > 3) {
// Last option is custom character string
unset($options[0]);
unset($options[1]);
unset($options[2]);
$characterString = implode($options);
$defaultOptions['customCharacterSet'] = $characterString;
}
$img->setAsciiOptions($defaultOptions);
}
/**
* dpr - change to get larger image to easier support larger dpr, such as retina.
*/
$dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1);
verbose("dpr = $dpr");
/**
* convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php
*/
$convolve = get('convolve', null);
$convolutionConstant = getConfig('convolution_constant', array());
// Check if the convolve is matching an existing constant
if ($convolve && isset($convolutionConstant)) {
$img->addConvolveExpressions($convolutionConstant);
verbose("convolve constant = " . print_r($convolutionConstant, 1));
}
verbose("convolve = " . print_r($convolve, 1));
/**
* no-upscale, nu - Do not upscale smaller image to larger dimension.
*/
$upscale = getDefined(array('no-upscale', 'nu'), false, true);
verbose("upscale = $upscale");
/**
* Get details for post processing
*/
$postProcessing = getConfig('postprocessing', array(
'png_lossy' => false,
'png_lossy_cmd' => '/usr/local/bin/pngquant --force --output',
'png_filter' => false,
'png_filter_cmd' => '/usr/local/bin/optipng -q',
'png_deflate' => false,
'png_deflate_cmd' => '/usr/local/bin/pngout -q',
'jpeg_optimize' => false,
'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize',
));
/**
* lossy - Do lossy postprocessing, if available.
*/
$lossy = getDefined(array('lossy'), true, null);
verbose("lossy = $lossy");
/**
* alias - Save resulting image to another alias name.
* Password always apply, must be defined.
*/
$alias = get('alias', null);
$aliasPath = getConfig('alias_path', null);
$validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#');
$aliasTarget = null;
if ($alias && $aliasPath && $passwordMatch) {
$aliasTarget = $aliasPath . $alias;
$useCache = false;
is_writable($aliasPath)
or errorPage("Directory for alias is not writable.", 403);
preg_match($validAliasname, $alias)
or errorPage('Filename for alias contains invalid characters. Do not add extension.', 404);
} elseif ($alias) {
errorPage('Alias is not enabled in the config file or password not matching.', 403);
}
verbose("alias = $alias");
/**
* Add cache control HTTP header.
*/
$cacheControl = getConfig('cache_control', null);
if ($cacheControl) {
verbose("cacheControl = $cacheControl");
$img->addHTTPHeader("Cache-Control", $cacheControl);
}
/**
* interlace - Enable configuration for interlaced progressive JPEG images.
*/
$interlaceConfig = getConfig('interlace', null);
$interlaceValue = getValue('interlace', null);
$interlaceDefined = getDefined('interlace', true, null);
$interlace = $interlaceValue ?? $interlaceDefined ?? $interlaceConfig;
verbose("interlace (configfile) = ", $interlaceConfig);
verbose("interlace = ", $interlace);
/**
* Prepare a dummy image and use it as source image.
*/
if ($dummyImage === true) {
$dummyDir = $cache->getPathToSubdir("dummy");
$img->setSaveFolder($dummyDir)
->setSource($dummyFilename, $dummyDir)
->setOptions(
array(
'newWidth' => $newWidth,
'newHeight' => $newHeight,
'bgColor' => $bgColor,
)
)
->setJpegQuality($quality)
->setPngCompression($compress)
->createDummyImage()
->generateFilename(null, false)
->save(null, null, false);
$srcImage = $img->getTarget();
$imagePath = null;
verbose("src (updated) = $srcImage");
}
/**
* Prepare a sRGB version of the image and use it as source image.
*/
$srgbDefault = getConfig('srgb_default', false);
$srgbColorProfile = getConfig('srgb_colorprofile', __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc');
$srgb = getDefined('srgb', true, null);
if ($srgb || $srgbDefault) {
$filename = $img->convert2sRGBColorSpace(
$srcImage,
$imagePath,
$cache->getPathToSubdir("srgb"),
$srgbColorProfile,
$useCache
);
if ($filename) {
$srcImage = $img->getTarget();
$imagePath = null;
verbose("srgb conversion and saved to cache = $srcImage");
} else {
verbose("srgb not op");
}
}
/**
* Display status
*/
if ($status) {
$text = "img.php version = " . CIMAGE_VERSION . "\n";
$text .= "PHP version = " . PHP_VERSION . "\n";
$text .= "Running on: " . $_SERVER['SERVER_SOFTWARE'] . "\n";
$text .= "Allow remote images = $allowRemote\n";
$res = $cache->getStatusOfSubdir("");
$text .= "Cache $res\n";
$res = $cache->getStatusOfSubdir("remote");
$text .= "Cache remote $res\n";
$res = $cache->getStatusOfSubdir("dummy");
$text .= "Cache dummy $res\n";
$res = $cache->getStatusOfSubdir("srgb");
$text .= "Cache srgb $res\n";
$res = $cache->getStatusOfSubdir($fastTrackCache);
$text .= "Cache fasttrack $res\n";
$text .= "Alias path writable = " . is_writable($aliasPath) . "\n";
$no = extension_loaded('exif') ? null : 'NOT';
$text .= "Extension exif is $no loaded.$textEOD; exit; } /** * Log verbose details to file */ if ($verboseFile) { $img->setVerboseToFile("$cachePath/log.txt"); } /** * Hook after img.php configuration and before processing with CImage */ $hookBeforeCImage = getConfig('hook_before_CImage', null); if (is_callable($hookBeforeCImage)) { verbose("hookBeforeCImage activated"); $allConfig = $hookBeforeCImage($img, array( // Options for calculate dimensions 'newWidth' => $newWidth, 'newHeight' => $newHeight, 'aspectRatio' => $aspectRatio, 'keepRatio' => $keepRatio, 'cropToFit' => $cropToFit, 'fillToFit' => $fillToFit, 'crop' => $crop, 'area' => $area, 'upscale' => $upscale, // Pre-processing, before resizing is done 'scale' => $scale, 'rotateBefore' => $rotateBefore, 'autoRotate' => $autoRotate, // General processing options 'bgColor' => $bgColor, // Post-processing, after resizing is done 'palette' => $palette, 'filters' => $filters, 'sharpen' => $sharpen, 'emboss' => $emboss, 'blur' => $blur, 'convolve' => $convolve, 'rotateAfter' => $rotateAfter, 'interlace' => $interlace, // Output format 'outputFormat' => $outputFormat, 'dpr' => $dpr, // Other 'postProcessing' => $postProcessing, 'lossy' => $lossy, )); verbose(print_r($allConfig, 1)); extract($allConfig); } /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } /** * Get HTTP code of response. * * @return integer as HTTP status code or null if not available. */ public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } /** * Get file modification time of response. * * @return int as timestamp. */ public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } /** * Get content type. * * @return string as the content type or null if not existing or invalid. */ public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : ''; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } /** * Get file modification time of response. * * @param mixed $default as default value (int seconds) if date is * missing in response header. * * @return int as timestamp or $default if Date is missing in * response header. */ public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } /** * Get max age of cachable item. * * @param mixed $default as default value if date is missing in response * header. * * @return int as timestamp or false if not available. */ public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { // max-age=2592000 $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } /** * Get body of response. * * @return string as body. */ public function getBody() { return $this->response['body']; } } /** * Get a image from a remote server using HTTP GET and If-Modified-Since. * */ class CRemoteImage { /** * Path to cache files. */ private $saveFolder = null; /** * Use cache or not. */ private $useCache = true; /** * HTTP object to aid in download file. */ private $http; /** * Status of the HTTP request. */ private $status; /** * Defalt age for cached items 60*60*24*7. */ private $defaultMaxAge = 604800; /** * Url of downloaded item. */ private $url; /** * Base name of cache file for downloaded item and name of image. */ private $fileName; /** * Filename for json-file with details of cached item. */ private $fileJson; /** * Cache details loaded from file. */ private $cache; /** * Get status of last HTTP request. * * @return int as status */ public function getStatus() { return $this->status; } /** * Get JSON details for cache item. * * @return array with json details on cache. */ public function getDetails() { return $this->cache; } /** * Set the path to the cache directory. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function setCache($path) { $this->saveFolder = rtrim($path, "/") . "/"; return $this; } /** * Check if cache is writable or throw exception. * * @return $this * * @throws Exception if cahce folder is not writable. */ public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } /** * Decide if the cache should be used or not before trying to download * a remote file. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Set header fields. * * @return $this */ public function setHeaderFields() { $cimageVersion = "CImage"; if (defined("CIMAGE_USER_AGENT")) { $cimageVersion = CIMAGE_USER_AGENT; } $this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } /** * Save downloaded resource to cache. * * @return string as path to saved file or false if not saved. */ public function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['Url'] = $this->url; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } // Save only if body is a valid image $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileName, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } return false; } /** * Got a 304 and updates cache with new age. * * @return string as path to cached file. */ public function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } /** * Download a remote file and keep a cache of downloaded files. * * @param string $url a remote url. * * @throws Exception when status code does not match 200 or 304. * * @return string as path to downloaded file or false if failed. */ public function download($url) { $this->http = new CHttpGet(); $this->url = $url; // First check if the cache is valid and can be used $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } // Do a HTTP request to download item $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } elseif ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } throw new Exception("Unknown statuscode when downloading remote image: " . $this->status); } /** * Get the path to the cached image file if the cache is valid. * * @return $this */ public function loadCacheDetails() { $cacheFile = md5($this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } /** * Get the path to the cached image file if the cache is valid. * * @return string as the path ot the image file or false if no cache. */ public function getCachedSource() { $imageExists = is_readable($this->fileName); // Is cache valid? $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileName; } // Prepare for a 304 if available if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } /** * Act as whitelist (or blacklist). * */ class CWhitelist { /** * Array to contain the whitelist options. */ private $whitelist = array(); /** * Set the whitelist from an array of strings, each item in the * whitelist should be a regexp without the surrounding / or #. * * @param array $whitelist with all valid options, * default is to clear the whitelist. * * @return $this */ public function set($whitelist = array()) { if (!is_array($whitelist)) { throw new Exception("Whitelist is not of a supported format."); } $this->whitelist = $whitelist; return $this; } /** * Check if item exists in the whitelist. * * @param string $item string to check. * @param array $whitelist optional with all valid options, default is null. * * @return boolean true if item is in whitelist, else false. */ public function check($item, $whitelist = null) { if ($whitelist !== null) { $this->set($whitelist); } if (empty($item) or empty($this->whitelist)) { return false; } foreach ($this->whitelist as $regexp) { if (preg_match("#$regexp#", $item)) { return true; } } return false; } } /** * Create an ASCII version of an image. * */ class CAsciiArt { /** * Character set to use. */ private $characterSet = array( 'one' => "#0XT|:,.' ", 'two' => "@%#*+=-:. ", 'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. " ); /** * Current character set. */ private $characters = null; /** * Length of current character set. */ private $charCount = null; /** * Scale of the area to swap to a character. */ private $scale = null; /** * Strategy to calculate luminance. */ private $luminanceStrategy = null; /** * Constructor which sets default options. */ public function __construct() { $this->setOptions(); } /** * Add a custom character set. * * @param string $key for the character set. * @param string $value for the character set. * * @return $this */ public function addCharacterSet($key, $value) { $this->characterSet[$key] = $value; return $this; } /** * Set options for processing, defaults are available. * * @param array $options to use as default settings. * * @return $this */ public function setOptions($options = array()) { $default = array( "characterSet" => 'two', "scale" => 14, "luminanceStrategy" => 3, "customCharacterSet" => null, ); $default = array_merge($default, $options); if (!is_null($default['customCharacterSet'])) { $this->addCharacterSet('custom', $default['customCharacterSet']); $default['characterSet'] = 'custom'; } $this->scale = $default['scale']; $this->characters = $this->characterSet[$default['characterSet']]; $this->charCount = strlen($this->characters); $this->luminanceStrategy = $default['luminanceStrategy']; return $this; } /** * Create an Ascii image from an image file. * * @param string $filename of the image to use. * * @return string $ascii with the ASCII image. */ public function createFromFile($filename) { $img = imagecreatefromstring(file_get_contents($filename)); list($width, $height) = getimagesize($filename); $ascii = null; $incY = $this->scale; $incX = $this->scale / 2; for ($y = 0; $y < $height - 1; $y += $incY) { for ($x = 0; $x < $width - 1; $x += $incX) { $toX = min($x + $this->scale / 2, $width - 1); $toY = min($y + $this->scale, $height - 1); $luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY); $ascii .= $this->luminance2character($luminance); } $ascii .= PHP_EOL; } return $ascii; } /** * Get the luminance from a region of an image using average color value. * * @param string $img the image. * @param integer $x1 the area to get pixels from. * @param integer $y1 the area to get pixels from. * @param integer $x2 the area to get pixels from. * @param integer $y2 the area to get pixels from. * * @return integer $luminance with a value between 0 and 100. */ public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2) { $numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1); $luminance = 0; for ($x = $x1; $x <= $x2; $x++) { for ($y = $y1; $y <= $y2; $y++) { $rgb = imagecolorat($img, $x, $y); $red = (($rgb >> 16) & 0xFF); $green = (($rgb >> 8) & 0xFF); $blue = ($rgb & 0xFF); $luminance += $this->getLuminance($red, $green, $blue); } } return $luminance / $numPixels; } /** * Calculate luminance value with different strategies. * * @param integer $red The color red. * @param integer $green The color green. * @param integer $blue The color blue. * * @return float $luminance with a value between 0 and 1. */ public function getLuminance($red, $green, $blue) { switch ($this->luminanceStrategy) { case 1: $luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255; break; case 2: $luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255; break; case 3: $luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255; break; case 0: default: $luminance = ($red + $green + $blue) / (255 * 3); } return $luminance; } /** * Translate the luminance value to a character. * * @param string $position a value between 0-100 representing the * luminance. * * @return string with the ascii character. */ public function luminance2character($luminance) { $position = (int) round($luminance * ($this->charCount - 1)); $char = $this->characters[$position]; return $char; } } /** * Resize and crop images on the fly, store generated images in a cache. * * @author Mikael Roos mos@dbwebb.se * @example http://dbwebb.se/opensource/cimage * @link https://github.com/mosbth/cimage */ #[AllowDynamicProperties] class CImage { /** * Constants type of PNG image */ const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; /** * Constant for default image quality when not set */ const JPEG_QUALITY_DEFAULT = 60; /** * Quality level for JPEG images. */ private $quality; /** * Is the quality level set from external use (true) or is it default (false)? */ private $useQuality = false; /** * Constant for default image quality when not set */ const PNG_COMPRESSION_DEFAULT = -1; /** * Compression level for PNG images. */ private $compress; /** * Is the compress level set from external use (true) or is it default (false)? */ private $useCompress = false; /** * Add HTTP headers for outputing image. */ private $HTTPHeader = array(); /** * Default background color, red, green, blue, alpha. * * @todo remake when upgrading to PHP 5.5 */ /* const BACKGROUND_COLOR = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, );*/ /** * Default background color to use. * * @todo remake when upgrading to PHP 5.5 */ //private $bgColorDefault = self::BACKGROUND_COLOR; private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); /** * Background color to use, specified as part of options. */ private $bgColor; /** * Where to save the target file. */ private $saveFolder; /** * The working image object. */ private $image; /** * Image filename, may include subdirectory, relative from $imageFolder */ private $imageSrc; /** * Actual path to the image, $imageFolder . '/' . $imageSrc */ private $pathToImage; /** * File type for source image, as provided by getimagesize() */ private $fileType; /** * File extension to use when saving image. */ private $extension; /** * Output format, supports null (image) or json. */ private $outputFormat = null; /** * Do lossy output using external postprocessing tools. */ private $lossy = null; /** * Verbose mode to print out a trace and display the created image */ private $verbose = false; /** * Keep a log/trace on what happens */ private $log = array(); /** * Handle image as palette image */ private $palette; /** * Target filename, with path, to save resulting image in. */ private $cacheFileName; /** * Set a format to save image as, or null to use original format. */ private $saveAs; /** * Path to command for lossy optimize, for example pngquant. */ private $pngLossy; private $pngLossyCmd; /** * Path to command for filter optimize, for example optipng. */ private $pngFilter; private $pngFilterCmd; /** * Path to command for deflate optimize, for example pngout. */ private $pngDeflate; private $pngDeflateCmd; /** * Path to command to optimize jpeg images, for example jpegtran or null. */ private $jpegOptimize; private $jpegOptimizeCmd; /** * Image dimensions, calculated from loaded image. */ private $width; // Calculated from source image private $height; // Calculated from source image /** * New image dimensions, incoming as argument or calculated. */ private $newWidth; private $newWidthOrig; // Save original value private $newHeight; private $newHeightOrig; // Save original value /** * Change target height & width when different dpr, dpr 2 means double image dimensions. */ private $dpr = 1; /** * Always upscale images, even if they are smaller than target image. */ const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; /** * Array with details on how to crop, incoming as argument and calculated. */ public $crop; public $cropOrig; // Save original value /** * String with details on how to do image convolution. String * should map a key in the $convolvs array or be a string of * 11 float values separated by comma. The first nine builds * up the matrix, then divisor and last offset. */ private $convolve; /** * Custom convolution expressions, matrix 3x3, divisor and offset. */ private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); /** * Resize strategy to fill extra area with background color. * True or false. */ private $fillToFit; /** * To store value for option scale. */ private $scale; /** * To store value for option. */ private $rotateBefore; /** * To store value for option. */ private $rotateAfter; /** * To store value for option. */ private $autoRotate; /** * To store value for option. */ private $sharpen; /** * To store value for option. */ private $emboss; /** * To store value for option. */ private $blur; /** * Used with option area to set which parts of the image to use. */ private $offset; /** * Calculate target dimension for image when using fill-to-fit resize strategy. */ private $fillWidth; private $fillHeight; /** * Allow remote file download, default is to disallow remote file download. */ private $allowRemote = false; /** * Path to cache for remote download. */ private $remoteCache; /** * Pattern to recognize a remote file. */ //private $remotePattern = '#^[http|https]://#'; private $remotePattern = '#^https?://#'; /** * Use the cache if true, set to false to ignore the cached file. */ private $useCache = true; /** * Disable the fasttrackCacke to start with, inject an object to enable it. */ private $fastTrackCache = null; /* * Set whitelist for valid hostnames from where remote source can be * downloaded. */ private $remoteHostWhitelist = null; /* * Do verbose logging to file by setting this to a filename. */ private $verboseFileName = null; /* * Output to ascii can take som options as an array. */ private $asciiOptions = array(); /* * Use interlaced progressive mode for JPEG images. */ private $interlace = false; /* * Image copy strategy, defaults to RESAMPLE. */ const RESIZE = 1; const RESAMPLE = 2; private $copyStrategy = NULL; /** * Properties, the class is mutable and the method setOptions() * decides (partly) what properties are created. * * @todo Clean up these and check if and how they are used */ public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $attr; // Calculated from source image /** * Constructor, can take arguments to init the object. * * @param string $imageSrc filename which may contain subdirectory. * @param string $imageFolder path to root folder for images. * @param string $saveFolder path to folder where to save the new file or null to skip saving. * @param string $saveName name of target file when saveing. */ public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } /** * Inject object and use it, must be available as member. * * @param string $property to set as object. * @param object $object to set to property. * * @return $this */ public function injectDependency($property, $object) { if (!property_exists($this, $property)) { $this->raiseError("Injecting unknown property."); } $this->$property = $object; return $this; } /** * Set verbose mode. * * @param boolean $mode true or false to enable and disable verbose mode, * default is true. * * @return $this */ public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } /** * Set save folder, base folder for saving cache files. * * @todo clean up how $this->saveFolder is used in other methods. * * @param string $path where to store cached files. * * @return $this */ public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } /** * Use cache or not. * * @param boolean $use true or false to use cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Create and save a dummy image. Use dimensions as stated in * $this->newWidth, or $width or default to 100 (same for height. * * @param integer $width use specified width for image dimension. * @param integer $height use specified width for image dimension. * * @return $this */ public function createDummyImage($width = null, $height = null) { $this->newWidth = $this->newWidth ?: $width ?: 100; $this->newHeight = $this->newHeight ?: $height ?: 100; $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); return $this; } /** * Allow or disallow remote image download. * * @param boolean $allow true or false to enable and disable. * @param string $cache path to cache dir. * @param string $pattern to use to detect if its a remote file. * * @return $this */ public function setRemoteDownload($allow, $cache, $pattern = null) { $this->allowRemote = $allow; $this->remoteCache = $cache; $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern; $this->log( "Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern ); return $this; } /** * Check if the image resource is a remote file or not. * * @param string $src check if src is remote. * * @return boolean true if $src is a remote file, else false. */ public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return !!$remote; } /** * Set whitelist for valid hostnames from where remote source can be * downloaded. * * @param array $whitelist with regexp hostnames to allow download from. * * @return $this */ public function setRemoteHostWhitelist($whitelist = null) { $this->remoteHostWhitelist = $whitelist; $this->log( "Setting remote host whitelist to: " . (is_null($whitelist) ? "null" : print_r($whitelist, 1)) ); return $this; } /** * Check if the hostname for the remote image, is on a whitelist, * if the whitelist is defined. * * @param string $src the remote source. * * @return boolean true if hostname on $src is in the whitelist, else false. */ public function isRemoteSourceOnWhitelist($src) { if (is_null($this->remoteHostWhitelist)) { $this->log("Remote host on whitelist not configured - allowing."); return true; } $whitelist = new CWhitelist(); $hostname = parse_url($src, PHP_URL_HOST); $allow = $whitelist->check($hostname, $this->remoteHostWhitelist); $this->log( "Remote host is on whitelist: " . ($allow ? "true" : "false") ); return $allow; } /** * Check if file extension is valid as a file extension. * * @param string $extension of image file. * * @return $this */ private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } /** * Normalize the file extension. * * @param string $extension of image file or skip to use internal. * * @return string $extension as a normalized file extension. */ private function normalizeFileExtension($extension = "") { $extension = strtolower($extension ? $extension : $this->extension ?? ""); if ($extension == 'jpeg') { $extension = 'jpg'; } return $extension; } /** * Download a remote image and return path to its local copy. * * @param string $src remote path to image. * * @return string as path to downloaded remote source. */ public function downloadRemoteSource($src) { if (!$this->isRemoteSourceOnWhitelist($src)) { throw new Exception("Hostname is not on whitelist for remote sources."); } $remote = new CRemoteImage(); if (!is_writable($this->remoteCache)) { $this->log("The remote cache is not writable."); } $remote->setCache($this->remoteCache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item is in local cache: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } /** * Set source file to use as image source. * * @param string $src of image. * @param string $dir as optional base directory where images are. * * @return $this */ public function setSource($src, $dir = null) { if (!isset($src)) { $this->imageSrc = null; $this->pathToImage = null; return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $imageFolder = rtrim($dir, '/'); $this->pathToImage = $imageFolder . '/' . $this->imageSrc; return $this; } /** * Set target file. * * @param string $src of target image. * @param string $dir as optional base directory where images are stored. * Uses $this->saveFolder if null. * * @return $this */ public function setTarget($src = null, $dir = null) { if (!isset($src)) { $this->cacheFileName = null; return $this; } if (isset($dir)) { $this->saveFolder = rtrim($dir, '/'); } $this->cacheFileName = $this->saveFolder . '/' . $src; // Sanitize filename $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } /** * Get filename of target file. * * @return Boolean|String as filename of target or false if not set. */ public function getTarget() { return $this->cacheFileName; } /** * Set options to use when processing image. * * @param array $args used when processing image. * * @return $this */ public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( // Options for calculate dimensions 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), 'area' => null, //'0,0,0,0', 'upscale' => self::UPSCALE_DEFAULT, // Options for caching or using original 'useCache' => true, 'useOriginal' => true, // Pre-processing, before resizing is done 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, // General options 'bgColor' => null, // Post-processing, after resizing is done 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, 'interlace' => null, // Output format 'outputFormat' => null, 'dpr' => 1, // Postprocessing using external tools 'lossy' => null, ); // Convert crop settings from string to array if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } // Convert area settings from string to array if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } // Convert filter settings from array of string to array of array if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } // Merge default arguments with incoming and set properties. //$args = array_merge_recursive($defaults, $args); $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } // Save original values to enable re-calculating $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } /** * Map filter name to PHP filter and id. * * @param string $name the name of the filter. * * @return array with filter settings * @throws Exception */ private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } /** * Load image details from original image file. * * @param string $file the file to load or null to use $this->pathToImage. * * @return $this * @throws Exception */ public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; // Special case to solve Windows 2 WSL integration if (!defined('WINDOWS2WSL')) { is_readable($file) or $this->raiseError('Image file does not exist.'); } $info = list($this->width, $this->height, $this->fileType) = getimagesize($file); if (empty($info)) { // To support webp $this->fileType = false; if (function_exists("exif_imagetype")) { $this->fileType = exif_imagetype($file); if ($this->fileType === false) { if (function_exists("imagecreatefromwebp")) { $webp = imagecreatefromwebp($file); if ($webp !== false) { $this->width = imagesx($webp); $this->height = imagesy($webp); $this->fileType = IMG_WEBP; } } } } } if (!$this->fileType) { throw new Exception("Loading image details, the file doesn't seem to be a valid image."); } if ($this->verbose) { $this->log("Loading image details for: {$file}"); $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType})."); $this->log(" Image filesize: " . filesize($file) . " bytes."); $this->log(" Image mimetype: " . $this->getMimeType()); } return $this; } /** * Get mime type for image type. * * @return $this * @throws Exception */ protected function getMimeType() { if ($this->fileType === IMG_WEBP) { return "image/webp"; } return image_type_to_mime_type($this->fileType); } /** * Init new width and height and do some sanity checks on constraints, before any * processing can be done. * * @return $this * @throws Exception */ public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // width as % if ($this->newWidth && $this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } // height as % if ($this->newHeight && $this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); // width & height from aspect ratio if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } // Change width & height based on dpr if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } // Check values to be within domain is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Calculate new width and height of image, based on settings. * * @return $this */ public function calculateNewWidthAndHeight() { // Crop, use cropped width and height as base for calulations $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // Check if there is an area to crop off if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; // Check if crop is set if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } // Calculate new width and height if keeping aspect-ratio. if ($this->keepRatio) { $this->log("Keep aspect ratio."); // Crop-to-fit and both new width and height are set. if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { // Use newWidth and newHeigh as width/height, image should fit in box. $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { // Both new width and height are set. // Use newWidth and newHeigh as max width/height, image should not be larger. $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { // Use new width as max-width $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { // Use new height as max-hight $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } else { // Use existing width and height as new width and height. $this->newWidth = $width; $this->newHeight = $height; } // Get image dimensions for pre-resize image. if ($this->cropToFit || $this->fillToFit) { // Get relations of original & target image $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } elseif ($this->fillToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } // Crop, ensure to set new width and height if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } // Fill to fit, ensure to set new width and height /*if ($this->fillToFit) { $this->log("FillToFit."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); }*/ // No new height or width is set, use existing measures. $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Re-calculate image dimensions when original image dimension has changed. * * @return $this */ public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } /** * Set extension for filename to save as. * * @param string $saveas extension to save image as * * @return $this */ public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image as: " . $this->extension); return $this; } /** * Set JPEG quality to use when saving image * * @param int $quality as the quality to set. * * @return $this */ public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } /** * Set PNG compressen algorithm to use when saving image * * @param int $compress as the algorithm to use. * * @return $this */ public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } /** * Use original image if possible, check options which affects image processing. * * @param boolean $useOrig default is to use original if possible, else set to false. * * @return $this */ public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) && !$this->lossy ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } /** * Generate filename to save file in cache. * * @param string $base as optional basepath for storing file. * @param boolean $useSubdir use or skip the subdir part when creating the * filename. * @param string $prefix to add as part of filename * * @return $this */ public function generateFilename($base = null, $useSubdir = true, $prefix = null) { $filename = basename($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $lossy = $this->lossy ? "_l" : null; $interlace = $this->interlace ? "_i" : null; $saveAs = $this->normalizeFileExtension(); $saveAs = $saveAs ? "_$saveAs" : null; $copyStrat = null; if ($this->copyStrategy === self::RESIZE) { $copyStrat = "_rs"; } $width = $this->newWidth ? '_' . $this->newWidth : null; $height = $this->newHeight ? '_' . $this->newHeight : null; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= "-".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $optimize = $this->jpegOptimize ? 'o' : null; $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = null; if ($useSubdir === true) { $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $subdir .= '_'; } $file = $prefix . $subdir . $filename . $width . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $compress . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . $copyStrat . $lossy . $interlace . $saveAs; return $this->setTarget($file, $base); } /** * Use cached version of image, if possible. * * @param boolean $useCache is default true, set to false to avoid using cached object. * * @return $this */ public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } /** * Load image from disk. Try to load image without verbose error message, * if fail, load again and display error messages. * * @param string $src of image. * @param string $dir as base directory where images are. * * @return $this * */ public function load($src = null, $dir = null) { if (isset($src)) { $this->setSource($src, $dir); } $this->loadImageDetails(); if ($this->fileType === IMG_WEBP) { $this->image = imagecreatefromwebp($this->pathToImage); } else { $imageAsString = file_get_contents($this->pathToImage); $this->image = imagecreatefromstring($imageAsString); } if ($this->image === false) { throw new Exception("Could not load image."); } /* Removed v0.7.7 if (image_type_to_mime_type($this->fileType) == 'image/png') { $type = $this->getPngType(); $hasFewColors = imagecolorstotal($this->image); if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) { if ($this->verbose) { $this->log("Handle this image as a palette image."); } $this->palette = true; } } */ if ($this->verbose) { $this->log("### Image successfully loaded from file."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->colorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } /** * Get the type of PNG image. * * @param string $filename to use instead of default. * * @return int as the type of the png-image * */ public function getPngType($filename = null) { $filename = $filename ? $filename : $this->pathToImage; $pngType = ord(file_get_contents($filename, false, null, 25, 1)); if ($this->verbose) { $this->log("Checking png type of: " . $filename); $this->log($this->getPngTypeAsString($pngType)); } return $pngType; } /** * Get the type of PNG image as a verbose string. * * @param integer $type to use, default is to check the type. * @param string $filename to use instead of default. * * @return int as the type of the png-image * */ private function getPngTypeAsString($pngType = null, $filename = null) { if ($filename || !$pngType) { $pngType = $this->getPngType($filename); } $index = imagecolortransparent($this->image); $transparent = null; if ($index != -1) { $transparent = " (transparent)"; } switch ($pngType) { case self::PNG_GREYSCALE: $text = "PNG is type 0, Greyscale$transparent"; break; case self::PNG_RGB: $text = "PNG is type 2, RGB$transparent"; break; case self::PNG_RGB_PALETTE: $text = "PNG is type 3, RGB with palette$transparent"; break; case self::PNG_GREYSCALE_ALPHA: $text = "PNG is type 4, Greyscale with alpha channel"; break; case self::PNG_RGB_ALPHA: $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)"; break; default: $text = "PNG is UNKNOWN type, is it really a PNG image?"; } return $text; } /** * Calculate number of colors in an image. * * @param resource $im the image. * * @return int */ private function colorsTotal($im) { if (imageistruecolor($im)) { $this->log("Colors as true color."); $h = imagesy($im); $w = imagesx($im); $c = array(); for ($x=0; $x < $w; $x++) { for ($y=0; $y < $h; $y++) { @$c['c'.imagecolorat($im, $x, $y)]++; } } return count($c); } else { $this->log("Colors as palette."); return imagecolorstotal($im); } } /** * Preprocess image before rezising it. * * @return $this */ public function preResize() { $this->log("### Pre-process before resizing"); // Rotate image if ($this->rotateBefore) { $this->log("Rotating image."); $this->rotate($this->rotateBefore, $this->bgColor) ->reCalculateDimensions(); } // Auto-rotate image if ($this->autoRotate) { $this->log("Auto rotating image."); $this->rotateExif() ->reCalculateDimensions(); } // Scale the original image before starting if (isset($this->scale)) { $this->log("Scale by {$this->scale}%"); $newWidth = $this->width * $this->scale / 100; $newHeight = $this->height * $this->scale / 100; $img = $this->CreateImageKeepTransparency($newWidth, $newHeight); imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height); $this->image = $img; $this->width = $newWidth; $this->height = $newHeight; } return $this; } /** * Resize or resample the image while resizing. * * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE * * @return $this */ public function setCopyResizeStrategy($strategy) { $this->copyStrategy = $strategy; return $this; } /** * Resize and or crop the image. * * @return void */ public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) { if($this->copyStrategy == self::RESIZE) { $this->log("Copy by resize"); imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } else { $this->log("Copy by resample"); imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } } /** * Resize and or crop the image. * * @return $this */ public function resize() { $this->log("### Starting to Resize()"); $this->log("Upscale = '$this->upscale'"); // Only use a specified area of the image, $this->offset is defining the area to use if (isset($this->offset)) { $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}"); $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']); imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']); $this->image = $img; $this->width = $this->offset['width']; $this->height = $this->offset['height']; } if ($this->crop) { // Do as crop, take only part of image $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}"); $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']); imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']); $this->image = $img; $this->width = $this->crop['width']; $this->height = $this->crop['height']; } if (!$this->upscale) { // Consider rewriting the no-upscale code to fit within this if-statement, // likely to be more readable code. // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch } if ($this->cropToFit) { // Resize by crop to fit $this->log("Resizing using strategy - Crop to fit"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight)) { $this->log("Resizing - smaller image, do not upscale."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newWidth < $this->width) { $cropX = round(($this->width/2) - ($this->newWidth/2)); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } if ($this->newHeight < $this->height) { $cropY = round(($this->height/2) - ($this->newHeight/2)); } $this->log(" cwidth: $this->cropWidth"); $this->log(" cheight: $this->cropHeight"); $this->log(" nwidth: $this->newWidth"); $this->log(" nheight: $this->newHeight"); $this->log(" width: $this->width"); $this->log(" height: $this->height"); $this->log(" posX: $posX"); $this->log(" posY: $posY"); $this->log(" cropX: $cropX"); $this->log(" cropY: $cropY"); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); } else { $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $imgPreCrop = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif ($this->fillToFit) { // Resize by fill to fit $this->log("Resizing using strategy - Fill to fit"); $posX = 0; $posY = 0; $ratioOrig = $this->width / $this->height; $ratioNew = $this->newWidth / $this->newHeight; // Check ratio for landscape or portrait if ($ratioOrig < $ratioNew) { $posX = round(($this->newWidth - $this->fillWidth) / 2); } else { $posY = round(($this->newHeight - $this->fillHeight) / 2); } if (!$this->upscale && ($this->width < $this->newWidth && $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) { // Resize it $this->log("Resizing, new height and/or width"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); if (!$this->keepRatio) { $this->log("Resizing - stretch to fit selected."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width && $this->newHeight > $this->height) { $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); } elseif ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } elseif ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } return $this; } /** * Postprocess image after rezising image. * * @return $this */ public function postResize() { $this->log("### Post-process after resizing"); // Rotate image if ($this->rotateAfter) { $this->log("Rotating image."); $this->rotate($this->rotateAfter, $this->bgColor); } // Apply filters if (isset($this->filters) && is_array($this->filters)) { foreach ($this->filters as $filter) { $this->log("Applying filter {$filter['type']}."); switch ($filter['argc']) { case 0: imagefilter($this->image, $filter['type']); break; case 1: imagefilter($this->image, $filter['type'], $filter['arg1']); break; case 2: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']); break; case 3: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']); break; case 4: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']); break; } } } // Convert to palette image if ($this->palette) { $this->log("Converting to palette image."); $this->trueColorToPalette(); } // Blur the image if ($this->blur) { $this->log("Blur."); $this->blurImage(); } // Emboss the image if ($this->emboss) { $this->log("Emboss."); $this->embossImage(); } // Sharpen the image if ($this->sharpen) { $this->log("Sharpen."); $this->sharpenImage(); } // Custom convolution if ($this->convolve) { //$this->log("Convolve: " . $this->convolve); $this->imageConvolution(); } return $this; } /** * Rotate image using angle. * * @param float $angle to rotate image. * @param int $anglebgColor to fill image with if needed. * * @return $this */ public function rotate($angle, $bgColor) { $this->log("Rotate image " . $angle . " degrees with filler color."); $color = $this->getBackgroundColor(); $this->image = imagerotate($this->image, $angle, $color); $this->width = imagesx($this->image); $this->height = imagesy($this->image); $this->log("New image dimension width x height: " . $this->width . " x " . $this->height); return $this; } /** * Rotate image using information in EXIF. * * @return $this */ public function rotateExif() { if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) { $this->log("Autorotate ignored, EXIF not supported by this filetype."); return $this; } $exif = exif_read_data($this->pathToImage); if (!empty($exif['Orientation'])) { switch ($exif['Orientation']) { case 3: $this->log("Autorotate 180."); $this->rotate(180, $this->bgColor); break; case 6: $this->log("Autorotate -90."); $this->rotate(-90, $this->bgColor); break; case 8: $this->log("Autorotate 90."); $this->rotate(90, $this->bgColor); break; default: $this->log("Autorotate ignored, unknown value as orientation."); } } else { $this->log("Autorotate ignored, no orientation in EXIF."); } return $this; } /** * Convert true color image to palette image, keeping alpha. * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library * * @return void */ public function trueColorToPalette() { $img = imagecreatetruecolor($this->width, $this->height); $bga = imagecolorallocatealpha($img, 0, 0, 0, 127); imagecolortransparent($img, $bga); imagefill($img, 0, 0, $bga); imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height); imagetruecolortopalette($img, false, 255); imagesavealpha($img, true); if (imageistruecolor($this->image)) { $this->log("Matching colors with true color image."); imagecolormatch($this->image, $img); } $this->image = $img; } /** * Sharpen image using image convolution. * * @return $this */ public function sharpenImage() { $this->imageConvolution('sharpen'); return $this; } /** * Emboss image using image convolution. * * @return $this */ public function embossImage() { $this->imageConvolution('emboss'); return $this; } /** * Blur image using image convolution. * * @return $this */ public function blurImage() { $this->imageConvolution('blur'); return $this; } /** * Create convolve expression and return arguments for image convolution. * * @param string $expression constant string which evaluates to a list of * 11 numbers separated by komma or such a list. * * @return array as $matrix (3x3), $divisor and $offset */ public function createConvolveArguments($expression) { // Check of matching constant if (isset($this->convolves[$expression])) { $expression = $this->convolves[$expression]; } $part = explode(',', $expression); $this->log("Creating convolution expressen: $expression"); // Expect list of 11 numbers, split by , and build up arguments if (count($part) != 11) { throw new Exception( "Missmatch in argument convolve. Expected comma-separated string with 11 float values. Got $expression." ); } array_walk($part, function ($item, $key) { if (!is_numeric($item)) { throw new Exception("Argument to convolve expression should be float but is not."); } }); return array( array( array($part[0], $part[1], $part[2]), array($part[3], $part[4], $part[5]), array($part[6], $part[7], $part[8]), ), $part[9], $part[10], ); } /** * Add custom expressions (or overwrite existing) for image convolution. * * @param array $options Key value array with strings to be converted * to convolution expressions. * * @return $this */ public function addConvolveExpressions($options) { $this->convolves = array_merge($this->convolves, $options); return $this; } /** * Image convolution. * * @param string $options A string with 11 float separated by comma. * * @return $this */ public function imageConvolution($options = null) { // Use incoming options or use $this. $options = $options ? $options : $this->convolve; // Treat incoming as string, split by + $this->log("Convolution with '$options'"); $options = explode(":", $options); // Check each option if it matches constant value foreach ($options as $option) { list($matrix, $divisor, $offset) = $this->createConvolveArguments($option); imageconvolution($this->image, $matrix, $divisor, $offset); } return $this; } /** * Set default background color between 000000-FFFFFF or if using * alpha 00000000-FFFFFF7F. * * @param string $color as hex value. * * @return $this */ public function setDefaultBackgroundColor($color) { $this->log("Setting default background color to '$color'."); if (!(strlen($color) == 6 || strlen($color) == 8)) { throw new Exception( "Background color needs a hex value of 6 or 8 digits. 000000-FFFFFF or 00000000-FFFFFF7F. Current value was: '$color'." ); } $red = hexdec(substr($color, 0, 2)); $green = hexdec(substr($color, 2, 2)); $blue = hexdec(substr($color, 4, 2)); $alpha = (strlen($color) == 8) ? hexdec(substr($color, 6, 2)) : null; if (($red < 0 || $red > 255) || ($green < 0 || $green > 255) || ($blue < 0 || $blue > 255) || ($alpha < 0 || $alpha > 127) ) { throw new Exception( "Background color out of range. Red, green blue should be 00-FF and alpha should be 00-7F. Current value was: '$color'." ); } $this->bgColor = strtolower($color); $this->bgColorDefault = array( 'red' => $red, 'green' => $green, 'blue' => $blue, 'alpha' => $alpha ); return $this; } /** * Get the background color. * * @param resource $img the image to work with or null if using $this->image. * * @return color value or null if no background color is set. */ private function getBackgroundColor($img = null) { $img = isset($img) ? $img : $this->image; if ($this->bgColorDefault) { $red = $this->bgColorDefault['red']; $green = $this->bgColorDefault['green']; $blue = $this->bgColorDefault['blue']; $alpha = $this->bgColorDefault['alpha']; if ($alpha) { $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha); } else { $color = imagecolorallocate($img, $red, $green, $blue); } return $color; } else { return 0; } } /** * Create a image and keep transparency for png and gifs. * * @param int $width of the new image. * @param int $height of the new image. * * @return image resource. */ private function createImageKeepTransparency($width, $height) { $this->log("Creating a new working image width={$width}px, height={$height}px."); $img = imagecreatetruecolor($width, $height); imagealphablending($img, false); imagesavealpha($img, true); $index = $this->image ? imagecolortransparent($this->image) : -1; if ($index != -1) { imagealphablending($img, true); $transparent = imagecolorsforindex($this->image, $index); $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']); imagefill($img, 0, 0, $color); $index = imagecolortransparent($img, $color); $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index"); } elseif ($this->bgColorDefault) { $color = $this->getBackgroundColor($img); imagefill($img, 0, 0, $color); $this->Log("Filling image with background color."); } return $img; } /** * Set optimizing and post-processing options. * * @param array $options with config for postprocessing with external tools. * * @return $this */ public function setPostProcessingOptions($options) { if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) { $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd']; } else { $this->jpegOptimizeCmd = null; } if (array_key_exists("png_lossy", $options) && $options['png_lossy'] !== false) { $this->pngLossy = $options['png_lossy']; $this->pngLossyCmd = $options['png_lossy_cmd']; } else { $this->pngLossyCmd = null; } if (isset($options['png_filter']) && $options['png_filter']) { $this->pngFilterCmd = $options['png_filter_cmd']; } else { $this->pngFilterCmd = null; } if (isset($options['png_deflate']) && $options['png_deflate']) { $this->pngDeflateCmd = $options['png_deflate_cmd']; } else { $this->pngDeflateCmd = null; } return $this; } /** * Find out the type (file extension) for the image to be saved. * * @return string as image extension. */ protected function getTargetImageExtension() { // switch on mimetype if (isset($this->extension)) { return strtolower($this->extension); } elseif ($this->fileType === IMG_WEBP) { return "webp"; } return substr(image_type_to_extension($this->fileType), 1); } /** * Save image. * * @param string $src as target filename. * @param string $base as base directory where to store images. * @param boolean $overwrite or not, default to always overwrite file. * * @return $this or false if no folder is set. */ public function save($src = null, $base = null, $overwrite = true) { if (isset($src)) { $this->setTarget($src, $base); } if ($overwrite === false && is_file($this->cacheFileName)) { $this->Log("Not overwriting file since its already exists and \$overwrite if false."); return; } if (!defined("WINDOWS2WSL")) { is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); } $type = $this->getTargetImageExtension(); $this->Log("Saving image as " . $type); switch($type) { case 'jpeg': case 'jpg': // Set as interlaced progressive JPEG if ($this->interlace) { $this->Log("Set JPEG image to be interlaced."); $res = imageinterlace($this->image, true); } $this->Log("Saving image as JPEG to cache using quality = {$this->quality}."); imagejpeg($this->image, $this->cacheFileName, $this->quality); // Use JPEG optimize if defined if ($this->jpegOptimizeCmd) { if ($this->verbose) { clearstatcache(); $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->log($cmd); $this->log($res); } break; case 'gif': $this->Log("Saving image as GIF to cache."); imagegif($this->image, $this->cacheFileName); break; case 'webp': $this->Log("Saving image as WEBP to cache using quality = {$this->quality}."); imagewebp($this->image, $this->cacheFileName, $this->quality); break; case 'png': default: $this->Log("Saving image as PNG to cache using compression = {$this->compress}."); // Turn off alpha blending and set alpha flag imagealphablending($this->image, false); imagesavealpha($this->image, true); imagepng($this->image, $this->cacheFileName, $this->compress); // Use external program to process lossy PNG, if defined $lossyEnabled = $this->pngLossy === true; $lossySoftEnabled = $this->pngLossy === null; $lossyActiveEnabled = $this->lossy === true; if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) { if ($this->verbose) { clearstatcache(); $this->log("Lossy enabled: $lossyEnabled"); $this->log("Lossy soft enabled: $lossySoftEnabled"); $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } // Use external program to filter PNG, if defined if ($this->pngFilterCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngFilterCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } // Use external program to deflate PNG, if defined if ($this->pngDeflateCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngDeflateCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } break; } if ($this->verbose) { clearstatcache(); $this->log("Saved image to cache."); $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } /** * Convert image from one colorpsace/color profile to sRGB without * color profile. * * @param string $src of image. * @param string $dir as base directory where images are. * @param string $cache as base directory where to store images. * @param string $iccFile filename of colorprofile. * @param boolean $useCache or not, default to always use cache. * * @return string | boolean false if no conversion else the converted * filename. */ public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true) { if ($this->verbose) { $this->log("# Converting image to sRGB colorspace."); } if (!class_exists("Imagick")) { $this->log(" Ignoring since Imagemagick is not installed."); return false; } // Prepare $this->setSaveFolder($cache) ->setSource($src, $dir) ->generateFilename(null, false, 'srgb_'); // Check if the cached version is accurate. if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { $this->log(" Using cached version: " . $this->cacheFileName); return $this->cacheFileName; } } // Only covert if cachedir is writable if (is_writable($this->saveFolder)) { // Load file and check if conversion is needed $image = new Imagick($this->pathToImage); $colorspace = $image->getImageColorspace(); $this->log(" Current colorspace: " . $colorspace); $profiles = $image->getImageProfiles('*', false); $hasICCProfile = (array_search('icc', $profiles) !== false); $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO")); if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) { $this->log(" Converting to sRGB."); $sRGBicc = file_get_contents($iccFile); $image->profileImage('icc', $sRGBicc); $image->transformImageColorspace(Imagick::COLORSPACE_SRGB); $image->writeImage($this->cacheFileName); return $this->cacheFileName; } } return false; } /** * Create a hard link, as an alias, to the cached file. * * @param string $alias where to store the link, * filename without extension. * * @return $this */ public function linkToCacheFile($alias) { if ($alias === null) { $this->log("Ignore creating alias."); return $this; } if (is_readable($alias)) { unlink($alias); } $res = link($this->cacheFileName, $alias); if ($res) { $this->log("Created an alias as: $alias"); } else { $this->log("Failed to create the alias: $alias"); } return $this; } /** * Add HTTP header for output together with image. * * @param string $type the header type such as "Cache-Control" * @param string $value the value to use * * @return void */ public function addHTTPHeader($type, $value) { $this->HTTPHeader[$type] = $value; } /** * Output image to browser using caching. * * @param string $file to read and output, default is to * use $this->cacheFileName * @param string $format set to json to output file as json * object with details * * @return void */ public function output($file = null, $format = null) { if (is_null($file)) { $file = $this->cacheFileName; } if (is_null($format)) { $format = $this->outputFormat; } $this->log("### Output"); $this->log("Output format is: $format"); if (!$this->verbose && $format == 'json') { header('Content-type: application/json'); echo $this->json($file); exit; } elseif ($format == 'ascii') { header('Content-type: text/plain'); echo $this->ascii($file); exit; } $this->log("Outputting image: $file"); // Get image modification time clearstatcache(); $lastModified = filemtime($file); $lastModifiedFormat = "D, d M Y H:i:s"; $gmdate = gmdate($lastModifiedFormat, $lastModified); if (!$this->verbose) { $header = "Last-Modified: $gmdate GMT"; header($header); $this->fastTrackCache->addHeader($header); $this->fastTrackCache->setLastModified($lastModified); } foreach ($this->HTTPHeader as $key => $val) { $header = "$key: $val"; header($header); $this->fastTrackCache->addHeader($header); } if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) { if ($this->verbose) { $this->log("304 not modified"); $this->verboseOutput(); exit; } header("HTTP/1.0 304 Not Modified"); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 304"); } } else { $this->loadImageDetails($file); $mime = $this->getMimeType(); $size = filesize($file); if ($this->verbose) { $this->log("Last-Modified: " . $gmdate . " GMT"); $this->log("Content-type: " . $mime); $this->log("Content-length: " . $size); $this->verboseOutput(); if (is_null($this->verboseFileName)) { exit; } } $header = "Content-type: $mime"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $header = "Content-length: $size"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $this->fastTrackCache->setSource($file); $this->fastTrackCache->writeToCache(); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 200"); } readfile($file); } exit; } /** * Create a JSON object from the image details. * * @param string $file the file to output. * * @return string json-encoded representation of the image. */ public function json($file = null) { $file = $file ? $file : $this->cacheFileName; $details = array(); clearstatcache(); $details['src'] = $this->imageSrc; $lastModified = filemtime($this->pathToImage); $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $details['cache'] = basename($this->cacheFileName ?? ""); $lastModified = filemtime($this->cacheFileName ?? ""); $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $this->load($file); $details['filename'] = basename($file ?? ""); $details['mimeType'] = $this->getMimeType($this->fileType); $details['width'] = $this->width; $details['height'] = $this->height; $details['aspectRatio'] = round($this->width / $this->height, 3); $details['size'] = filesize($file ?? ""); $details['colors'] = $this->colorsTotal($this->image); $details['includedFiles'] = count(get_included_files()); $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ; $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB"; $details['memoryLimit'] = ini_get('memory_limit'); if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s"; } if ($details['mimeType'] == 'image/png') { $details['pngType'] = $this->getPngTypeAsString(null, $file); } $options = null; if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; } return json_encode($details, $options); } /** * Set options for creating ascii version of image. * * @param array $options empty to use default or set options to change. * * @return void. */ public function setAsciiOptions($options = array()) { $this->asciiOptions = $options; } /** * Create an ASCII version from the image details. * * @param string $file the file to output. * * @return string ASCII representation of the image. */ public function ascii($file = null) { $file = $file ? $file : $this->cacheFileName; $asciiArt = new CAsciiArt(); $asciiArt->setOptions($this->asciiOptions); return $asciiArt->createFromFile($file); } /** * Log an event if verbose mode. * * @param string $message to log. * * @return this */ public function log($message) { if ($this->verbose) { $this->log[] = $message; } return $this; } /** * Do verbose output to a file. * * @param string $fileName where to write the verbose output. * * @return void */ public function setVerboseToFile($fileName) { $this->log("Setting verbose output to file."); $this->verboseFileName = $fileName; } /** * Do verbose output and print out the log and the actual images. * * @return void */ private function verboseOutput() { $log = null; $this->log("### Summary of verbose log"); $this->log("As JSON: \n" . $this->json()); $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M"); $this->log("Memory limit: " . ini_get('memory_limit')); $included = get_included_files(); $this->log("Included files: " . count($included)); foreach ($this->log as $val) { if (is_array($val)) { foreach ($val as $val1) { $log .= htmlentities($val1) . '
{$log}
EOD;
}
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
}
/**
* Deal with the cache directory and cached items.
*
*/
class CCache
{
/**
* Path to the cache directory.
*/
private $path;
/**
* Set the path to the cache dir which must exist.
*
* @param string path to the cache dir.
*
* @throws Exception when $path is not a directory.
*
* @return $this
*/
public function setDir($path)
{
if (!is_dir($path)) {
throw new Exception("Cachedir is not a directory.");
}
$this->path = $path;
return $this;
}
/**
* Get the path to the cache subdir and try to create it if its not there.
*
* @param string $subdir name of subdir
* @param array $create default is to try to create the subdir
*
* @return string | boolean as real path to the subdir or
* false if it does not exists
*/
public function getPathToSubdir($subdir, $create = true)
{
$path = realpath($this->path . "/" . $subdir);
if (is_dir($path)) {
return $path;
}
if ($create && defined('WINDOWS2WSL')) {
// Special case to solve Windows 2 WSL integration
$path = $this->path . "/" . $subdir;
if (mkdir($path)) {
return realpath($path);
}
}
if ($create && is_writable($this->path)) {
$path = $this->path . "/" . $subdir;
if (mkdir($path)) {
return realpath($path);
}
}
return false;
}
/**
* Get status of the cache subdir.
*
* @param string $subdir name of subdir
*
* @return string with status
*/
public function getStatusOfSubdir($subdir)
{
$path = realpath($this->path . "/" . $subdir);
$exists = is_dir($path);
$res = $exists ? "exists" : "does not exist";
if ($exists) {
$res .= is_writable($path) ? ", writable" : ", not writable";
}
return $res;
}
/**
* Remove the cache subdir.
*
* @param string $subdir name of subdir
*
* @return null | boolean true if success else false, null if no operation
*/
public function removeSubdir($subdir)
{
$path = realpath($this->path . "/" . $subdir);
if (is_dir($path)) {
return rmdir($path);
}
return null;
}
}
/**
* Enable a fast track cache with a json representation of the image delivery.
*
*/
class CFastTrackCache
{
/**
* Cache is disabled to start with.
*/
private $enabled = false;
/**
* Path to the cache directory.
*/
private $path;
/**
* Filename of current cache item.
*/
private $filename;
/**
* Container with items to store as cached item.
*/
private $container;
/**
* Enable or disable cache.
*
* @param boolean $enable set to true to enable, false to disable
*
* @return $this
*/
public function enable($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* Set the path to the cache dir which must exist.
*
* @param string $path to the cache dir.
*
* @throws Exception when $path is not a directory.
*
* @return $this
*/
public function setCacheDir($path)
{
if (!is_dir($path)) {
throw new Exception("Cachedir is not a directory.");
}
$this->path = rtrim($path, "/");
return $this;
}
/**
* Set the filename to store in cache, use the querystring to create that
* filename.
*
* @param array $clear items to clear in $_GET when creating the filename.
*
* @return string as filename created.
*/
public function setFilename($clear)
{
$query = $_GET;
// Remove parts from querystring that should not be part of filename
foreach ($clear as $value) {
unset($query[$value]);
}
arsort($query);
$queryAsString = http_build_query($query);
$this->filename = md5($queryAsString);
if (CIMAGE_DEBUG) {
$this->container["query-string"] = $queryAsString;
}
return $this->filename;
}
/**
* Add header items.
*
* @param string $header add this as header.
*
* @return $this
*/
public function addHeader($header)
{
$this->container["header"][] = $header;
return $this;
}
/**
* Add header items on output, these are not output when 304.
*
* @param string $header add this as header.
*
* @return $this
*/
public function addHeaderOnOutput($header)
{
$this->container["header-output"][] = $header;
return $this;
}
/**
* Set path to source image to.
*
* @param string $source path to source image file.
*
* @return $this
*/
public function setSource($source)
{
$this->container["source"] = $source;
return $this;
}
/**
* Set last modified of source image, use to check for 304.
*
* @param string $lastModified
*
* @return $this
*/
public function setLastModified($lastModified)
{
$this->container["last-modified"] = $lastModified;
return $this;
}
/**
* Get filename of cached item.
*
* @return string as filename.
*/
public function getFilename()
{
return $this->path . "/" . $this->filename;
}
/**
* Write current item to cache.
*
* @return boolean if cache file was written.
*/
public function writeToCache()
{
if (!$this->enabled) {
return false;
}
if (is_dir($this->path) && is_writable($this->path)) {
$filename = $this->getFilename();
return file_put_contents($filename, json_encode($this->container)) !== false;
}
return false;
}
/**
* Output current item from cache, if available.
*
* @return void
*/
public function output()
{
$filename = $this->getFilename();
if (!is_readable($filename)) {
return;
}
$item = json_decode(file_get_contents($filename), true);
if (!is_readable($item["source"])) {
return;
}
foreach ($item["header"] as $value) {
header($value);
}
if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])
&& strtotime($_SERVER["HTTP_IF_MODIFIED_SINCE"]) == $item["last-modified"]) {
header("HTTP/1.0 304 Not Modified");
if (CIMAGE_DEBUG) {
trace(__CLASS__ . " 304");
}
exit;
}
foreach ($item["header-output"] as $value) {
header($value);
}
if (CIMAGE_DEBUG) {
trace(__CLASS__ . " 200");
}
readfile($item["source"]);
exit;
}
}
/**
* Resize and crop images on the fly, store generated images in a cache.
*
* @author Mikael Roos mos@dbwebb.se
* @example http://dbwebb.se/opensource/cimage
* @link https://github.com/mosbth/cimage
*
*/
/**
* Custom exception handler.
*/
set_exception_handler(function ($exception) {
errorPage(
"img.php: Uncaught exception:
" . $exception->getMessage() . "
"
. $exception->getTraceAsString()
. "",
500
);
});
/**
* Get configuration options from file, if the file exists, else use $config
* if its defined or create an empty $config.
*/
$configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php';
if (is_file($configFile)) {
$config = require $configFile;
} elseif (!isset($config)) {
$config = array();
}
// Make CIMAGE_DEBUG false by default, if not already defined
if (!defined("CIMAGE_DEBUG")) {
define("CIMAGE_DEBUG", false);
}
/**
* Setup the autoloader, but not when using a bundle.
*/
if (!defined("CIMAGE_BUNDLE")) {
if (!isset($config["autoloader"])) {
die("CImage: Missing autoloader.");
}
require $config["autoloader"];
}
/**
* verbose, v - do a verbose dump of what happens
* vf - do verbose dump to file
*/
$verbose = getDefined(array('verbose', 'v'), true, false);
$verboseFile = getDefined('vf', true, false);
verbose("img.php version = " . CIMAGE_VERSION);
/**
* status - do a verbose dump of the configuration
*/
$status = getDefined('status', true, false);
/**
* Set mode as strict, production or development.
* Default is production environment.
*/
$mode = getConfig('mode', 'production');
// Settings for any mode
set_time_limit(20);
ini_set('gd.jpeg_ignore_warning', 1);
if (!extension_loaded('gd')) {
errorPage("Extension gd is not loaded.", 500);
}
// Specific settings for each mode
if ($mode == 'strict') {
error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'production') {
error_reporting(-1);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'development') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
$verboseFile = false;
} elseif ($mode == 'test') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
} else {
errorPage("Unknown mode: $mode", 500);
}
verbose("mode = $mode");
verbose("error log = " . ini_get('error_log'));
/**
* Set default timezone if not set or if its set in the config-file.
*/
$defaultTimezone = getConfig('default_timezone', null);
if ($defaultTimezone) {
date_default_timezone_set($defaultTimezone);
} elseif (!ini_get('default_timezone')) {
date_default_timezone_set('UTC');
}
/**
* Check if passwords are configured, used and match.
* Options decide themself if they require passwords to be used.
*/
$pwdConfig = getConfig('password', false);
$pwdAlways = getConfig('password_always', false);
$pwdType = getConfig('password_type', 'text');
$pwd = get(array('password', 'pwd'), null);
// Check if passwords match, if configured to use passwords
$passwordMatch = null;
if ($pwd) {
switch ($pwdType) {
case 'md5':
$passwordMatch = ($pwdConfig === md5($pwd));
break;
case 'hash':
$passwordMatch = password_verify($pwd, $pwdConfig);
break;
case 'text':
$passwordMatch = ($pwdConfig === $pwd);
break;
default:
$passwordMatch = false;
}
}
if ($pwdAlways && $passwordMatch !== true) {
errorPage("Password required and does not match or exists.", 403);
}
verbose("password match = $passwordMatch");
/**
* Prevent hotlinking, leeching, of images by controlling who access them
* from where.
*
*/
$allowHotlinking = getConfig('allow_hotlinking', true);
$hotlinkingWhitelist = getConfig('hotlinking_whitelist', array());
$serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
$refererHost = parse_url($referer ?? "", PHP_URL_HOST);
if (!$allowHotlinking) {
if ($passwordMatch) {
; // Always allow when password match
verbose("Hotlinking since passwordmatch");
} elseif ($passwordMatch === false) {
errorPage("Hotlinking/leeching not allowed when password missmatch.", 403);
} elseif (!$referer) {
errorPage("Hotlinking/leeching not allowed and referer is missing.", 403);
} elseif (strcmp($serverName, $refererHost) == 0) {
; // Allow when serverName matches refererHost
verbose("Hotlinking disallowed but serverName matches refererHost.");
} elseif (!empty($hotlinkingWhitelist)) {
$whitelist = new CWhitelist();
$allowedByWhitelist = $whitelist->check($refererHost, $hotlinkingWhitelist);
if ($allowedByWhitelist) {
verbose("Hotlinking/leeching allowed by whitelist.");
} else {
errorPage("Hotlinking/leeching not allowed by whitelist. Referer: $referer.", 403);
}
} else {
errorPage("Hotlinking/leeching not allowed.", 403);
}
}
verbose("allow_hotlinking = $allowHotlinking");
verbose("referer = $referer");
verbose("referer host = $refererHost");
/**
* Create the class for the image.
*/
$CImage = getConfig('CImage', 'CImage');
$img = new $CImage();
$img->setVerbose($verbose || $verboseFile);
/**
* Get the cachepath from config.
*/
$CCache = getConfig('CCache', 'CCache');
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');
$cache = new $CCache();
$cache->setDir($cachePath);
/**
* no-cache, nc - skip the cached version and process and create a new version in cache.
*/
$useCache = getDefined(array('no-cache', 'nc'), false, true);
verbose("use cache = $useCache");
/**
* Prepare fast track cache for swriting cache items.
*/
$fastTrackCache = "fasttrack";
$allowFastTrackCache = getConfig('fast_track_allow', false);
$CFastTrackCache = getConfig('CFastTrackCache', 'CFastTrackCache');
$ftc = new $CFastTrackCache();
$ftc->setCacheDir($cache->getPathToSubdir($fastTrackCache))
->enable($allowFastTrackCache)
->setFilename(array('no-cache', 'nc'));
$img->injectDependency("fastTrackCache", $ftc);
/**
* Load and output images from fast track cache, if items are available
* in cache.
*/
if ($useCache && $allowFastTrackCache) {
if (CIMAGE_DEBUG) {
trace("img.php fast track cache enabled and used");
}
$ftc->output();
}
/**
* Allow or disallow remote download of images from other servers.
* Passwords apply if used.
*
*/
$allowRemote = getConfig('remote_allow', false);
if ($allowRemote && $passwordMatch !== false) {
$cacheRemote = $cache->getPathToSubdir("remote");
$pattern = getConfig('remote_pattern', null);
$img->setRemoteDownload($allowRemote, $cacheRemote, $pattern);
$whitelist = getConfig('remote_whitelist', null);
$img->setRemoteHostWhitelist($whitelist);
}
/**
* shortcut, sc - extend arguments with a constant value, defined
* in config-file.
*/
$shortcut = get(array('shortcut', 'sc'), null);
$shortcutConfig = getConfig('shortcut', array(
'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
));
verbose("shortcut = $shortcut");
if (isset($shortcut)
&& isset($shortcutConfig[$shortcut])) {
parse_str($shortcutConfig[$shortcut], $get);
verbose("shortcut-constant = {$shortcutConfig[$shortcut]}");
$_GET = array_merge($_GET, $get);
}
/**
* src - the source image file.
*/
$srcImage = urldecode(get('src', ""))
or errorPage('Must set src-attribute.', 404);
// Get settings for src-alt as backup image
$srcAltImage = urldecode(get('src-alt', ""));
$srcAltConfig = getConfig('src_alt', null);
if (empty($srcAltImage)) {
$srcAltImage = $srcAltConfig;
}
// Check for valid/invalid characters
$imagePath = getConfig('image_path', __DIR__ . '/img/');
$imagePathConstraint = getConfig('image_path_constraint', true);
$validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_ \.:]+$#');
// Source is remote
$remoteSource = false;
// Dummy image feature
$dummyEnabled = getConfig('dummy_enabled', true);
$dummyFilename = getConfig('dummy_filename', 'dummy');
$dummyImage = false;
preg_match($validFilename, $srcImage)
or errorPage('Source filename contains invalid characters.', 404);
if ($dummyEnabled && $srcImage === $dummyFilename) {
// Prepare to create a dummy image and use it as the source image.
$dummyImage = true;
} elseif ($allowRemote && $img->isRemoteSource($srcImage)) {
// If source is a remote file, ignore local file checks.
$remoteSource = true;
} else {
// Check if file exists on disk or try using src-alt
$pathToImage = realpath($imagePath . $srcImage);
if (!is_file($pathToImage) && !empty($srcAltImage)) {
// Try using the src-alt instead
$srcImage = $srcAltImage;
$pathToImage = realpath($imagePath . $srcImage);
preg_match($validFilename, $srcImage)
or errorPage('Source (alt) filename contains invalid characters.', 404);
if ($dummyEnabled && $srcImage === $dummyFilename) {
// Check if src-alt is the dummy image
$dummyImage = true;
}
}
if (!$dummyImage) {
is_file($pathToImage)
or errorPage(
'Source image is not a valid file, check the filename and that a
matching file exists on the filesystem.',
404
);
}
}
if ($imagePathConstraint && !$dummyImage && !$remoteSource) {
// Check that the image is a file below the directory 'image_path'.
$imageDir = realpath($imagePath);
substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0
or errorPage(
'Security constraint: Source image is not below the directory "image_path"
as specified in the config file img_config.php.',
404
);
}
verbose("src = $srcImage");
/**
* Manage size constants from config file, use constants to replace values
* for width and height.
*/
$sizeConstant = getConfig('size_constant', function () {
// Set sizes to map constant to value, easier to use with width or height
$sizes = array(
'w1' => 613,
'w2' => 630,
);
// Add grid column width, useful for use as predefined size for width (or height).
$gridColumnWidth = 30;
$gridGutterWidth = 10;
$gridColumns = 24;
for ($i = 1; $i <= $gridColumns; $i++) {
$sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth;
}
return $sizes;
});
$sizes = call_user_func($sizeConstant);
/**
* width, w - set target width, affecting the resulting image width, height and resize options
*/
$newWidth = get(array('width', 'w'));
$maxWidth = getConfig('max_width', 2000);
// Check to replace predefined size
if (isset($sizes[$newWidth])) {
$newWidth = $sizes[$newWidth];
}
// Support width as % of original width
if ($newWidth && $newWidth[strlen($newWidth)-1] == '%') {
is_numeric(substr($newWidth, 0, -1))
or errorPage('Width % not numeric.', 404);
} else {
is_null($newWidth)
or ($newWidth > 10 && $newWidth <= $maxWidth)
or errorPage('Width out of range.', 404);
}
verbose("new width = $newWidth");
/**
* height, h - set target height, affecting the resulting image width, height and resize options
*/
$newHeight = get(array('height', 'h'));
$maxHeight = getConfig('max_height', 2000);
// Check to replace predefined size
if (isset($sizes[$newHeight])) {
$newHeight = $sizes[$newHeight];
}
// height
if ($newHeight && $newHeight[strlen($newHeight)-1] == '%') {
is_numeric(substr($newHeight, 0, -1))
or errorPage('Height % out of range.', 404);
} else {
is_null($newHeight)
or ($newHeight > 10 && $newHeight <= $maxHeight)
or errorPage('Height out of range.', 404);
}
verbose("new height = $newHeight");
/**
* aspect-ratio, ar - affecting the resulting image width, height and resize options
*/
$aspectRatio = get(array('aspect-ratio', 'ar'));
$aspectRatioConstant = getConfig('aspect_ratio_constant', function () {
return array(
'3:1' => 3/1,
'3:2' => 3/2,
'4:3' => 4/3,
'8:5' => 8/5,
'16:10' => 16/10,
'16:9' => 16/9,
'golden' => 1.618,
);
});
// Check to replace predefined aspect ratio
$aspectRatios = call_user_func($aspectRatioConstant);
$negateAspectRatio = ($aspectRatio && $aspectRatio[0] == '!') ? true : false;
$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio;
if (isset($aspectRatios[$aspectRatio])) {
$aspectRatio = $aspectRatios[$aspectRatio];
}
if ($negateAspectRatio) {
$aspectRatio = 1 / $aspectRatio;
}
is_null($aspectRatio)
or is_numeric($aspectRatio)
or errorPage('Aspect ratio out of range', 404);
verbose("aspect ratio = $aspectRatio");
/**
* crop-to-fit, cf - affecting the resulting image width, height and resize options
*/
$cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false);
verbose("crop to fit = $cropToFit");
/**
* Set default background color from config file.
*/
$backgroundColor = getConfig('background_color', null);
if ($backgroundColor) {
$img->setDefaultBackgroundColor($backgroundColor);
verbose("Using default background_color = $backgroundColor");
}
/**
* bgColor - Default background color to use
*/
$bgColor = get(array('bgColor', 'bg-color', 'bgc'), null);
verbose("bgColor = $bgColor");
/**
* Do or do not resample image when resizing.
*/
$resizeStrategy = getDefined(array('no-resample'), true, false);
if ($resizeStrategy) {
$img->setCopyResizeStrategy($img::RESIZE);
verbose("Setting = Resize instead of resample");
}
/**
* fill-to-fit, ff - affecting the resulting image width, height and resize options
*/
$fillToFit = get(array('fill-to-fit', 'ff'), null);
verbose("fill-to-fit = $fillToFit");
if ($fillToFit !== null) {
if (!empty($fillToFit)) {
$bgColor = $fillToFit;
verbose("fillToFit changed bgColor to = $bgColor");
}
$fillToFit = true;
verbose("fill-to-fit (fixed) = $fillToFit");
}
/**
* no-ratio, nr, stretch - affecting the resulting image width, height and resize options
*/
$keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true);
verbose("keep ratio = $keepRatio");
/**
* crop, c - affecting the resulting image width, height and resize options
*/
$crop = get(array('crop', 'c'));
verbose("crop = $crop");
/**
* area, a - affecting the resulting image width, height and resize options
*/
$area = get(array('area', 'a'));
verbose("area = $area");
/**
* skip-original, so - skip the original image and always process a new image
*/
$useOriginal = getDefined(array('skip-original', 'so'), false, true);
$useOriginalDefault = getConfig('skip_original', false);
if ($useOriginalDefault === true) {
verbose("skip original is default ON");
$useOriginal = false;
}
verbose("use original = $useOriginal");
/**
* quality, q - set level of quality for jpeg images
*/
$quality = get(array('quality', 'q'));
$qualityDefault = getConfig('jpg_quality', null);
is_null($quality)
or ($quality > 0 and $quality <= 100)
or errorPage('Quality out of range', 404);
if (is_null($quality) && !is_null($qualityDefault)) {
$quality = $qualityDefault;
}
verbose("quality = $quality");
/**
* compress, co - what strategy to use when compressing png images
*/
$compress = get(array('compress', 'co'));
$compressDefault = getConfig('png_compression', null);
is_null($compress)
or ($compress > 0 and $compress <= 9)
or errorPage('Compress out of range', 404);
if (is_null($compress) && !is_null($compressDefault)) {
$compress = $compressDefault;
}
verbose("compress = $compress");
/**
* save-as, sa - what type of image to save
*/
$saveAs = get(array('save-as', 'sa'));
verbose("save as = $saveAs");
/**
* scale, s - Processing option, scale up or down the image prior actual resize
*/
$scale = get(array('scale', 's'));
is_null($scale)
or ($scale >= 0 and $scale <= 400)
or errorPage('Scale out of range', 404);
verbose("scale = $scale");
/**
* palette, p - Processing option, create a palette version of the image
*/
$palette = getDefined(array('palette', 'p'), true, false);
verbose("palette = $palette");
/**
* sharpen - Processing option, post filter for sharpen effect
*/
$sharpen = getDefined('sharpen', true, null);
verbose("sharpen = $sharpen");
/**
* emboss - Processing option, post filter for emboss effect
*/
$emboss = getDefined('emboss', true, null);
verbose("emboss = $emboss");
/**
* blur - Processing option, post filter for blur effect
*/
$blur = getDefined('blur', true, null);
verbose("blur = $blur");
/**
* rotateBefore - Rotate the image with an angle, before processing
*/
$rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb'));
is_null($rotateBefore)
or ($rotateBefore >= -360 and $rotateBefore <= 360)
or errorPage('RotateBefore out of range', 404);
verbose("rotateBefore = $rotateBefore");
/**
* rotateAfter - Rotate the image with an angle, before processing
*/
$rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r'));
is_null($rotateAfter)
or ($rotateAfter >= -360 and $rotateAfter <= 360)
or errorPage('RotateBefore out of range', 404);
verbose("rotateAfter = $rotateAfter");
/**
* autoRotate - Auto rotate based on EXIF information
*/
$autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false);
verbose("autoRotate = $autoRotate");
/**
* filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter()
*/
$filters = array();
$filter = get(array('filter', 'f'));
if ($filter) {
$filters[] = $filter;
}
for ($i = 0; $i < 10; $i++) {
$filter = get(array("filter{$i}", "f{$i}"));
if ($filter) {
$filters[] = $filter;
}
}
verbose("filters = " . print_r($filters, 1));
/**
* json - output the image as a JSON object with details on the image.
* ascii - output the image as ASCII art.
*/
$outputFormat = getDefined('json', 'json', null);
$outputFormat = getDefined('ascii', 'ascii', $outputFormat);
verbose("outputformat = $outputFormat");
if ($outputFormat == 'ascii') {
$defaultOptions = getConfig(
'ascii-options',
array(
"characterSet" => 'two',
"scale" => 14,
"luminanceStrategy" => 3,
"customCharacterSet" => null,
)
);
$options = get('ascii');
$options = explode(',', $options);
if (isset($options[0]) && !empty($options[0])) {
$defaultOptions['characterSet'] = $options[0];
}
if (isset($options[1]) && !empty($options[1])) {
$defaultOptions['scale'] = $options[1];
}
if (isset($options[2]) && !empty($options[2])) {
$defaultOptions['luminanceStrategy'] = $options[2];
}
if (count($options) > 3) {
// Last option is custom character string
unset($options[0]);
unset($options[1]);
unset($options[2]);
$characterString = implode($options);
$defaultOptions['customCharacterSet'] = $characterString;
}
$img->setAsciiOptions($defaultOptions);
}
/**
* dpr - change to get larger image to easier support larger dpr, such as retina.
*/
$dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1);
verbose("dpr = $dpr");
/**
* convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php
*/
$convolve = get('convolve', null);
$convolutionConstant = getConfig('convolution_constant', array());
// Check if the convolve is matching an existing constant
if ($convolve && isset($convolutionConstant)) {
$img->addConvolveExpressions($convolutionConstant);
verbose("convolve constant = " . print_r($convolutionConstant, 1));
}
verbose("convolve = " . print_r($convolve, 1));
/**
* no-upscale, nu - Do not upscale smaller image to larger dimension.
*/
$upscale = getDefined(array('no-upscale', 'nu'), false, true);
verbose("upscale = $upscale");
/**
* Get details for post processing
*/
$postProcessing = getConfig('postprocessing', array(
'png_lossy' => false,
'png_lossy_cmd' => '/usr/local/bin/pngquant --force --output',
'png_filter' => false,
'png_filter_cmd' => '/usr/local/bin/optipng -q',
'png_deflate' => false,
'png_deflate_cmd' => '/usr/local/bin/pngout -q',
'jpeg_optimize' => false,
'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize',
));
/**
* lossy - Do lossy postprocessing, if available.
*/
$lossy = getDefined(array('lossy'), true, null);
verbose("lossy = $lossy");
/**
* alias - Save resulting image to another alias name.
* Password always apply, must be defined.
*/
$alias = get('alias', null);
$aliasPath = getConfig('alias_path', null);
$validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#');
$aliasTarget = null;
if ($alias && $aliasPath && $passwordMatch) {
$aliasTarget = $aliasPath . $alias;
$useCache = false;
is_writable($aliasPath)
or errorPage("Directory for alias is not writable.", 403);
preg_match($validAliasname, $alias)
or errorPage('Filename for alias contains invalid characters. Do not add extension.', 404);
} elseif ($alias) {
errorPage('Alias is not enabled in the config file or password not matching.', 403);
}
verbose("alias = $alias");
/**
* Add cache control HTTP header.
*/
$cacheControl = getConfig('cache_control', null);
if ($cacheControl) {
verbose("cacheControl = $cacheControl");
$img->addHTTPHeader("Cache-Control", $cacheControl);
}
/**
* interlace - Enable configuration for interlaced progressive JPEG images.
*/
$interlaceConfig = getConfig('interlace', null);
$interlaceValue = getValue('interlace', null);
$interlaceDefined = getDefined('interlace', true, null);
$interlace = $interlaceValue ?? $interlaceDefined ?? $interlaceConfig;
verbose("interlace (configfile) = ", $interlaceConfig);
verbose("interlace = ", $interlace);
/**
* Prepare a dummy image and use it as source image.
*/
if ($dummyImage === true) {
$dummyDir = $cache->getPathToSubdir("dummy");
$img->setSaveFolder($dummyDir)
->setSource($dummyFilename, $dummyDir)
->setOptions(
array(
'newWidth' => $newWidth,
'newHeight' => $newHeight,
'bgColor' => $bgColor,
)
)
->setJpegQuality($quality)
->setPngCompression($compress)
->createDummyImage()
->generateFilename(null, false)
->save(null, null, false);
$srcImage = $img->getTarget();
$imagePath = null;
verbose("src (updated) = $srcImage");
}
/**
* Prepare a sRGB version of the image and use it as source image.
*/
$srgbDefault = getConfig('srgb_default', false);
$srgbColorProfile = getConfig('srgb_colorprofile', __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc');
$srgb = getDefined('srgb', true, null);
if ($srgb || $srgbDefault) {
$filename = $img->convert2sRGBColorSpace(
$srcImage,
$imagePath,
$cache->getPathToSubdir("srgb"),
$srgbColorProfile,
$useCache
);
if ($filename) {
$srcImage = $img->getTarget();
$imagePath = null;
verbose("srgb conversion and saved to cache = $srcImage");
} else {
verbose("srgb not op");
}
}
/**
* Display status
*/
if ($status) {
$text = "img.php version = " . CIMAGE_VERSION . "\n";
$text .= "PHP version = " . PHP_VERSION . "\n";
$text .= "Running on: " . $_SERVER['SERVER_SOFTWARE'] . "\n";
$text .= "Allow remote images = $allowRemote\n";
$res = $cache->getStatusOfSubdir("");
$text .= "Cache $res\n";
$res = $cache->getStatusOfSubdir("remote");
$text .= "Cache remote $res\n";
$res = $cache->getStatusOfSubdir("dummy");
$text .= "Cache dummy $res\n";
$res = $cache->getStatusOfSubdir("srgb");
$text .= "Cache srgb $res\n";
$res = $cache->getStatusOfSubdir($fastTrackCache);
$text .= "Cache fasttrack $res\n";
$text .= "Alias path writable = " . is_writable($aliasPath) . "\n";
$no = extension_loaded('exif') ? null : 'NOT';
$text .= "Extension exif is $no loaded.$textEOD; exit; } /** * Log verbose details to file */ if ($verboseFile) { $img->setVerboseToFile("$cachePath/log.txt"); } /** * Hook after img.php configuration and before processing with CImage */ $hookBeforeCImage = getConfig('hook_before_CImage', null); if (is_callable($hookBeforeCImage)) { verbose("hookBeforeCImage activated"); $allConfig = $hookBeforeCImage($img, array( // Options for calculate dimensions 'newWidth' => $newWidth, 'newHeight' => $newHeight, 'aspectRatio' => $aspectRatio, 'keepRatio' => $keepRatio, 'cropToFit' => $cropToFit, 'fillToFit' => $fillToFit, 'crop' => $crop, 'area' => $area, 'upscale' => $upscale, // Pre-processing, before resizing is done 'scale' => $scale, 'rotateBefore' => $rotateBefore, 'autoRotate' => $autoRotate, // General processing options 'bgColor' => $bgColor, // Post-processing, after resizing is done 'palette' => $palette, 'filters' => $filters, 'sharpen' => $sharpen, 'emboss' => $emboss, 'blur' => $blur, 'convolve' => $convolve, 'rotateAfter' => $rotateAfter, 'interlace' => $interlace, // Output format 'outputFormat' => $outputFormat, 'dpr' => $dpr, // Other 'postProcessing' => $postProcessing, 'lossy' => $lossy, )); verbose(print_r($allConfig, 1)); extract($allConfig); } /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } /** * Get HTTP code of response. * * @return integer as HTTP status code or null if not available. */ public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } /** * Get file modification time of response. * * @return int as timestamp. */ public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } /** * Get content type. * * @return string as the content type or null if not existing or invalid. */ public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : ''; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } /** * Get file modification time of response. * * @param mixed $default as default value (int seconds) if date is * missing in response header. * * @return int as timestamp or $default if Date is missing in * response header. */ public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } /** * Get max age of cachable item. * * @param mixed $default as default value if date is missing in response * header. * * @return int as timestamp or false if not available. */ public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { // max-age=2592000 $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } /** * Get body of response. * * @return string as body. */ public function getBody() { return $this->response['body']; } } /** * Get a image from a remote server using HTTP GET and If-Modified-Since. * */ class CRemoteImage { /** * Path to cache files. */ private $saveFolder = null; /** * Use cache or not. */ private $useCache = true; /** * HTTP object to aid in download file. */ private $http; /** * Status of the HTTP request. */ private $status; /** * Defalt age for cached items 60*60*24*7. */ private $defaultMaxAge = 604800; /** * Url of downloaded item. */ private $url; /** * Base name of cache file for downloaded item and name of image. */ private $fileName; /** * Filename for json-file with details of cached item. */ private $fileJson; /** * Cache details loaded from file. */ private $cache; /** * Get status of last HTTP request. * * @return int as status */ public function getStatus() { return $this->status; } /** * Get JSON details for cache item. * * @return array with json details on cache. */ public function getDetails() { return $this->cache; } /** * Set the path to the cache directory. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function setCache($path) { $this->saveFolder = rtrim($path, "/") . "/"; return $this; } /** * Check if cache is writable or throw exception. * * @return $this * * @throws Exception if cahce folder is not writable. */ public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } /** * Decide if the cache should be used or not before trying to download * a remote file. * * @param boolean $use true to use the cache and false to ignore cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Set header fields. * * @return $this */ public function setHeaderFields() { $cimageVersion = "CImage"; if (defined("CIMAGE_USER_AGENT")) { $cimageVersion = CIMAGE_USER_AGENT; } $this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } /** * Save downloaded resource to cache. * * @return string as path to saved file or false if not saved. */ public function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['Url'] = $this->url; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } // Save only if body is a valid image $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileName, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } return false; } /** * Got a 304 and updates cache with new age. * * @return string as path to cached file. */ public function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } /** * Download a remote file and keep a cache of downloaded files. * * @param string $url a remote url. * * @throws Exception when status code does not match 200 or 304. * * @return string as path to downloaded file or false if failed. */ public function download($url) { $this->http = new CHttpGet(); $this->url = $url; // First check if the cache is valid and can be used $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } // Do a HTTP request to download item $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } elseif ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } throw new Exception("Unknown statuscode when downloading remote image: " . $this->status); } /** * Get the path to the cached image file if the cache is valid. * * @return $this */ public function loadCacheDetails() { $cacheFile = md5($this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } /** * Get the path to the cached image file if the cache is valid. * * @return string as the path ot the image file or false if no cache. */ public function getCachedSource() { $imageExists = is_readable($this->fileName); // Is cache valid? $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileName; } // Prepare for a 304 if available if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } /** * Act as whitelist (or blacklist). * */ class CWhitelist { /** * Array to contain the whitelist options. */ private $whitelist = array(); /** * Set the whitelist from an array of strings, each item in the * whitelist should be a regexp without the surrounding / or #. * * @param array $whitelist with all valid options, * default is to clear the whitelist. * * @return $this */ public function set($whitelist = array()) { if (!is_array($whitelist)) { throw new Exception("Whitelist is not of a supported format."); } $this->whitelist = $whitelist; return $this; } /** * Check if item exists in the whitelist. * * @param string $item string to check. * @param array $whitelist optional with all valid options, default is null. * * @return boolean true if item is in whitelist, else false. */ public function check($item, $whitelist = null) { if ($whitelist !== null) { $this->set($whitelist); } if (empty($item) or empty($this->whitelist)) { return false; } foreach ($this->whitelist as $regexp) { if (preg_match("#$regexp#", $item)) { return true; } } return false; } } /** * Create an ASCII version of an image. * */ class CAsciiArt { /** * Character set to use. */ private $characterSet = array( 'one' => "#0XT|:,.' ", 'two' => "@%#*+=-:. ", 'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. " ); /** * Current character set. */ private $characters = null; /** * Length of current character set. */ private $charCount = null; /** * Scale of the area to swap to a character. */ private $scale = null; /** * Strategy to calculate luminance. */ private $luminanceStrategy = null; /** * Constructor which sets default options. */ public function __construct() { $this->setOptions(); } /** * Add a custom character set. * * @param string $key for the character set. * @param string $value for the character set. * * @return $this */ public function addCharacterSet($key, $value) { $this->characterSet[$key] = $value; return $this; } /** * Set options for processing, defaults are available. * * @param array $options to use as default settings. * * @return $this */ public function setOptions($options = array()) { $default = array( "characterSet" => 'two', "scale" => 14, "luminanceStrategy" => 3, "customCharacterSet" => null, ); $default = array_merge($default, $options); if (!is_null($default['customCharacterSet'])) { $this->addCharacterSet('custom', $default['customCharacterSet']); $default['characterSet'] = 'custom'; } $this->scale = $default['scale']; $this->characters = $this->characterSet[$default['characterSet']]; $this->charCount = strlen($this->characters); $this->luminanceStrategy = $default['luminanceStrategy']; return $this; } /** * Create an Ascii image from an image file. * * @param string $filename of the image to use. * * @return string $ascii with the ASCII image. */ public function createFromFile($filename) { $img = imagecreatefromstring(file_get_contents($filename)); list($width, $height) = getimagesize($filename); $ascii = null; $incY = $this->scale; $incX = $this->scale / 2; for ($y = 0; $y < $height - 1; $y += $incY) { for ($x = 0; $x < $width - 1; $x += $incX) { $toX = min($x + $this->scale / 2, $width - 1); $toY = min($y + $this->scale, $height - 1); $luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY); $ascii .= $this->luminance2character($luminance); } $ascii .= PHP_EOL; } return $ascii; } /** * Get the luminance from a region of an image using average color value. * * @param string $img the image. * @param integer $x1 the area to get pixels from. * @param integer $y1 the area to get pixels from. * @param integer $x2 the area to get pixels from. * @param integer $y2 the area to get pixels from. * * @return integer $luminance with a value between 0 and 100. */ public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2) { $numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1); $luminance = 0; for ($x = $x1; $x <= $x2; $x++) { for ($y = $y1; $y <= $y2; $y++) { $rgb = imagecolorat($img, $x, $y); $red = (($rgb >> 16) & 0xFF); $green = (($rgb >> 8) & 0xFF); $blue = ($rgb & 0xFF); $luminance += $this->getLuminance($red, $green, $blue); } } return $luminance / $numPixels; } /** * Calculate luminance value with different strategies. * * @param integer $red The color red. * @param integer $green The color green. * @param integer $blue The color blue. * * @return float $luminance with a value between 0 and 1. */ public function getLuminance($red, $green, $blue) { switch ($this->luminanceStrategy) { case 1: $luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255; break; case 2: $luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255; break; case 3: $luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255; break; case 0: default: $luminance = ($red + $green + $blue) / (255 * 3); } return $luminance; } /** * Translate the luminance value to a character. * * @param string $position a value between 0-100 representing the * luminance. * * @return string with the ascii character. */ public function luminance2character($luminance) { $position = (int) round($luminance * ($this->charCount - 1)); $char = $this->characters[$position]; return $char; } } /** * Resize and crop images on the fly, store generated images in a cache. * * @author Mikael Roos mos@dbwebb.se * @example http://dbwebb.se/opensource/cimage * @link https://github.com/mosbth/cimage */ #[AllowDynamicProperties] class CImage { /** * Constants type of PNG image */ const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; /** * Constant for default image quality when not set */ const JPEG_QUALITY_DEFAULT = 60; /** * Quality level for JPEG images. */ private $quality; /** * Is the quality level set from external use (true) or is it default (false)? */ private $useQuality = false; /** * Constant for default image quality when not set */ const PNG_COMPRESSION_DEFAULT = -1; /** * Compression level for PNG images. */ private $compress; /** * Is the compress level set from external use (true) or is it default (false)? */ private $useCompress = false; /** * Add HTTP headers for outputing image. */ private $HTTPHeader = array(); /** * Default background color, red, green, blue, alpha. * * @todo remake when upgrading to PHP 5.5 */ /* const BACKGROUND_COLOR = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, );*/ /** * Default background color to use. * * @todo remake when upgrading to PHP 5.5 */ //private $bgColorDefault = self::BACKGROUND_COLOR; private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); /** * Background color to use, specified as part of options. */ private $bgColor; /** * Where to save the target file. */ private $saveFolder; /** * The working image object. */ private $image; /** * Image filename, may include subdirectory, relative from $imageFolder */ private $imageSrc; /** * Actual path to the image, $imageFolder . '/' . $imageSrc */ private $pathToImage; /** * File type for source image, as provided by getimagesize() */ private $fileType; /** * File extension to use when saving image. */ private $extension; /** * Output format, supports null (image) or json. */ private $outputFormat = null; /** * Do lossy output using external postprocessing tools. */ private $lossy = null; /** * Verbose mode to print out a trace and display the created image */ private $verbose = false; /** * Keep a log/trace on what happens */ private $log = array(); /** * Handle image as palette image */ private $palette; /** * Target filename, with path, to save resulting image in. */ private $cacheFileName; /** * Set a format to save image as, or null to use original format. */ private $saveAs; /** * Path to command for lossy optimize, for example pngquant. */ private $pngLossy; private $pngLossyCmd; /** * Path to command for filter optimize, for example optipng. */ private $pngFilter; private $pngFilterCmd; /** * Path to command for deflate optimize, for example pngout. */ private $pngDeflate; private $pngDeflateCmd; /** * Path to command to optimize jpeg images, for example jpegtran or null. */ private $jpegOptimize; private $jpegOptimizeCmd; /** * Image dimensions, calculated from loaded image. */ private $width; // Calculated from source image private $height; // Calculated from source image /** * New image dimensions, incoming as argument or calculated. */ private $newWidth; private $newWidthOrig; // Save original value private $newHeight; private $newHeightOrig; // Save original value /** * Change target height & width when different dpr, dpr 2 means double image dimensions. */ private $dpr = 1; /** * Always upscale images, even if they are smaller than target image. */ const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; /** * Array with details on how to crop, incoming as argument and calculated. */ public $crop; public $cropOrig; // Save original value /** * String with details on how to do image convolution. String * should map a key in the $convolvs array or be a string of * 11 float values separated by comma. The first nine builds * up the matrix, then divisor and last offset. */ private $convolve; /** * Custom convolution expressions, matrix 3x3, divisor and offset. */ private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); /** * Resize strategy to fill extra area with background color. * True or false. */ private $fillToFit; /** * To store value for option scale. */ private $scale; /** * To store value for option. */ private $rotateBefore; /** * To store value for option. */ private $rotateAfter; /** * To store value for option. */ private $autoRotate; /** * To store value for option. */ private $sharpen; /** * To store value for option. */ private $emboss; /** * To store value for option. */ private $blur; /** * Used with option area to set which parts of the image to use. */ private $offset; /** * Calculate target dimension for image when using fill-to-fit resize strategy. */ private $fillWidth; private $fillHeight; /** * Allow remote file download, default is to disallow remote file download. */ private $allowRemote = false; /** * Path to cache for remote download. */ private $remoteCache; /** * Pattern to recognize a remote file. */ //private $remotePattern = '#^[http|https]://#'; private $remotePattern = '#^https?://#'; /** * Use the cache if true, set to false to ignore the cached file. */ private $useCache = true; /** * Disable the fasttrackCacke to start with, inject an object to enable it. */ private $fastTrackCache = null; /* * Set whitelist for valid hostnames from where remote source can be * downloaded. */ private $remoteHostWhitelist = null; /* * Do verbose logging to file by setting this to a filename. */ private $verboseFileName = null; /* * Output to ascii can take som options as an array. */ private $asciiOptions = array(); /* * Use interlaced progressive mode for JPEG images. */ private $interlace = false; /* * Image copy strategy, defaults to RESAMPLE. */ const RESIZE = 1; const RESAMPLE = 2; private $copyStrategy = NULL; /** * Properties, the class is mutable and the method setOptions() * decides (partly) what properties are created. * * @todo Clean up these and check if and how they are used */ public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $attr; // Calculated from source image /** * Constructor, can take arguments to init the object. * * @param string $imageSrc filename which may contain subdirectory. * @param string $imageFolder path to root folder for images. * @param string $saveFolder path to folder where to save the new file or null to skip saving. * @param string $saveName name of target file when saveing. */ public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } /** * Inject object and use it, must be available as member. * * @param string $property to set as object. * @param object $object to set to property. * * @return $this */ public function injectDependency($property, $object) { if (!property_exists($this, $property)) { $this->raiseError("Injecting unknown property."); } $this->$property = $object; return $this; } /** * Set verbose mode. * * @param boolean $mode true or false to enable and disable verbose mode, * default is true. * * @return $this */ public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } /** * Set save folder, base folder for saving cache files. * * @todo clean up how $this->saveFolder is used in other methods. * * @param string $path where to store cached files. * * @return $this */ public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } /** * Use cache or not. * * @param boolean $use true or false to use cache. * * @return $this */ public function useCache($use = true) { $this->useCache = $use; return $this; } /** * Create and save a dummy image. Use dimensions as stated in * $this->newWidth, or $width or default to 100 (same for height. * * @param integer $width use specified width for image dimension. * @param integer $height use specified width for image dimension. * * @return $this */ public function createDummyImage($width = null, $height = null) { $this->newWidth = $this->newWidth ?: $width ?: 100; $this->newHeight = $this->newHeight ?: $height ?: 100; $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); return $this; } /** * Allow or disallow remote image download. * * @param boolean $allow true or false to enable and disable. * @param string $cache path to cache dir. * @param string $pattern to use to detect if its a remote file. * * @return $this */ public function setRemoteDownload($allow, $cache, $pattern = null) { $this->allowRemote = $allow; $this->remoteCache = $cache; $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern; $this->log( "Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern ); return $this; } /** * Check if the image resource is a remote file or not. * * @param string $src check if src is remote. * * @return boolean true if $src is a remote file, else false. */ public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return !!$remote; } /** * Set whitelist for valid hostnames from where remote source can be * downloaded. * * @param array $whitelist with regexp hostnames to allow download from. * * @return $this */ public function setRemoteHostWhitelist($whitelist = null) { $this->remoteHostWhitelist = $whitelist; $this->log( "Setting remote host whitelist to: " . (is_null($whitelist) ? "null" : print_r($whitelist, 1)) ); return $this; } /** * Check if the hostname for the remote image, is on a whitelist, * if the whitelist is defined. * * @param string $src the remote source. * * @return boolean true if hostname on $src is in the whitelist, else false. */ public function isRemoteSourceOnWhitelist($src) { if (is_null($this->remoteHostWhitelist)) { $this->log("Remote host on whitelist not configured - allowing."); return true; } $whitelist = new CWhitelist(); $hostname = parse_url($src, PHP_URL_HOST); $allow = $whitelist->check($hostname, $this->remoteHostWhitelist); $this->log( "Remote host is on whitelist: " . ($allow ? "true" : "false") ); return $allow; } /** * Check if file extension is valid as a file extension. * * @param string $extension of image file. * * @return $this */ private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } /** * Normalize the file extension. * * @param string $extension of image file or skip to use internal. * * @return string $extension as a normalized file extension. */ private function normalizeFileExtension($extension = "") { $extension = strtolower($extension ? $extension : $this->extension ?? ""); if ($extension == 'jpeg') { $extension = 'jpg'; } return $extension; } /** * Download a remote image and return path to its local copy. * * @param string $src remote path to image. * * @return string as path to downloaded remote source. */ public function downloadRemoteSource($src) { if (!$this->isRemoteSourceOnWhitelist($src)) { throw new Exception("Hostname is not on whitelist for remote sources."); } $remote = new CRemoteImage(); if (!is_writable($this->remoteCache)) { $this->log("The remote cache is not writable."); } $remote->setCache($this->remoteCache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item is in local cache: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } /** * Set source file to use as image source. * * @param string $src of image. * @param string $dir as optional base directory where images are. * * @return $this */ public function setSource($src, $dir = null) { if (!isset($src)) { $this->imageSrc = null; $this->pathToImage = null; return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $imageFolder = rtrim($dir, '/'); $this->pathToImage = $imageFolder . '/' . $this->imageSrc; return $this; } /** * Set target file. * * @param string $src of target image. * @param string $dir as optional base directory where images are stored. * Uses $this->saveFolder if null. * * @return $this */ public function setTarget($src = null, $dir = null) { if (!isset($src)) { $this->cacheFileName = null; return $this; } if (isset($dir)) { $this->saveFolder = rtrim($dir, '/'); } $this->cacheFileName = $this->saveFolder . '/' . $src; // Sanitize filename $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } /** * Get filename of target file. * * @return Boolean|String as filename of target or false if not set. */ public function getTarget() { return $this->cacheFileName; } /** * Set options to use when processing image. * * @param array $args used when processing image. * * @return $this */ public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( // Options for calculate dimensions 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, //array('width'=>null, 'height'=>null, 'start_x'=>0, 'start_y'=>0), 'area' => null, //'0,0,0,0', 'upscale' => self::UPSCALE_DEFAULT, // Options for caching or using original 'useCache' => true, 'useOriginal' => true, // Pre-processing, before resizing is done 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, // General options 'bgColor' => null, // Post-processing, after resizing is done 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, 'interlace' => null, // Output format 'outputFormat' => null, 'dpr' => 1, // Postprocessing using external tools 'lossy' => null, ); // Convert crop settings from string to array if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } // Convert area settings from string to array if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } // Convert filter settings from array of string to array of array if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } // Merge default arguments with incoming and set properties. //$args = array_merge_recursive($defaults, $args); $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } // Save original values to enable re-calculating $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } /** * Map filter name to PHP filter and id. * * @param string $name the name of the filter. * * @return array with filter settings * @throws Exception */ private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } /** * Load image details from original image file. * * @param string $file the file to load or null to use $this->pathToImage. * * @return $this * @throws Exception */ public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; // Special case to solve Windows 2 WSL integration if (!defined('WINDOWS2WSL')) { is_readable($file) or $this->raiseError('Image file does not exist.'); } $info = list($this->width, $this->height, $this->fileType) = getimagesize($file); if (empty($info)) { // To support webp $this->fileType = false; if (function_exists("exif_imagetype")) { $this->fileType = exif_imagetype($file); if ($this->fileType === false) { if (function_exists("imagecreatefromwebp")) { $webp = imagecreatefromwebp($file); if ($webp !== false) { $this->width = imagesx($webp); $this->height = imagesy($webp); $this->fileType = IMG_WEBP; } } } } } if (!$this->fileType) { throw new Exception("Loading image details, the file doesn't seem to be a valid image."); } if ($this->verbose) { $this->log("Loading image details for: {$file}"); $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType})."); $this->log(" Image filesize: " . filesize($file) . " bytes."); $this->log(" Image mimetype: " . $this->getMimeType()); } return $this; } /** * Get mime type for image type. * * @return $this * @throws Exception */ protected function getMimeType() { if ($this->fileType === IMG_WEBP) { return "image/webp"; } return image_type_to_mime_type($this->fileType); } /** * Init new width and height and do some sanity checks on constraints, before any * processing can be done. * * @return $this * @throws Exception */ public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // width as % if ($this->newWidth && $this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } // height as % if ($this->newHeight && $this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); // width & height from aspect ratio if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } // Change width & height based on dpr if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } // Check values to be within domain is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Calculate new width and height of image, based on settings. * * @return $this */ public function calculateNewWidthAndHeight() { // Crop, use cropped width and height as base for calulations $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); // Check if there is an area to crop off if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; // Check if crop is set if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } // Calculate new width and height if keeping aspect-ratio. if ($this->keepRatio) { $this->log("Keep aspect ratio."); // Crop-to-fit and both new width and height are set. if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { // Use newWidth and newHeigh as width/height, image should fit in box. $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { // Both new width and height are set. // Use newWidth and newHeigh as max width/height, image should not be larger. $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { // Use new width as max-width $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { // Use new height as max-hight $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } else { // Use existing width and height as new width and height. $this->newWidth = $width; $this->newHeight = $height; } // Get image dimensions for pre-resize image. if ($this->cropToFit || $this->fillToFit) { // Get relations of original & target image $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } elseif ($this->fillToFit) { // Use newWidth and newHeigh as defined width/height, // image should fit the area. $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } // Crop, ensure to set new width and height if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } // Fill to fit, ensure to set new width and height /*if ($this->fillToFit) { $this->log("FillToFit."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); }*/ // No new height or width is set, use existing measures. $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } /** * Re-calculate image dimensions when original image dimension has changed. * * @return $this */ public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } /** * Set extension for filename to save as. * * @param string $saveas extension to save image as * * @return $this */ public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image as: " . $this->extension); return $this; } /** * Set JPEG quality to use when saving image * * @param int $quality as the quality to set. * * @return $this */ public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } /** * Set PNG compressen algorithm to use when saving image * * @param int $compress as the algorithm to use. * * @return $this */ public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } /** * Use original image if possible, check options which affects image processing. * * @param boolean $useOrig default is to use original if possible, else set to false. * * @return $this */ public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) && !$this->lossy ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } /** * Generate filename to save file in cache. * * @param string $base as optional basepath for storing file. * @param boolean $useSubdir use or skip the subdir part when creating the * filename. * @param string $prefix to add as part of filename * * @return $this */ public function generateFilename($base = null, $useSubdir = true, $prefix = null) { $filename = basename($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $lossy = $this->lossy ? "_l" : null; $interlace = $this->interlace ? "_i" : null; $saveAs = $this->normalizeFileExtension(); $saveAs = $saveAs ? "_$saveAs" : null; $copyStrat = null; if ($this->copyStrategy === self::RESIZE) { $copyStrat = "_rs"; } $width = $this->newWidth ? '_' . $this->newWidth : null; $height = $this->newHeight ? '_' . $this->newHeight : null; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= "-".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $optimize = $this->jpegOptimize ? 'o' : null; $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = null; if ($useSubdir === true) { $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $subdir .= '_'; } $file = $prefix . $subdir . $filename . $width . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $compress . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . $copyStrat . $lossy . $interlace . $saveAs; return $this->setTarget($file, $base); } /** * Use cached version of image, if possible. * * @param boolean $useCache is default true, set to false to avoid using cached object. * * @return $this */ public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } /** * Load image from disk. Try to load image without verbose error message, * if fail, load again and display error messages. * * @param string $src of image. * @param string $dir as base directory where images are. * * @return $this * */ public function load($src = null, $dir = null) { if (isset($src)) { $this->setSource($src, $dir); } $this->loadImageDetails(); if ($this->fileType === IMG_WEBP) { $this->image = imagecreatefromwebp($this->pathToImage); } else { $imageAsString = file_get_contents($this->pathToImage); $this->image = imagecreatefromstring($imageAsString); } if ($this->image === false) { throw new Exception("Could not load image."); } /* Removed v0.7.7 if (image_type_to_mime_type($this->fileType) == 'image/png') { $type = $this->getPngType(); $hasFewColors = imagecolorstotal($this->image); if ($type == self::PNG_RGB_PALETTE || ($hasFewColors > 0 && $hasFewColors <= 256)) { if ($this->verbose) { $this->log("Handle this image as a palette image."); } $this->palette = true; } } */ if ($this->verbose) { $this->log("### Image successfully loaded from file."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->colorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } /** * Get the type of PNG image. * * @param string $filename to use instead of default. * * @return int as the type of the png-image * */ public function getPngType($filename = null) { $filename = $filename ? $filename : $this->pathToImage; $pngType = ord(file_get_contents($filename, false, null, 25, 1)); if ($this->verbose) { $this->log("Checking png type of: " . $filename); $this->log($this->getPngTypeAsString($pngType)); } return $pngType; } /** * Get the type of PNG image as a verbose string. * * @param integer $type to use, default is to check the type. * @param string $filename to use instead of default. * * @return int as the type of the png-image * */ private function getPngTypeAsString($pngType = null, $filename = null) { if ($filename || !$pngType) { $pngType = $this->getPngType($filename); } $index = imagecolortransparent($this->image); $transparent = null; if ($index != -1) { $transparent = " (transparent)"; } switch ($pngType) { case self::PNG_GREYSCALE: $text = "PNG is type 0, Greyscale$transparent"; break; case self::PNG_RGB: $text = "PNG is type 2, RGB$transparent"; break; case self::PNG_RGB_PALETTE: $text = "PNG is type 3, RGB with palette$transparent"; break; case self::PNG_GREYSCALE_ALPHA: $text = "PNG is type 4, Greyscale with alpha channel"; break; case self::PNG_RGB_ALPHA: $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)"; break; default: $text = "PNG is UNKNOWN type, is it really a PNG image?"; } return $text; } /** * Calculate number of colors in an image. * * @param resource $im the image. * * @return int */ private function colorsTotal($im) { if (imageistruecolor($im)) { $this->log("Colors as true color."); $h = imagesy($im); $w = imagesx($im); $c = array(); for ($x=0; $x < $w; $x++) { for ($y=0; $y < $h; $y++) { @$c['c'.imagecolorat($im, $x, $y)]++; } } return count($c); } else { $this->log("Colors as palette."); return imagecolorstotal($im); } } /** * Preprocess image before rezising it. * * @return $this */ public function preResize() { $this->log("### Pre-process before resizing"); // Rotate image if ($this->rotateBefore) { $this->log("Rotating image."); $this->rotate($this->rotateBefore, $this->bgColor) ->reCalculateDimensions(); } // Auto-rotate image if ($this->autoRotate) { $this->log("Auto rotating image."); $this->rotateExif() ->reCalculateDimensions(); } // Scale the original image before starting if (isset($this->scale)) { $this->log("Scale by {$this->scale}%"); $newWidth = $this->width * $this->scale / 100; $newHeight = $this->height * $this->scale / 100; $img = $this->CreateImageKeepTransparency($newWidth, $newHeight); imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height); $this->image = $img; $this->width = $newWidth; $this->height = $newHeight; } return $this; } /** * Resize or resample the image while resizing. * * @param int $strategy as CImage::RESIZE or CImage::RESAMPLE * * @return $this */ public function setCopyResizeStrategy($strategy) { $this->copyStrategy = $strategy; return $this; } /** * Resize and or crop the image. * * @return void */ public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) { if($this->copyStrategy == self::RESIZE) { $this->log("Copy by resize"); imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } else { $this->log("Copy by resample"); imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } } /** * Resize and or crop the image. * * @return $this */ public function resize() { $this->log("### Starting to Resize()"); $this->log("Upscale = '$this->upscale'"); // Only use a specified area of the image, $this->offset is defining the area to use if (isset($this->offset)) { $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}"); $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']); imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']); $this->image = $img; $this->width = $this->offset['width']; $this->height = $this->offset['height']; } if ($this->crop) { // Do as crop, take only part of image $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}"); $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']); imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']); $this->image = $img; $this->width = $this->crop['width']; $this->height = $this->crop['height']; } if (!$this->upscale) { // Consider rewriting the no-upscale code to fit within this if-statement, // likely to be more readable code. // The code is more or leass equal in below crop-to-fit, fill-to-fit and stretch } if ($this->cropToFit) { // Resize by crop to fit $this->log("Resizing using strategy - Crop to fit"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight)) { $this->log("Resizing - smaller image, do not upscale."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newWidth < $this->width) { $cropX = round(($this->width/2) - ($this->newWidth/2)); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } if ($this->newHeight < $this->height) { $cropY = round(($this->height/2) - ($this->newHeight/2)); } $this->log(" cwidth: $this->cropWidth"); $this->log(" cheight: $this->cropHeight"); $this->log(" nwidth: $this->newWidth"); $this->log(" nheight: $this->newHeight"); $this->log(" width: $this->width"); $this->log(" height: $this->height"); $this->log(" posX: $posX"); $this->log(" posY: $posY"); $this->log(" cropX: $cropX"); $this->log(" cropY: $cropY"); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); } else { $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $imgPreCrop = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif ($this->fillToFit) { // Resize by fill to fit $this->log("Resizing using strategy - Fill to fit"); $posX = 0; $posY = 0; $ratioOrig = $this->width / $this->height; $ratioNew = $this->newWidth / $this->newHeight; // Check ratio for landscape or portrait if ($ratioOrig < $ratioNew) { $posX = round(($this->newWidth - $this->fillWidth) / 2); } else { $posY = round(($this->newHeight - $this->fillHeight) / 2); } if (!$this->upscale && ($this->width < $this->newWidth && $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) { // Resize it $this->log("Resizing, new height and/or width"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); if (!$this->keepRatio) { $this->log("Resizing - stretch to fit selected."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width && $this->newHeight > $this->height) { $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); } elseif ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } elseif ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } return $this; } /** * Postprocess image after rezising image. * * @return $this */ public function postResize() { $this->log("### Post-process after resizing"); // Rotate image if ($this->rotateAfter) { $this->log("Rotating image."); $this->rotate($this->rotateAfter, $this->bgColor); } // Apply filters if (isset($this->filters) && is_array($this->filters)) { foreach ($this->filters as $filter) { $this->log("Applying filter {$filter['type']}."); switch ($filter['argc']) { case 0: imagefilter($this->image, $filter['type']); break; case 1: imagefilter($this->image, $filter['type'], $filter['arg1']); break; case 2: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']); break; case 3: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']); break; case 4: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']); break; } } } // Convert to palette image if ($this->palette) { $this->log("Converting to palette image."); $this->trueColorToPalette(); } // Blur the image if ($this->blur) { $this->log("Blur."); $this->blurImage(); } // Emboss the image if ($this->emboss) { $this->log("Emboss."); $this->embossImage(); } // Sharpen the image if ($this->sharpen) { $this->log("Sharpen."); $this->sharpenImage(); } // Custom convolution if ($this->convolve) { //$this->log("Convolve: " . $this->convolve); $this->imageConvolution(); } return $this; } /** * Rotate image using angle. * * @param float $angle to rotate image. * @param int $anglebgColor to fill image with if needed. * * @return $this */ public function rotate($angle, $bgColor) { $this->log("Rotate image " . $angle . " degrees with filler color."); $color = $this->getBackgroundColor(); $this->image = imagerotate($this->image, $angle, $color); $this->width = imagesx($this->image); $this->height = imagesy($this->image); $this->log("New image dimension width x height: " . $this->width . " x " . $this->height); return $this; } /** * Rotate image using information in EXIF. * * @return $this */ public function rotateExif() { if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) { $this->log("Autorotate ignored, EXIF not supported by this filetype."); return $this; } $exif = exif_read_data($this->pathToImage); if (!empty($exif['Orientation'])) { switch ($exif['Orientation']) { case 3: $this->log("Autorotate 180."); $this->rotate(180, $this->bgColor); break; case 6: $this->log("Autorotate -90."); $this->rotate(-90, $this->bgColor); break; case 8: $this->log("Autorotate 90."); $this->rotate(90, $this->bgColor); break; default: $this->log("Autorotate ignored, unknown value as orientation."); } } else { $this->log("Autorotate ignored, no orientation in EXIF."); } return $this; } /** * Convert true color image to palette image, keeping alpha. * http://stackoverflow.com/questions/5752514/how-to-convert-png-to-8-bit-png-using-php-gd-library * * @return void */ public function trueColorToPalette() { $img = imagecreatetruecolor($this->width, $this->height); $bga = imagecolorallocatealpha($img, 0, 0, 0, 127); imagecolortransparent($img, $bga); imagefill($img, 0, 0, $bga); imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height); imagetruecolortopalette($img, false, 255); imagesavealpha($img, true); if (imageistruecolor($this->image)) { $this->log("Matching colors with true color image."); imagecolormatch($this->image, $img); } $this->image = $img; } /** * Sharpen image using image convolution. * * @return $this */ public function sharpenImage() { $this->imageConvolution('sharpen'); return $this; } /** * Emboss image using image convolution. * * @return $this */ public function embossImage() { $this->imageConvolution('emboss'); return $this; } /** * Blur image using image convolution. * * @return $this */ public function blurImage() { $this->imageConvolution('blur'); return $this; } /** * Create convolve expression and return arguments for image convolution. * * @param string $expression constant string which evaluates to a list of * 11 numbers separated by komma or such a list. * * @return array as $matrix (3x3), $divisor and $offset */ public function createConvolveArguments($expression) { // Check of matching constant if (isset($this->convolves[$expression])) { $expression = $this->convolves[$expression]; } $part = explode(',', $expression); $this->log("Creating convolution expressen: $expression"); // Expect list of 11 numbers, split by , and build up arguments if (count($part) != 11) { throw new Exception( "Missmatch in argument convolve. Expected comma-separated string with 11 float values. Got $expression." ); } array_walk($part, function ($item, $key) { if (!is_numeric($item)) { throw new Exception("Argument to convolve expression should be float but is not."); } }); return array( array( array($part[0], $part[1], $part[2]), array($part[3], $part[4], $part[5]), array($part[6], $part[7], $part[8]), ), $part[9], $part[10], ); } /** * Add custom expressions (or overwrite existing) for image convolution. * * @param array $options Key value array with strings to be converted * to convolution expressions. * * @return $this */ public function addConvolveExpressions($options) { $this->convolves = array_merge($this->convolves, $options); return $this; } /** * Image convolution. * * @param string $options A string with 11 float separated by comma. * * @return $this */ public function imageConvolution($options = null) { // Use incoming options or use $this. $options = $options ? $options : $this->convolve; // Treat incoming as string, split by + $this->log("Convolution with '$options'"); $options = explode(":", $options); // Check each option if it matches constant value foreach ($options as $option) { list($matrix, $divisor, $offset) = $this->createConvolveArguments($option); imageconvolution($this->image, $matrix, $divisor, $offset); } return $this; } /** * Set default background color between 000000-FFFFFF or if using * alpha 00000000-FFFFFF7F. * * @param string $color as hex value. * * @return $this */ public function setDefaultBackgroundColor($color) { $this->log("Setting default background color to '$color'."); if (!(strlen($color) == 6 || strlen($color) == 8)) { throw new Exception( "Background color needs a hex value of 6 or 8 digits. 000000-FFFFFF or 00000000-FFFFFF7F. Current value was: '$color'." ); } $red = hexdec(substr($color, 0, 2)); $green = hexdec(substr($color, 2, 2)); $blue = hexdec(substr($color, 4, 2)); $alpha = (strlen($color) == 8) ? hexdec(substr($color, 6, 2)) : null; if (($red < 0 || $red > 255) || ($green < 0 || $green > 255) || ($blue < 0 || $blue > 255) || ($alpha < 0 || $alpha > 127) ) { throw new Exception( "Background color out of range. Red, green blue should be 00-FF and alpha should be 00-7F. Current value was: '$color'." ); } $this->bgColor = strtolower($color); $this->bgColorDefault = array( 'red' => $red, 'green' => $green, 'blue' => $blue, 'alpha' => $alpha ); return $this; } /** * Get the background color. * * @param resource $img the image to work with or null if using $this->image. * * @return color value or null if no background color is set. */ private function getBackgroundColor($img = null) { $img = isset($img) ? $img : $this->image; if ($this->bgColorDefault) { $red = $this->bgColorDefault['red']; $green = $this->bgColorDefault['green']; $blue = $this->bgColorDefault['blue']; $alpha = $this->bgColorDefault['alpha']; if ($alpha) { $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha); } else { $color = imagecolorallocate($img, $red, $green, $blue); } return $color; } else { return 0; } } /** * Create a image and keep transparency for png and gifs. * * @param int $width of the new image. * @param int $height of the new image. * * @return image resource. */ private function createImageKeepTransparency($width, $height) { $this->log("Creating a new working image width={$width}px, height={$height}px."); $img = imagecreatetruecolor($width, $height); imagealphablending($img, false); imagesavealpha($img, true); $index = $this->image ? imagecolortransparent($this->image) : -1; if ($index != -1) { imagealphablending($img, true); $transparent = imagecolorsforindex($this->image, $index); $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']); imagefill($img, 0, 0, $color); $index = imagecolortransparent($img, $color); $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index"); } elseif ($this->bgColorDefault) { $color = $this->getBackgroundColor($img); imagefill($img, 0, 0, $color); $this->Log("Filling image with background color."); } return $img; } /** * Set optimizing and post-processing options. * * @param array $options with config for postprocessing with external tools. * * @return $this */ public function setPostProcessingOptions($options) { if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) { $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd']; } else { $this->jpegOptimizeCmd = null; } if (array_key_exists("png_lossy", $options) && $options['png_lossy'] !== false) { $this->pngLossy = $options['png_lossy']; $this->pngLossyCmd = $options['png_lossy_cmd']; } else { $this->pngLossyCmd = null; } if (isset($options['png_filter']) && $options['png_filter']) { $this->pngFilterCmd = $options['png_filter_cmd']; } else { $this->pngFilterCmd = null; } if (isset($options['png_deflate']) && $options['png_deflate']) { $this->pngDeflateCmd = $options['png_deflate_cmd']; } else { $this->pngDeflateCmd = null; } return $this; } /** * Find out the type (file extension) for the image to be saved. * * @return string as image extension. */ protected function getTargetImageExtension() { // switch on mimetype if (isset($this->extension)) { return strtolower($this->extension); } elseif ($this->fileType === IMG_WEBP) { return "webp"; } return substr(image_type_to_extension($this->fileType), 1); } /** * Save image. * * @param string $src as target filename. * @param string $base as base directory where to store images. * @param boolean $overwrite or not, default to always overwrite file. * * @return $this or false if no folder is set. */ public function save($src = null, $base = null, $overwrite = true) { if (isset($src)) { $this->setTarget($src, $base); } if ($overwrite === false && is_file($this->cacheFileName)) { $this->Log("Not overwriting file since its already exists and \$overwrite if false."); return; } if (!defined("WINDOWS2WSL")) { is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); } $type = $this->getTargetImageExtension(); $this->Log("Saving image as " . $type); switch($type) { case 'jpeg': case 'jpg': // Set as interlaced progressive JPEG if ($this->interlace) { $this->Log("Set JPEG image to be interlaced."); $res = imageinterlace($this->image, true); } $this->Log("Saving image as JPEG to cache using quality = {$this->quality}."); imagejpeg($this->image, $this->cacheFileName, $this->quality); // Use JPEG optimize if defined if ($this->jpegOptimizeCmd) { if ($this->verbose) { clearstatcache(); $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->log($cmd); $this->log($res); } break; case 'gif': $this->Log("Saving image as GIF to cache."); imagegif($this->image, $this->cacheFileName); break; case 'webp': $this->Log("Saving image as WEBP to cache using quality = {$this->quality}."); imagewebp($this->image, $this->cacheFileName, $this->quality); break; case 'png': default: $this->Log("Saving image as PNG to cache using compression = {$this->compress}."); // Turn off alpha blending and set alpha flag imagealphablending($this->image, false); imagesavealpha($this->image, true); imagepng($this->image, $this->cacheFileName, $this->compress); // Use external program to process lossy PNG, if defined $lossyEnabled = $this->pngLossy === true; $lossySoftEnabled = $this->pngLossy === null; $lossyActiveEnabled = $this->lossy === true; if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) { if ($this->verbose) { clearstatcache(); $this->log("Lossy enabled: $lossyEnabled"); $this->log("Lossy soft enabled: $lossySoftEnabled"); $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } // Use external program to filter PNG, if defined if ($this->pngFilterCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngFilterCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } // Use external program to deflate PNG, if defined if ($this->pngDeflateCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngDeflateCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } break; } if ($this->verbose) { clearstatcache(); $this->log("Saved image to cache."); $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } /** * Convert image from one colorpsace/color profile to sRGB without * color profile. * * @param string $src of image. * @param string $dir as base directory where images are. * @param string $cache as base directory where to store images. * @param string $iccFile filename of colorprofile. * @param boolean $useCache or not, default to always use cache. * * @return string | boolean false if no conversion else the converted * filename. */ public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true) { if ($this->verbose) { $this->log("# Converting image to sRGB colorspace."); } if (!class_exists("Imagick")) { $this->log(" Ignoring since Imagemagick is not installed."); return false; } // Prepare $this->setSaveFolder($cache) ->setSource($src, $dir) ->generateFilename(null, false, 'srgb_'); // Check if the cached version is accurate. if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { $this->log(" Using cached version: " . $this->cacheFileName); return $this->cacheFileName; } } // Only covert if cachedir is writable if (is_writable($this->saveFolder)) { // Load file and check if conversion is needed $image = new Imagick($this->pathToImage); $colorspace = $image->getImageColorspace(); $this->log(" Current colorspace: " . $colorspace); $profiles = $image->getImageProfiles('*', false); $hasICCProfile = (array_search('icc', $profiles) !== false); $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO")); if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) { $this->log(" Converting to sRGB."); $sRGBicc = file_get_contents($iccFile); $image->profileImage('icc', $sRGBicc); $image->transformImageColorspace(Imagick::COLORSPACE_SRGB); $image->writeImage($this->cacheFileName); return $this->cacheFileName; } } return false; } /** * Create a hard link, as an alias, to the cached file. * * @param string $alias where to store the link, * filename without extension. * * @return $this */ public function linkToCacheFile($alias) { if ($alias === null) { $this->log("Ignore creating alias."); return $this; } if (is_readable($alias)) { unlink($alias); } $res = link($this->cacheFileName, $alias); if ($res) { $this->log("Created an alias as: $alias"); } else { $this->log("Failed to create the alias: $alias"); } return $this; } /** * Add HTTP header for output together with image. * * @param string $type the header type such as "Cache-Control" * @param string $value the value to use * * @return void */ public function addHTTPHeader($type, $value) { $this->HTTPHeader[$type] = $value; } /** * Output image to browser using caching. * * @param string $file to read and output, default is to * use $this->cacheFileName * @param string $format set to json to output file as json * object with details * * @return void */ public function output($file = null, $format = null) { if (is_null($file)) { $file = $this->cacheFileName; } if (is_null($format)) { $format = $this->outputFormat; } $this->log("### Output"); $this->log("Output format is: $format"); if (!$this->verbose && $format == 'json') { header('Content-type: application/json'); echo $this->json($file); exit; } elseif ($format == 'ascii') { header('Content-type: text/plain'); echo $this->ascii($file); exit; } $this->log("Outputting image: $file"); // Get image modification time clearstatcache(); $lastModified = filemtime($file); $lastModifiedFormat = "D, d M Y H:i:s"; $gmdate = gmdate($lastModifiedFormat, $lastModified); if (!$this->verbose) { $header = "Last-Modified: $gmdate GMT"; header($header); $this->fastTrackCache->addHeader($header); $this->fastTrackCache->setLastModified($lastModified); } foreach ($this->HTTPHeader as $key => $val) { $header = "$key: $val"; header($header); $this->fastTrackCache->addHeader($header); } if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) { if ($this->verbose) { $this->log("304 not modified"); $this->verboseOutput(); exit; } header("HTTP/1.0 304 Not Modified"); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 304"); } } else { $this->loadImageDetails($file); $mime = $this->getMimeType(); $size = filesize($file); if ($this->verbose) { $this->log("Last-Modified: " . $gmdate . " GMT"); $this->log("Content-type: " . $mime); $this->log("Content-length: " . $size); $this->verboseOutput(); if (is_null($this->verboseFileName)) { exit; } } $header = "Content-type: $mime"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $header = "Content-length: $size"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $this->fastTrackCache->setSource($file); $this->fastTrackCache->writeToCache(); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 200"); } readfile($file); } exit; } /** * Create a JSON object from the image details. * * @param string $file the file to output. * * @return string json-encoded representation of the image. */ public function json($file = null) { $file = $file ? $file : $this->cacheFileName; $details = array(); clearstatcache(); $details['src'] = $this->imageSrc; $lastModified = filemtime($this->pathToImage); $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $details['cache'] = basename($this->cacheFileName ?? ""); $lastModified = filemtime($this->cacheFileName ?? ""); $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $this->load($file); $details['filename'] = basename($file ?? ""); $details['mimeType'] = $this->getMimeType($this->fileType); $details['width'] = $this->width; $details['height'] = $this->height; $details['aspectRatio'] = round($this->width / $this->height, 3); $details['size'] = filesize($file ?? ""); $details['colors'] = $this->colorsTotal($this->image); $details['includedFiles'] = count(get_included_files()); $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ; $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB"; $details['memoryLimit'] = ini_get('memory_limit'); if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s"; } if ($details['mimeType'] == 'image/png') { $details['pngType'] = $this->getPngTypeAsString(null, $file); } $options = null; if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; } return json_encode($details, $options); } /** * Set options for creating ascii version of image. * * @param array $options empty to use default or set options to change. * * @return void. */ public function setAsciiOptions($options = array()) { $this->asciiOptions = $options; } /** * Create an ASCII version from the image details. * * @param string $file the file to output. * * @return string ASCII representation of the image. */ public function ascii($file = null) { $file = $file ? $file : $this->cacheFileName; $asciiArt = new CAsciiArt(); $asciiArt->setOptions($this->asciiOptions); return $asciiArt->createFromFile($file); } /** * Log an event if verbose mode. * * @param string $message to log. * * @return this */ public function log($message) { if ($this->verbose) { $this->log[] = $message; } return $this; } /** * Do verbose output to a file. * * @param string $fileName where to write the verbose output. * * @return void */ public function setVerboseToFile($fileName) { $this->log("Setting verbose output to file."); $this->verboseFileName = $fileName; } /** * Do verbose output and print out the log and the actual images. * * @return void */ private function verboseOutput() { $log = null; $this->log("### Summary of verbose log"); $this->log("As JSON: \n" . $this->json()); $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M"); $this->log("Memory limit: " . ini_get('memory_limit')); $included = get_included_files(); $this->log("Included files: " . count($included)); foreach ($this->log as $val) { if (is_array($val)) { foreach ($val as $val1) { $log .= htmlentities($val1) . '
{$log}
EOD;
}
}
/**
* Raise error, enables to implement a selection of error methods.
*
* @param string $message the error message to display.
*
* @return void
* @throws Exception
*/
private function raiseError($message)
{
throw new Exception($message);
}
}
/**
* Deal with the cache directory and cached items.
*
*/
class CCache
{
/**
* Path to the cache directory.
*/
private $path;
/**
* Set the path to the cache dir which must exist.
*
* @param string path to the cache dir.
*
* @throws Exception when $path is not a directory.
*
* @return $this
*/
public function setDir($path)
{
if (!is_dir($path)) {
throw new Exception("Cachedir is not a directory.");
}
$this->path = $path;
return $this;
}
/**
* Get the path to the cache subdir and try to create it if its not there.
*
* @param string $subdir name of subdir
* @param array $create default is to try to create the subdir
*
* @return string | boolean as real path to the subdir or
* false if it does not exists
*/
public function getPathToSubdir($subdir, $create = true)
{
$path = realpath($this->path . "/" . $subdir);
if (is_dir($path)) {
return $path;
}
if ($create && defined('WINDOWS2WSL')) {
// Special case to solve Windows 2 WSL integration
$path = $this->path . "/" . $subdir;
if (mkdir($path)) {
return realpath($path);
}
}
if ($create && is_writable($this->path)) {
$path = $this->path . "/" . $subdir;
if (mkdir($path)) {
return realpath($path);
}
}
return false;
}
/**
* Get status of the cache subdir.
*
* @param string $subdir name of subdir
*
* @return string with status
*/
public function getStatusOfSubdir($subdir)
{
$path = realpath($this->path . "/" . $subdir);
$exists = is_dir($path);
$res = $exists ? "exists" : "does not exist";
if ($exists) {
$res .= is_writable($path) ? ", writable" : ", not writable";
}
return $res;
}
/**
* Remove the cache subdir.
*
* @param string $subdir name of subdir
*
* @return null | boolean true if success else false, null if no operation
*/
public function removeSubdir($subdir)
{
$path = realpath($this->path . "/" . $subdir);
if (is_dir($path)) {
return rmdir($path);
}
return null;
}
}
/**
* Enable a fast track cache with a json representation of the image delivery.
*
*/
class CFastTrackCache
{
/**
* Cache is disabled to start with.
*/
private $enabled = false;
/**
* Path to the cache directory.
*/
private $path;
/**
* Filename of current cache item.
*/
private $filename;
/**
* Container with items to store as cached item.
*/
private $container;
/**
* Enable or disable cache.
*
* @param boolean $enable set to true to enable, false to disable
*
* @return $this
*/
public function enable($enabled)
{
$this->enabled = $enabled;
return $this;
}
/**
* Set the path to the cache dir which must exist.
*
* @param string $path to the cache dir.
*
* @throws Exception when $path is not a directory.
*
* @return $this
*/
public function setCacheDir($path)
{
if (!is_dir($path)) {
throw new Exception("Cachedir is not a directory.");
}
$this->path = rtrim($path, "/");
return $this;
}
/**
* Set the filename to store in cache, use the querystring to create that
* filename.
*
* @param array $clear items to clear in $_GET when creating the filename.
*
* @return string as filename created.
*/
public function setFilename($clear)
{
$query = $_GET;
// Remove parts from querystring that should not be part of filename
foreach ($clear as $value) {
unset($query[$value]);
}
arsort($query);
$queryAsString = http_build_query($query);
$this->filename = md5($queryAsString);
if (CIMAGE_DEBUG) {
$this->container["query-string"] = $queryAsString;
}
return $this->filename;
}
/**
* Add header items.
*
* @param string $header add this as header.
*
* @return $this
*/
public function addHeader($header)
{
$this->container["header"][] = $header;
return $this;
}
/**
* Add header items on output, these are not output when 304.
*
* @param string $header add this as header.
*
* @return $this
*/
public function addHeaderOnOutput($header)
{
$this->container["header-output"][] = $header;
return $this;
}
/**
* Set path to source image to.
*
* @param string $source path to source image file.
*
* @return $this
*/
public function setSource($source)
{
$this->container["source"] = $source;
return $this;
}
/**
* Set last modified of source image, use to check for 304.
*
* @param string $lastModified
*
* @return $this
*/
public function setLastModified($lastModified)
{
$this->container["last-modified"] = $lastModified;
return $this;
}
/**
* Get filename of cached item.
*
* @return string as filename.
*/
public function getFilename()
{
return $this->path . "/" . $this->filename;
}
/**
* Write current item to cache.
*
* @return boolean if cache file was written.
*/
public function writeToCache()
{
if (!$this->enabled) {
return false;
}
if (is_dir($this->path) && is_writable($this->path)) {
$filename = $this->getFilename();
return file_put_contents($filename, json_encode($this->container)) !== false;
}
return false;
}
/**
* Output current item from cache, if available.
*
* @return void
*/
public function output()
{
$filename = $this->getFilename();
if (!is_readable($filename)) {
return;
}
$item = json_decode(file_get_contents($filename), true);
if (!is_readable($item["source"])) {
return;
}
foreach ($item["header"] as $value) {
header($value);
}
if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])
&& strtotime($_SERVER["HTTP_IF_MODIFIED_SINCE"]) == $item["last-modified"]) {
header("HTTP/1.0 304 Not Modified");
if (CIMAGE_DEBUG) {
trace(__CLASS__ . " 304");
}
exit;
}
foreach ($item["header-output"] as $value) {
header($value);
}
if (CIMAGE_DEBUG) {
trace(__CLASS__ . " 200");
}
readfile($item["source"]);
exit;
}
}
/**
* Resize and crop images on the fly, store generated images in a cache.
*
* @author Mikael Roos mos@dbwebb.se
* @example http://dbwebb.se/opensource/cimage
* @link https://github.com/mosbth/cimage
*
*/
/**
* Custom exception handler.
*/
set_exception_handler(function ($exception) {
errorPage(
"img.php: Uncaught exception:
" . $exception->getMessage() . "
"
. $exception->getTraceAsString()
. "",
500
);
});
/**
* Get configuration options from file, if the file exists, else use $config
* if its defined or create an empty $config.
*/
$configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php';
if (is_file($configFile)) {
$config = require $configFile;
} elseif (!isset($config)) {
$config = array();
}
// Make CIMAGE_DEBUG false by default, if not already defined
if (!defined("CIMAGE_DEBUG")) {
define("CIMAGE_DEBUG", false);
}
/**
* Setup the autoloader, but not when using a bundle.
*/
if (!defined("CIMAGE_BUNDLE")) {
if (!isset($config["autoloader"])) {
die("CImage: Missing autoloader.");
}
require $config["autoloader"];
}
/**
* verbose, v - do a verbose dump of what happens
* vf - do verbose dump to file
*/
$verbose = getDefined(array('verbose', 'v'), true, false);
$verboseFile = getDefined('vf', true, false);
verbose("img.php version = " . CIMAGE_VERSION);
/**
* status - do a verbose dump of the configuration
*/
$status = getDefined('status', true, false);
/**
* Set mode as strict, production or development.
* Default is production environment.
*/
$mode = getConfig('mode', 'production');
// Settings for any mode
set_time_limit(20);
ini_set('gd.jpeg_ignore_warning', 1);
if (!extension_loaded('gd')) {
errorPage("Extension gd is not loaded.", 500);
}
// Specific settings for each mode
if ($mode == 'strict') {
error_reporting(0);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'production') {
error_reporting(-1);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$verbose = false;
$status = false;
$verboseFile = false;
} elseif ($mode == 'development') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
$verboseFile = false;
} elseif ($mode == 'test') {
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('log_errors', 0);
} else {
errorPage("Unknown mode: $mode", 500);
}
verbose("mode = $mode");
verbose("error log = " . ini_get('error_log'));
/**
* Set default timezone if not set or if its set in the config-file.
*/
$defaultTimezone = getConfig('default_timezone', null);
if ($defaultTimezone) {
date_default_timezone_set($defaultTimezone);
} elseif (!ini_get('default_timezone')) {
date_default_timezone_set('UTC');
}
/**
* Check if passwords are configured, used and match.
* Options decide themself if they require passwords to be used.
*/
$pwdConfig = getConfig('password', false);
$pwdAlways = getConfig('password_always', false);
$pwdType = getConfig('password_type', 'text');
$pwd = get(array('password', 'pwd'), null);
// Check if passwords match, if configured to use passwords
$passwordMatch = null;
if ($pwd) {
switch ($pwdType) {
case 'md5':
$passwordMatch = ($pwdConfig === md5($pwd));
break;
case 'hash':
$passwordMatch = password_verify($pwd, $pwdConfig);
break;
case 'text':
$passwordMatch = ($pwdConfig === $pwd);
break;
default:
$passwordMatch = false;
}
}
if ($pwdAlways && $passwordMatch !== true) {
errorPage("Password required and does not match or exists.", 403);
}
verbose("password match = $passwordMatch");
/**
* Prevent hotlinking, leeching, of images by controlling who access them
* from where.
*
*/
$allowHotlinking = getConfig('allow_hotlinking', true);
$hotlinkingWhitelist = getConfig('hotlinking_whitelist', array());
$serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null;
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
$refererHost = parse_url($referer ?? "", PHP_URL_HOST);
if (!$allowHotlinking) {
if ($passwordMatch) {
; // Always allow when password match
verbose("Hotlinking since passwordmatch");
} elseif ($passwordMatch === false) {
errorPage("Hotlinking/leeching not allowed when password missmatch.", 403);
} elseif (!$referer) {
errorPage("Hotlinking/leeching not allowed and referer is missing.", 403);
} elseif (strcmp($serverName, $refererHost) == 0) {
; // Allow when serverName matches refererHost
verbose("Hotlinking disallowed but serverName matches refererHost.");
} elseif (!empty($hotlinkingWhitelist)) {
$whitelist = new CWhitelist();
$allowedByWhitelist = $whitelist->check($refererHost, $hotlinkingWhitelist);
if ($allowedByWhitelist) {
verbose("Hotlinking/leeching allowed by whitelist.");
} else {
errorPage("Hotlinking/leeching not allowed by whitelist. Referer: $referer.", 403);
}
} else {
errorPage("Hotlinking/leeching not allowed.", 403);
}
}
verbose("allow_hotlinking = $allowHotlinking");
verbose("referer = $referer");
verbose("referer host = $refererHost");
/**
* Create the class for the image.
*/
$CImage = getConfig('CImage', 'CImage');
$img = new $CImage();
$img->setVerbose($verbose || $verboseFile);
/**
* Get the cachepath from config.
*/
$CCache = getConfig('CCache', 'CCache');
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');
$cache = new $CCache();
$cache->setDir($cachePath);
/**
* no-cache, nc - skip the cached version and process and create a new version in cache.
*/
$useCache = getDefined(array('no-cache', 'nc'), false, true);
verbose("use cache = $useCache");
/**
* Prepare fast track cache for swriting cache items.
*/
$fastTrackCache = "fasttrack";
$allowFastTrackCache = getConfig('fast_track_allow', false);
$CFastTrackCache = getConfig('CFastTrackCache', 'CFastTrackCache');
$ftc = new $CFastTrackCache();
$ftc->setCacheDir($cache->getPathToSubdir($fastTrackCache))
->enable($allowFastTrackCache)
->setFilename(array('no-cache', 'nc'));
$img->injectDependency("fastTrackCache", $ftc);
/**
* Load and output images from fast track cache, if items are available
* in cache.
*/
if ($useCache && $allowFastTrackCache) {
if (CIMAGE_DEBUG) {
trace("img.php fast track cache enabled and used");
}
$ftc->output();
}
/**
* Allow or disallow remote download of images from other servers.
* Passwords apply if used.
*
*/
$allowRemote = getConfig('remote_allow', false);
if ($allowRemote && $passwordMatch !== false) {
$cacheRemote = $cache->getPathToSubdir("remote");
$pattern = getConfig('remote_pattern', null);
$img->setRemoteDownload($allowRemote, $cacheRemote, $pattern);
$whitelist = getConfig('remote_whitelist', null);
$img->setRemoteHostWhitelist($whitelist);
}
/**
* shortcut, sc - extend arguments with a constant value, defined
* in config-file.
*/
$shortcut = get(array('shortcut', 'sc'), null);
$shortcutConfig = getConfig('shortcut', array(
'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
));
verbose("shortcut = $shortcut");
if (isset($shortcut)
&& isset($shortcutConfig[$shortcut])) {
parse_str($shortcutConfig[$shortcut], $get);
verbose("shortcut-constant = {$shortcutConfig[$shortcut]}");
$_GET = array_merge($_GET, $get);
}
/**
* src - the source image file.
*/
$srcImage = urldecode(get('src', ""))
or errorPage('Must set src-attribute.', 404);
// Get settings for src-alt as backup image
$srcAltImage = urldecode(get('src-alt', ""));
$srcAltConfig = getConfig('src_alt', null);
if (empty($srcAltImage)) {
$srcAltImage = $srcAltConfig;
}
// Check for valid/invalid characters
$imagePath = getConfig('image_path', __DIR__ . '/img/');
$imagePathConstraint = getConfig('image_path_constraint', true);
$validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_ \.:]+$#');
// Source is remote
$remoteSource = false;
// Dummy image feature
$dummyEnabled = getConfig('dummy_enabled', true);
$dummyFilename = getConfig('dummy_filename', 'dummy');
$dummyImage = false;
preg_match($validFilename, $srcImage)
or errorPage('Source filename contains invalid characters.', 404);
if ($dummyEnabled && $srcImage === $dummyFilename) {
// Prepare to create a dummy image and use it as the source image.
$dummyImage = true;
} elseif ($allowRemote && $img->isRemoteSource($srcImage)) {
// If source is a remote file, ignore local file checks.
$remoteSource = true;
} else {
// Check if file exists on disk or try using src-alt
$pathToImage = realpath($imagePath . $srcImage);
if (!is_file($pathToImage) && !empty($srcAltImage)) {
// Try using the src-alt instead
$srcImage = $srcAltImage;
$pathToImage = realpath($imagePath . $srcImage);
preg_match($validFilename, $srcImage)
or errorPage('Source (alt) filename contains invalid characters.', 404);
if ($dummyEnabled && $srcImage === $dummyFilename) {
// Check if src-alt is the dummy image
$dummyImage = true;
}
}
if (!$dummyImage) {
is_file($pathToImage)
or errorPage(
'Source image is not a valid file, check the filename and that a
matching file exists on the filesystem.',
404
);
}
}
if ($imagePathConstraint && !$dummyImage && !$remoteSource) {
// Check that the image is a file below the directory 'image_path'.
$imageDir = realpath($imagePath);
substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0
or errorPage(
'Security constraint: Source image is not below the directory "image_path"
as specified in the config file img_config.php.',
404
);
}
verbose("src = $srcImage");
/**
* Manage size constants from config file, use constants to replace values
* for width and height.
*/
$sizeConstant = getConfig('size_constant', function () {
// Set sizes to map constant to value, easier to use with width or height
$sizes = array(
'w1' => 613,
'w2' => 630,
);
// Add grid column width, useful for use as predefined size for width (or height).
$gridColumnWidth = 30;
$gridGutterWidth = 10;
$gridColumns = 24;
for ($i = 1; $i <= $gridColumns; $i++) {
$sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth;
}
return $sizes;
});
$sizes = call_user_func($sizeConstant);
/**
* width, w - set target width, affecting the resulting image width, height and resize options
*/
$newWidth = get(array('width', 'w'));
$maxWidth = getConfig('max_width', 2000);
// Check to replace predefined size
if (isset($sizes[$newWidth])) {
$newWidth = $sizes[$newWidth];
}
// Support width as % of original width
if ($newWidth && $newWidth[strlen($newWidth)-1] == '%') {
is_numeric(substr($newWidth, 0, -1))
or errorPage('Width % not numeric.', 404);
} else {
is_null($newWidth)
or ($newWidth > 10 && $newWidth <= $maxWidth)
or errorPage('Width out of range.', 404);
}
verbose("new width = $newWidth");
/**
* height, h - set target height, affecting the resulting image width, height and resize options
*/
$newHeight = get(array('height', 'h'));
$maxHeight = getConfig('max_height', 2000);
// Check to replace predefined size
if (isset($sizes[$newHeight])) {
$newHeight = $sizes[$newHeight];
}
// height
if ($newHeight && $newHeight[strlen($newHeight)-1] == '%') {
is_numeric(substr($newHeight, 0, -1))
or errorPage('Height % out of range.', 404);
} else {
is_null($newHeight)
or ($newHeight > 10 && $newHeight <= $maxHeight)
or errorPage('Height out of range.', 404);
}
verbose("new height = $newHeight");
/**
* aspect-ratio, ar - affecting the resulting image width, height and resize options
*/
$aspectRatio = get(array('aspect-ratio', 'ar'));
$aspectRatioConstant = getConfig('aspect_ratio_constant', function () {
return array(
'3:1' => 3/1,
'3:2' => 3/2,
'4:3' => 4/3,
'8:5' => 8/5,
'16:10' => 16/10,
'16:9' => 16/9,
'golden' => 1.618,
);
});
// Check to replace predefined aspect ratio
$aspectRatios = call_user_func($aspectRatioConstant);
$negateAspectRatio = ($aspectRatio && $aspectRatio[0] == '!') ? true : false;
$aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio;
if (isset($aspectRatios[$aspectRatio])) {
$aspectRatio = $aspectRatios[$aspectRatio];
}
if ($negateAspectRatio) {
$aspectRatio = 1 / $aspectRatio;
}
is_null($aspectRatio)
or is_numeric($aspectRatio)
or errorPage('Aspect ratio out of range', 404);
verbose("aspect ratio = $aspectRatio");
/**
* crop-to-fit, cf - affecting the resulting image width, height and resize options
*/
$cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false);
verbose("crop to fit = $cropToFit");
/**
* Set default background color from config file.
*/
$backgroundColor = getConfig('background_color', null);
if ($backgroundColor) {
$img->setDefaultBackgroundColor($backgroundColor);
verbose("Using default background_color = $backgroundColor");
}
/**
* bgColor - Default background color to use
*/
$bgColor = get(array('bgColor', 'bg-color', 'bgc'), null);
verbose("bgColor = $bgColor");
/**
* Do or do not resample image when resizing.
*/
$resizeStrategy = getDefined(array('no-resample'), true, false);
if ($resizeStrategy) {
$img->setCopyResizeStrategy($img::RESIZE);
verbose("Setting = Resize instead of resample");
}
/**
* fill-to-fit, ff - affecting the resulting image width, height and resize options
*/
$fillToFit = get(array('fill-to-fit', 'ff'), null);
verbose("fill-to-fit = $fillToFit");
if ($fillToFit !== null) {
if (!empty($fillToFit)) {
$bgColor = $fillToFit;
verbose("fillToFit changed bgColor to = $bgColor");
}
$fillToFit = true;
verbose("fill-to-fit (fixed) = $fillToFit");
}
/**
* no-ratio, nr, stretch - affecting the resulting image width, height and resize options
*/
$keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true);
verbose("keep ratio = $keepRatio");
/**
* crop, c - affecting the resulting image width, height and resize options
*/
$crop = get(array('crop', 'c'));
verbose("crop = $crop");
/**
* area, a - affecting the resulting image width, height and resize options
*/
$area = get(array('area', 'a'));
verbose("area = $area");
/**
* skip-original, so - skip the original image and always process a new image
*/
$useOriginal = getDefined(array('skip-original', 'so'), false, true);
$useOriginalDefault = getConfig('skip_original', false);
if ($useOriginalDefault === true) {
verbose("skip original is default ON");
$useOriginal = false;
}
verbose("use original = $useOriginal");
/**
* quality, q - set level of quality for jpeg images
*/
$quality = get(array('quality', 'q'));
$qualityDefault = getConfig('jpg_quality', null);
is_null($quality)
or ($quality > 0 and $quality <= 100)
or errorPage('Quality out of range', 404);
if (is_null($quality) && !is_null($qualityDefault)) {
$quality = $qualityDefault;
}
verbose("quality = $quality");
/**
* compress, co - what strategy to use when compressing png images
*/
$compress = get(array('compress', 'co'));
$compressDefault = getConfig('png_compression', null);
is_null($compress)
or ($compress > 0 and $compress <= 9)
or errorPage('Compress out of range', 404);
if (is_null($compress) && !is_null($compressDefault)) {
$compress = $compressDefault;
}
verbose("compress = $compress");
/**
* save-as, sa - what type of image to save
*/
$saveAs = get(array('save-as', 'sa'));
verbose("save as = $saveAs");
/**
* scale, s - Processing option, scale up or down the image prior actual resize
*/
$scale = get(array('scale', 's'));
is_null($scale)
or ($scale >= 0 and $scale <= 400)
or errorPage('Scale out of range', 404);
verbose("scale = $scale");
/**
* palette, p - Processing option, create a palette version of the image
*/
$palette = getDefined(array('palette', 'p'), true, false);
verbose("palette = $palette");
/**
* sharpen - Processing option, post filter for sharpen effect
*/
$sharpen = getDefined('sharpen', true, null);
verbose("sharpen = $sharpen");
/**
* emboss - Processing option, post filter for emboss effect
*/
$emboss = getDefined('emboss', true, null);
verbose("emboss = $emboss");
/**
* blur - Processing option, post filter for blur effect
*/
$blur = getDefined('blur', true, null);
verbose("blur = $blur");
/**
* rotateBefore - Rotate the image with an angle, before processing
*/
$rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb'));
is_null($rotateBefore)
or ($rotateBefore >= -360 and $rotateBefore <= 360)
or errorPage('RotateBefore out of range', 404);
verbose("rotateBefore = $rotateBefore");
/**
* rotateAfter - Rotate the image with an angle, before processing
*/
$rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r'));
is_null($rotateAfter)
or ($rotateAfter >= -360 and $rotateAfter <= 360)
or errorPage('RotateBefore out of range', 404);
verbose("rotateAfter = $rotateAfter");
/**
* autoRotate - Auto rotate based on EXIF information
*/
$autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false);
verbose("autoRotate = $autoRotate");
/**
* filter, f, f0-f9 - Processing option, post filter for various effects using imagefilter()
*/
$filters = array();
$filter = get(array('filter', 'f'));
if ($filter) {
$filters[] = $filter;
}
for ($i = 0; $i < 10; $i++) {
$filter = get(array("filter{$i}", "f{$i}"));
if ($filter) {
$filters[] = $filter;
}
}
verbose("filters = " . print_r($filters, 1));
/**
* json - output the image as a JSON object with details on the image.
* ascii - output the image as ASCII art.
*/
$outputFormat = getDefined('json', 'json', null);
$outputFormat = getDefined('ascii', 'ascii', $outputFormat);
verbose("outputformat = $outputFormat");
if ($outputFormat == 'ascii') {
$defaultOptions = getConfig(
'ascii-options',
array(
"characterSet" => 'two',
"scale" => 14,
"luminanceStrategy" => 3,
"customCharacterSet" => null,
)
);
$options = get('ascii');
$options = explode(',', $options);
if (isset($options[0]) && !empty($options[0])) {
$defaultOptions['characterSet'] = $options[0];
}
if (isset($options[1]) && !empty($options[1])) {
$defaultOptions['scale'] = $options[1];
}
if (isset($options[2]) && !empty($options[2])) {
$defaultOptions['luminanceStrategy'] = $options[2];
}
if (count($options) > 3) {
// Last option is custom character string
unset($options[0]);
unset($options[1]);
unset($options[2]);
$characterString = implode($options);
$defaultOptions['customCharacterSet'] = $characterString;
}
$img->setAsciiOptions($defaultOptions);
}
/**
* dpr - change to get larger image to easier support larger dpr, such as retina.
*/
$dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1);
verbose("dpr = $dpr");
/**
* convolve - image convolution as in http://php.net/manual/en/function.imageconvolution.php
*/
$convolve = get('convolve', null);
$convolutionConstant = getConfig('convolution_constant', array());
// Check if the convolve is matching an existing constant
if ($convolve && isset($convolutionConstant)) {
$img->addConvolveExpressions($convolutionConstant);
verbose("convolve constant = " . print_r($convolutionConstant, 1));
}
verbose("convolve = " . print_r($convolve, 1));
/**
* no-upscale, nu - Do not upscale smaller image to larger dimension.
*/
$upscale = getDefined(array('no-upscale', 'nu'), false, true);
verbose("upscale = $upscale");
/**
* Get details for post processing
*/
$postProcessing = getConfig('postprocessing', array(
'png_lossy' => false,
'png_lossy_cmd' => '/usr/local/bin/pngquant --force --output',
'png_filter' => false,
'png_filter_cmd' => '/usr/local/bin/optipng -q',
'png_deflate' => false,
'png_deflate_cmd' => '/usr/local/bin/pngout -q',
'jpeg_optimize' => false,
'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize',
));
/**
* lossy - Do lossy postprocessing, if available.
*/
$lossy = getDefined(array('lossy'), true, null);
verbose("lossy = $lossy");
/**
* alias - Save resulting image to another alias name.
* Password always apply, must be defined.
*/
$alias = get('alias', null);
$aliasPath = getConfig('alias_path', null);
$validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#');
$aliasTarget = null;
if ($alias && $aliasPath && $passwordMatch) {
$aliasTarget = $aliasPath . $alias;
$useCache = false;
is_writable($aliasPath)
or errorPage("Directory for alias is not writable.", 403);
preg_match($validAliasname, $alias)
or errorPage('Filename for alias contains invalid characters. Do not add extension.', 404);
} elseif ($alias) {
errorPage('Alias is not enabled in the config file or password not matching.', 403);
}
verbose("alias = $alias");
/**
* Add cache control HTTP header.
*/
$cacheControl = getConfig('cache_control', null);
if ($cacheControl) {
verbose("cacheControl = $cacheControl");
$img->addHTTPHeader("Cache-Control", $cacheControl);
}
/**
* interlace - Enable configuration for interlaced progressive JPEG images.
*/
$interlaceConfig = getConfig('interlace', null);
$interlaceValue = getValue('interlace', null);
$interlaceDefined = getDefined('interlace', true, null);
$interlace = $interlaceValue ?? $interlaceDefined ?? $interlaceConfig;
verbose("interlace (configfile) = ", $interlaceConfig);
verbose("interlace = ", $interlace);
/**
* Prepare a dummy image and use it as source image.
*/
if ($dummyImage === true) {
$dummyDir = $cache->getPathToSubdir("dummy");
$img->setSaveFolder($dummyDir)
->setSource($dummyFilename, $dummyDir)
->setOptions(
array(
'newWidth' => $newWidth,
'newHeight' => $newHeight,
'bgColor' => $bgColor,
)
)
->setJpegQuality($quality)
->setPngCompression($compress)
->createDummyImage()
->generateFilename(null, false)
->save(null, null, false);
$srcImage = $img->getTarget();
$imagePath = null;
verbose("src (updated) = $srcImage");
}
/**
* Prepare a sRGB version of the image and use it as source image.
*/
$srgbDefault = getConfig('srgb_default', false);
$srgbColorProfile = getConfig('srgb_colorprofile', __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc');
$srgb = getDefined('srgb', true, null);
if ($srgb || $srgbDefault) {
$filename = $img->convert2sRGBColorSpace(
$srcImage,
$imagePath,
$cache->getPathToSubdir("srgb"),
$srgbColorProfile,
$useCache
);
if ($filename) {
$srcImage = $img->getTarget();
$imagePath = null;
verbose("srgb conversion and saved to cache = $srcImage");
} else {
verbose("srgb not op");
}
}
/**
* Display status
*/
if ($status) {
$text = "img.php version = " . CIMAGE_VERSION . "\n";
$text .= "PHP version = " . PHP_VERSION . "\n";
$text .= "Running on: " . $_SERVER['SERVER_SOFTWARE'] . "\n";
$text .= "Allow remote images = $allowRemote\n";
$res = $cache->getStatusOfSubdir("");
$text .= "Cache $res\n";
$res = $cache->getStatusOfSubdir("remote");
$text .= "Cache remote $res\n";
$res = $cache->getStatusOfSubdir("dummy");
$text .= "Cache dummy $res\n";
$res = $cache->getStatusOfSubdir("srgb");
$text .= "Cache srgb $res\n";
$res = $cache->getStatusOfSubdir($fastTrackCache);
$text .= "Cache fasttrack $res\n";
$text .= "Alias path writable = " . is_writable($aliasPath) . "\n";
$no = extension_loaded('exif') ? null : 'NOT';
$text .= "Extension exif is $no loaded.$textEOD; exit; } /** * Log verbose details to file */ if ($verboseFile) { $img->setVerboseToFile("$cachePath/log.txt"); } /** * Hook after img.php configuration and before processing with CImage */ $hookBeforeCImage = getConfig('hook_before_CImage', null); if (is_callable($hookBeforeCImage)) { verbose("hookBeforeCImage activated"); $allConfig = $hookBeforeCImage($img, array( // Options for calculate dimensions 'newWidth' => $newWidth, 'newHeight' => $newHeight, 'aspectRatio' => $aspectRatio, 'keepRatio' => $keepRatio, 'cropToFit' => $cropToFit, 'fillToFit' => $fillToFit, 'crop' => $crop, 'area' => $area, 'upscale' => $upscale, // Pre-processing, before resizing is done 'scale' => $scale, 'rotateBefore' => $rotateBefore, 'autoRotate' => $autoRotate, // General processing options 'bgColor' => $bgColor, // Post-processing, after resizing is done 'palette' => $palette, 'filters' => $filters, 'sharpen' => $sharpen, 'emboss' => $emboss, 'blur' => $blur, 'convolve' => $convolve, 'rotateAfter' => $rotateAfter, 'interlace' => $interlace, // Output format 'outputFormat' => $outputFormat, 'dpr' => $dpr, // Other 'postProcessing' => $postProcessing, 'lossy' => $lossy, )); verbose(print_r($allConfig, 1)); extract($allConfig); } /** * Display image if verbose mode */ if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1", var_dump($info['request_header']), ""; echo "Response header (raw)
", var_dump($this->response['headerRaw']), ""; echo "Response header (parsed)
", var_dump($this->response['header']), ""; } curl_close($ch); return true; } public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : ''; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } public function getBody() { return $this->response['body']; } } class CRemoteImage { private $saveFolder = null; private $useCache = true; private $http; private $status; private $defaultMaxAge = 604800; private $url; private $fileName; private $fileJson; private $cache; public function getStatus() { return $this->status; } public function getDetails() { return $this->cache; } public function setCache($path) { $this->saveFolder = rtrim($path, "/") . "/"; return $this; } public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } public function useCache($use = true) { $this->useCache = $use; return $this; } public function setHeaderFields() { $cimageVersion = "CImage"; if (defined("CIMAGE_USER_AGENT")) { $cimageVersion = CIMAGE_USER_AGENT; } $this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } public function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['Url'] = $this->url; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileName, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } return false; } public function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } public function download($url) { $this->http = new CHttpGet(); $this->url = $url; $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } elseif ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } throw new Exception("Unknown statuscode when downloading remote image: " . $this->status); } public function loadCacheDetails() { $cacheFile = md5($this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } public function getCachedSource() { $imageExists = is_readable($this->fileName); $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileName; } if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } class CWhitelist { private $whitelist = array(); public function set($whitelist = array()) { if (!is_array($whitelist)) { throw new Exception("Whitelist is not of a supported format."); } $this->whitelist = $whitelist; return $this; } public function check($item, $whitelist = null) { if ($whitelist !== null) { $this->set($whitelist); } if (empty($item) or empty($this->whitelist)) { return false; } foreach ($this->whitelist as $regexp) { if (preg_match("#$regexp#", $item)) { return true; } } return false; } } class CAsciiArt { private $characterSet = array( 'one' => "#0XT|:,.' ", 'two' => "@%#*+=-:. ", 'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. " ); private $characters = null; private $charCount = null; private $scale = null; private $luminanceStrategy = null; public function __construct() { $this->setOptions(); } public function addCharacterSet($key, $value) { $this->characterSet[$key] = $value; return $this; } public function setOptions($options = array()) { $default = array( "characterSet" => 'two', "scale" => 14, "luminanceStrategy" => 3, "customCharacterSet" => null, ); $default = array_merge($default, $options); if (!is_null($default['customCharacterSet'])) { $this->addCharacterSet('custom', $default['customCharacterSet']); $default['characterSet'] = 'custom'; } $this->scale = $default['scale']; $this->characters = $this->characterSet[$default['characterSet']]; $this->charCount = strlen($this->characters); $this->luminanceStrategy = $default['luminanceStrategy']; return $this; } public function createFromFile($filename) { $img = imagecreatefromstring(file_get_contents($filename)); list($width, $height) = getimagesize($filename); $ascii = null; $incY = $this->scale; $incX = $this->scale / 2; for ($y = 0; $y < $height - 1; $y += $incY) { for ($x = 0; $x < $width - 1; $x += $incX) { $toX = min($x + $this->scale / 2, $width - 1); $toY = min($y + $this->scale, $height - 1); $luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY); $ascii .= $this->luminance2character($luminance); } $ascii .= PHP_EOL; } return $ascii; } public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2) { $numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1); $luminance = 0; for ($x = $x1; $x <= $x2; $x++) { for ($y = $y1; $y <= $y2; $y++) { $rgb = imagecolorat($img, $x, $y); $red = (($rgb >> 16) & 0xFF); $green = (($rgb >> 8) & 0xFF); $blue = ($rgb & 0xFF); $luminance += $this->getLuminance($red, $green, $blue); } } return $luminance / $numPixels; } public function getLuminance($red, $green, $blue) { switch ($this->luminanceStrategy) { case 1: $luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255; break; case 2: $luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255; break; case 3: $luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255; break; case 0: default: $luminance = ($red + $green + $blue) / (255 * 3); } return $luminance; } public function luminance2character($luminance) { $position = (int) round($luminance * ($this->charCount - 1)); $char = $this->characters[$position]; return $char; } } #[AllowDynamicProperties] class CImage { const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; const JPEG_QUALITY_DEFAULT = 60; private $quality; private $useQuality = false; const PNG_COMPRESSION_DEFAULT = -1; private $compress; private $useCompress = false; private $HTTPHeader = array(); private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); private $bgColor; private $saveFolder; private $image; private $imageSrc; private $pathToImage; private $fileType; private $extension; private $outputFormat = null; private $lossy = null; private $verbose = false; private $log = array(); private $palette; private $cacheFileName; private $saveAs; private $pngLossy; private $pngLossyCmd; private $pngFilter; private $pngFilterCmd; private $pngDeflate; private $pngDeflateCmd; private $jpegOptimize; private $jpegOptimizeCmd; private $width; private $height; private $newWidth; private $newWidthOrig; private $newHeight; private $newHeightOrig; private $dpr = 1; const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; public $crop; public $cropOrig; private $convolve; private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); private $fillToFit; private $scale; private $rotateBefore; private $rotateAfter; private $autoRotate; private $sharpen; private $emboss; private $blur; private $offset; private $fillWidth; private $fillHeight; private $allowRemote = false; private $remoteCache; private $remotePattern = '#^https?://#'; private $useCache = true; private $fastTrackCache = null; private $remoteHostWhitelist = null; private $verboseFileName = null; private $asciiOptions = array(); private $interlace = false; const RESIZE = 1; const RESAMPLE = 2; private $copyStrategy = NULL; public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $attr; public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } public function injectDependency($property, $object) { if (!property_exists($this, $property)) { $this->raiseError("Injecting unknown property."); } $this->$property = $object; return $this; } public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } public function useCache($use = true) { $this->useCache = $use; return $this; } public function createDummyImage($width = null, $height = null) { $this->newWidth = $this->newWidth ?: $width ?: 100; $this->newHeight = $this->newHeight ?: $height ?: 100; $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); return $this; } public function setRemoteDownload($allow, $cache, $pattern = null) { $this->allowRemote = $allow; $this->remoteCache = $cache; $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern; $this->log( "Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern ); return $this; } public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return !!$remote; } public function setRemoteHostWhitelist($whitelist = null) { $this->remoteHostWhitelist = $whitelist; $this->log( "Setting remote host whitelist to: " . (is_null($whitelist) ? "null" : print_r($whitelist, 1)) ); return $this; } public function isRemoteSourceOnWhitelist($src) { if (is_null($this->remoteHostWhitelist)) { $this->log("Remote host on whitelist not configured - allowing."); return true; } $whitelist = new CWhitelist(); $hostname = parse_url($src, PHP_URL_HOST); $allow = $whitelist->check($hostname, $this->remoteHostWhitelist); $this->log( "Remote host is on whitelist: " . ($allow ? "true" : "false") ); return $allow; } private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } private function normalizeFileExtension($extension = "") { $extension = strtolower($extension ? $extension : $this->extension ?? ""); if ($extension == 'jpeg') { $extension = 'jpg'; } return $extension; } public function downloadRemoteSource($src) { if (!$this->isRemoteSourceOnWhitelist($src)) { throw new Exception("Hostname is not on whitelist for remote sources."); } $remote = new CRemoteImage(); if (!is_writable($this->remoteCache)) { $this->log("The remote cache is not writable."); } $remote->setCache($this->remoteCache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item is in local cache: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } public function setSource($src, $dir = null) { if (!isset($src)) { $this->imageSrc = null; $this->pathToImage = null; return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $imageFolder = rtrim($dir, '/'); $this->pathToImage = $imageFolder . '/' . $this->imageSrc; return $this; } public function setTarget($src = null, $dir = null) { if (!isset($src)) { $this->cacheFileName = null; return $this; } if (isset($dir)) { $this->saveFolder = rtrim($dir, '/'); } $this->cacheFileName = $this->saveFolder . '/' . $src; $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } public function getTarget() { return $this->cacheFileName; } public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, 'area' => null, 'upscale' => self::UPSCALE_DEFAULT, 'useCache' => true, 'useOriginal' => true, 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, 'bgColor' => null, 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, 'interlace' => null, 'outputFormat' => null, 'dpr' => 1, 'lossy' => null, ); if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; if (!defined('WINDOWS2WSL')) { is_readable($file) or $this->raiseError('Image file does not exist.'); } $info = list($this->width, $this->height, $this->fileType) = getimagesize($file); if (empty($info)) { $this->fileType = false; if (function_exists("exif_imagetype")) { $this->fileType = exif_imagetype($file); if ($this->fileType === false) { if (function_exists("imagecreatefromwebp")) { $webp = imagecreatefromwebp($file); if ($webp !== false) { $this->width = imagesx($webp); $this->height = imagesy($webp); $this->fileType = IMG_WEBP; } } } } } if (!$this->fileType) { throw new Exception("Loading image details, the file doesn't seem to be a valid image."); } if ($this->verbose) { $this->log("Loading image details for: {$file}"); $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType})."); $this->log(" Image filesize: " . filesize($file) . " bytes."); $this->log(" Image mimetype: " . $this->getMimeType()); } return $this; } protected function getMimeType() { if ($this->fileType === IMG_WEBP) { return "image/webp"; } return image_type_to_mime_type($this->fileType); } public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); if ($this->newWidth && $this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } if ($this->newHeight && $this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } public function calculateNewWidthAndHeight() { $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } if ($this->keepRatio) { $this->log("Keep aspect ratio."); if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } else { $this->newWidth = $width; $this->newHeight = $height; } if ($this->cropToFit || $this->fillToFit) { $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } elseif ($this->fillToFit) { $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image as: " . $this->extension); return $this; } public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) && !$this->lossy ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } public function generateFilename($base = null, $useSubdir = true, $prefix = null) { $filename = basename($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $lossy = $this->lossy ? "_l" : null; $interlace = $this->interlace ? "_i" : null; $saveAs = $this->normalizeFileExtension(); $saveAs = $saveAs ? "_$saveAs" : null; $copyStrat = null; if ($this->copyStrategy === self::RESIZE) { $copyStrat = "_rs"; } $width = $this->newWidth ? '_' . $this->newWidth : null; $height = $this->newHeight ? '_' . $this->newHeight : null; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= "-".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $optimize = $this->jpegOptimize ? 'o' : null; $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = null; if ($useSubdir === true) { $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $subdir .= '_'; } $file = $prefix . $subdir . $filename . $width . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $compress . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . $copyStrat . $lossy . $interlace . $saveAs; return $this->setTarget($file, $base); } public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } public function load($src = null, $dir = null) { if (isset($src)) { $this->setSource($src, $dir); } $this->loadImageDetails(); if ($this->fileType === IMG_WEBP) { $this->image = imagecreatefromwebp($this->pathToImage); } else { $imageAsString = file_get_contents($this->pathToImage); $this->image = imagecreatefromstring($imageAsString); } if ($this->image === false) { throw new Exception("Could not load image."); } if ($this->verbose) { $this->log("### Image successfully loaded from file."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->colorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } public function getPngType($filename = null) { $filename = $filename ? $filename : $this->pathToImage; $pngType = ord(file_get_contents($filename, false, null, 25, 1)); if ($this->verbose) { $this->log("Checking png type of: " . $filename); $this->log($this->getPngTypeAsString($pngType)); } return $pngType; } private function getPngTypeAsString($pngType = null, $filename = null) { if ($filename || !$pngType) { $pngType = $this->getPngType($filename); } $index = imagecolortransparent($this->image); $transparent = null; if ($index != -1) { $transparent = " (transparent)"; } switch ($pngType) { case self::PNG_GREYSCALE: $text = "PNG is type 0, Greyscale$transparent"; break; case self::PNG_RGB: $text = "PNG is type 2, RGB$transparent"; break; case self::PNG_RGB_PALETTE: $text = "PNG is type 3, RGB with palette$transparent"; break; case self::PNG_GREYSCALE_ALPHA: $text = "PNG is type 4, Greyscale with alpha channel"; break; case self::PNG_RGB_ALPHA: $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)"; break; default: $text = "PNG is UNKNOWN type, is it really a PNG image?"; } return $text; } private function colorsTotal($im) { if (imageistruecolor($im)) { $this->log("Colors as true color."); $h = imagesy($im); $w = imagesx($im); $c = array(); for ($x=0; $x < $w; $x++) { for ($y=0; $y < $h; $y++) { @$c['c'.imagecolorat($im, $x, $y)]++; } } return count($c); } else { $this->log("Colors as palette."); return imagecolorstotal($im); } } public function preResize() { $this->log("### Pre-process before resizing"); if ($this->rotateBefore) { $this->log("Rotating image."); $this->rotate($this->rotateBefore, $this->bgColor) ->reCalculateDimensions(); } if ($this->autoRotate) { $this->log("Auto rotating image."); $this->rotateExif() ->reCalculateDimensions(); } if (isset($this->scale)) { $this->log("Scale by {$this->scale}%"); $newWidth = $this->width * $this->scale / 100; $newHeight = $this->height * $this->scale / 100; $img = $this->CreateImageKeepTransparency($newWidth, $newHeight); imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height); $this->image = $img; $this->width = $newWidth; $this->height = $newHeight; } return $this; } public function setCopyResizeStrategy($strategy) { $this->copyStrategy = $strategy; return $this; } public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) { if($this->copyStrategy == self::RESIZE) { $this->log("Copy by resize"); imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } else { $this->log("Copy by resample"); imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } } public function resize() { $this->log("### Starting to Resize()"); $this->log("Upscale = '$this->upscale'"); if (isset($this->offset)) { $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}"); $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']); imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']); $this->image = $img; $this->width = $this->offset['width']; $this->height = $this->offset['height']; } if ($this->crop) { $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}"); $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']); imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']); $this->image = $img; $this->width = $this->crop['width']; $this->height = $this->crop['height']; } if (!$this->upscale) { } if ($this->cropToFit) { $this->log("Resizing using strategy - Crop to fit"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight)) { $this->log("Resizing - smaller image, do not upscale."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newWidth < $this->width) { $cropX = round(($this->width/2) - ($this->newWidth/2)); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } if ($this->newHeight < $this->height) { $cropY = round(($this->height/2) - ($this->newHeight/2)); } $this->log(" cwidth: $this->cropWidth"); $this->log(" cheight: $this->cropHeight"); $this->log(" nwidth: $this->newWidth"); $this->log(" nheight: $this->newHeight"); $this->log(" width: $this->width"); $this->log(" height: $this->height"); $this->log(" posX: $posX"); $this->log(" posY: $posY"); $this->log(" cropX: $cropX"); $this->log(" cropY: $cropY"); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); } else { $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $imgPreCrop = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif ($this->fillToFit) { $this->log("Resizing using strategy - Fill to fit"); $posX = 0; $posY = 0; $ratioOrig = $this->width / $this->height; $ratioNew = $this->newWidth / $this->newHeight; if ($ratioOrig < $ratioNew) { $posX = round(($this->newWidth - $this->fillWidth) / 2); } else { $posY = round(($this->newHeight - $this->fillHeight) / 2); } if (!$this->upscale && ($this->width < $this->newWidth && $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) { $this->log("Resizing, new height and/or width"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); if (!$this->keepRatio) { $this->log("Resizing - stretch to fit selected."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width && $this->newHeight > $this->height) { $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); } elseif ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } elseif ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } return $this; } public function postResize() { $this->log("### Post-process after resizing"); if ($this->rotateAfter) { $this->log("Rotating image."); $this->rotate($this->rotateAfter, $this->bgColor); } if (isset($this->filters) && is_array($this->filters)) { foreach ($this->filters as $filter) { $this->log("Applying filter {$filter['type']}."); switch ($filter['argc']) { case 0: imagefilter($this->image, $filter['type']); break; case 1: imagefilter($this->image, $filter['type'], $filter['arg1']); break; case 2: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']); break; case 3: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']); break; case 4: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']); break; } } } if ($this->palette) { $this->log("Converting to palette image."); $this->trueColorToPalette(); } if ($this->blur) { $this->log("Blur."); $this->blurImage(); } if ($this->emboss) { $this->log("Emboss."); $this->embossImage(); } if ($this->sharpen) { $this->log("Sharpen."); $this->sharpenImage(); } if ($this->convolve) { $this->imageConvolution(); } return $this; } public function rotate($angle, $bgColor) { $this->log("Rotate image " . $angle . " degrees with filler color."); $color = $this->getBackgroundColor(); $this->image = imagerotate($this->image, $angle, $color); $this->width = imagesx($this->image); $this->height = imagesy($this->image); $this->log("New image dimension width x height: " . $this->width . " x " . $this->height); return $this; } public function rotateExif() { if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) { $this->log("Autorotate ignored, EXIF not supported by this filetype."); return $this; } $exif = exif_read_data($this->pathToImage); if (!empty($exif['Orientation'])) { switch ($exif['Orientation']) { case 3: $this->log("Autorotate 180."); $this->rotate(180, $this->bgColor); break; case 6: $this->log("Autorotate -90."); $this->rotate(-90, $this->bgColor); break; case 8: $this->log("Autorotate 90."); $this->rotate(90, $this->bgColor); break; default: $this->log("Autorotate ignored, unknown value as orientation."); } } else { $this->log("Autorotate ignored, no orientation in EXIF."); } return $this; } public function trueColorToPalette() { $img = imagecreatetruecolor($this->width, $this->height); $bga = imagecolorallocatealpha($img, 0, 0, 0, 127); imagecolortransparent($img, $bga); imagefill($img, 0, 0, $bga); imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height); imagetruecolortopalette($img, false, 255); imagesavealpha($img, true); if (imageistruecolor($this->image)) { $this->log("Matching colors with true color image."); imagecolormatch($this->image, $img); } $this->image = $img; } public function sharpenImage() { $this->imageConvolution('sharpen'); return $this; } public function embossImage() { $this->imageConvolution('emboss'); return $this; } public function blurImage() { $this->imageConvolution('blur'); return $this; } public function createConvolveArguments($expression) { if (isset($this->convolves[$expression])) { $expression = $this->convolves[$expression]; } $part = explode(',', $expression); $this->log("Creating convolution expressen: $expression"); if (count($part) != 11) { throw new Exception( "Missmatch in argument convolve. Expected comma-separated string with 11 float values. Got $expression." ); } array_walk($part, function ($item, $key) { if (!is_numeric($item)) { throw new Exception("Argument to convolve expression should be float but is not."); } }); return array( array( array($part[0], $part[1], $part[2]), array($part[3], $part[4], $part[5]), array($part[6], $part[7], $part[8]), ), $part[9], $part[10], ); } public function addConvolveExpressions($options) { $this->convolves = array_merge($this->convolves, $options); return $this; } public function imageConvolution($options = null) { $options = $options ? $options : $this->convolve; $this->log("Convolution with '$options'"); $options = explode(":", $options); foreach ($options as $option) { list($matrix, $divisor, $offset) = $this->createConvolveArguments($option); imageconvolution($this->image, $matrix, $divisor, $offset); } return $this; } public function setDefaultBackgroundColor($color) { $this->log("Setting default background color to '$color'."); if (!(strlen($color) == 6 || strlen($color) == 8)) { throw new Exception( "Background color needs a hex value of 6 or 8 digits. 000000-FFFFFF or 00000000-FFFFFF7F. Current value was: '$color'." ); } $red = hexdec(substr($color, 0, 2)); $green = hexdec(substr($color, 2, 2)); $blue = hexdec(substr($color, 4, 2)); $alpha = (strlen($color) == 8) ? hexdec(substr($color, 6, 2)) : null; if (($red < 0 || $red > 255) || ($green < 0 || $green > 255) || ($blue < 0 || $blue > 255) || ($alpha < 0 || $alpha > 127) ) { throw new Exception( "Background color out of range. Red, green blue should be 00-FF and alpha should be 00-7F. Current value was: '$color'." ); } $this->bgColor = strtolower($color); $this->bgColorDefault = array( 'red' => $red, 'green' => $green, 'blue' => $blue, 'alpha' => $alpha ); return $this; } private function getBackgroundColor($img = null) { $img = isset($img) ? $img : $this->image; if ($this->bgColorDefault) { $red = $this->bgColorDefault['red']; $green = $this->bgColorDefault['green']; $blue = $this->bgColorDefault['blue']; $alpha = $this->bgColorDefault['alpha']; if ($alpha) { $color = imagecolorallocatealpha($img, $red, $green, $blue, $alpha); } else { $color = imagecolorallocate($img, $red, $green, $blue); } return $color; } else { return 0; } } private function createImageKeepTransparency($width, $height) { $this->log("Creating a new working image width={$width}px, height={$height}px."); $img = imagecreatetruecolor($width, $height); imagealphablending($img, false); imagesavealpha($img, true); $index = $this->image ? imagecolortransparent($this->image) : -1; if ($index != -1) { imagealphablending($img, true); $transparent = imagecolorsforindex($this->image, $index); $color = imagecolorallocatealpha($img, $transparent['red'], $transparent['green'], $transparent['blue'], $transparent['alpha']); imagefill($img, 0, 0, $color); $index = imagecolortransparent($img, $color); $this->Log("Detected transparent color = " . implode(", ", $transparent) . " at index = $index"); } elseif ($this->bgColorDefault) { $color = $this->getBackgroundColor($img); imagefill($img, 0, 0, $color); $this->Log("Filling image with background color."); } return $img; } public function setPostProcessingOptions($options) { if (isset($options['jpeg_optimize']) && $options['jpeg_optimize']) { $this->jpegOptimizeCmd = $options['jpeg_optimize_cmd']; } else { $this->jpegOptimizeCmd = null; } if (array_key_exists("png_lossy", $options) && $options['png_lossy'] !== false) { $this->pngLossy = $options['png_lossy']; $this->pngLossyCmd = $options['png_lossy_cmd']; } else { $this->pngLossyCmd = null; } if (isset($options['png_filter']) && $options['png_filter']) { $this->pngFilterCmd = $options['png_filter_cmd']; } else { $this->pngFilterCmd = null; } if (isset($options['png_deflate']) && $options['png_deflate']) { $this->pngDeflateCmd = $options['png_deflate_cmd']; } else { $this->pngDeflateCmd = null; } return $this; } protected function getTargetImageExtension() { if (isset($this->extension)) { return strtolower($this->extension); } elseif ($this->fileType === IMG_WEBP) { return "webp"; } return substr(image_type_to_extension($this->fileType), 1); } public function save($src = null, $base = null, $overwrite = true) { if (isset($src)) { $this->setTarget($src, $base); } if ($overwrite === false && is_file($this->cacheFileName)) { $this->Log("Not overwriting file since its already exists and \$overwrite if false."); return; } if (!defined("WINDOWS2WSL")) { is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); } $type = $this->getTargetImageExtension(); $this->Log("Saving image as " . $type); switch($type) { case 'jpeg': case 'jpg': if ($this->interlace) { $this->Log("Set JPEG image to be interlaced."); $res = imageinterlace($this->image, true); } $this->Log("Saving image as JPEG to cache using quality = {$this->quality}."); imagejpeg($this->image, $this->cacheFileName, $this->quality); if ($this->jpegOptimizeCmd) { if ($this->verbose) { clearstatcache(); $this->log("Filesize before optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->jpegOptimizeCmd . " -outfile $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->log($cmd); $this->log($res); } break; case 'gif': $this->Log("Saving image as GIF to cache."); imagegif($this->image, $this->cacheFileName); break; case 'webp': $this->Log("Saving image as WEBP to cache using quality = {$this->quality}."); imagewebp($this->image, $this->cacheFileName, $this->quality); break; case 'png': default: $this->Log("Saving image as PNG to cache using compression = {$this->compress}."); imagealphablending($this->image, false); imagesavealpha($this->image, true); imagepng($this->image, $this->cacheFileName, $this->compress); $lossyEnabled = $this->pngLossy === true; $lossySoftEnabled = $this->pngLossy === null; $lossyActiveEnabled = $this->lossy === true; if ($lossyEnabled || ($lossySoftEnabled && $lossyActiveEnabled)) { if ($this->verbose) { clearstatcache(); $this->log("Lossy enabled: $lossyEnabled"); $this->log("Lossy soft enabled: $lossySoftEnabled"); $this->Log("Filesize before lossy optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngLossyCmd . " $this->cacheFileName $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } if ($this->pngFilterCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before filter optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngFilterCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } if ($this->pngDeflateCmd) { if ($this->verbose) { clearstatcache(); $this->Log("Filesize before deflate optimize: " . filesize($this->cacheFileName) . " bytes."); } $res = array(); $cmd = $this->pngDeflateCmd . " $this->cacheFileName"; exec($cmd, $res); $this->Log($cmd); $this->Log($res); } break; } if ($this->verbose) { clearstatcache(); $this->log("Saved image to cache."); $this->log(" Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->ColorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index > 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } public function convert2sRGBColorSpace($src, $dir, $cache, $iccFile, $useCache = true) { if ($this->verbose) { $this->log("# Converting image to sRGB colorspace."); } if (!class_exists("Imagick")) { $this->log(" Ignoring since Imagemagick is not installed."); return false; } $this->setSaveFolder($cache) ->setSource($src, $dir) ->generateFilename(null, false, 'srgb_'); if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { $this->log(" Using cached version: " . $this->cacheFileName); return $this->cacheFileName; } } if (is_writable($this->saveFolder)) { $image = new Imagick($this->pathToImage); $colorspace = $image->getImageColorspace(); $this->log(" Current colorspace: " . $colorspace); $profiles = $image->getImageProfiles('*', false); $hasICCProfile = (array_search('icc', $profiles) !== false); $this->log(" Has ICC color profile: " . ($hasICCProfile ? "YES" : "NO")); if ($colorspace != Imagick::COLORSPACE_SRGB || $hasICCProfile) { $this->log(" Converting to sRGB."); $sRGBicc = file_get_contents($iccFile); $image->profileImage('icc', $sRGBicc); $image->transformImageColorspace(Imagick::COLORSPACE_SRGB); $image->writeImage($this->cacheFileName); return $this->cacheFileName; } } return false; } public function linkToCacheFile($alias) { if ($alias === null) { $this->log("Ignore creating alias."); return $this; } if (is_readable($alias)) { unlink($alias); } $res = link($this->cacheFileName, $alias); if ($res) { $this->log("Created an alias as: $alias"); } else { $this->log("Failed to create the alias: $alias"); } return $this; } public function addHTTPHeader($type, $value) { $this->HTTPHeader[$type] = $value; } public function output($file = null, $format = null) { if (is_null($file)) { $file = $this->cacheFileName; } if (is_null($format)) { $format = $this->outputFormat; } $this->log("### Output"); $this->log("Output format is: $format"); if (!$this->verbose && $format == 'json') { header('Content-type: application/json'); echo $this->json($file); exit; } elseif ($format == 'ascii') { header('Content-type: text/plain'); echo $this->ascii($file); exit; } $this->log("Outputting image: $file"); clearstatcache(); $lastModified = filemtime($file); $lastModifiedFormat = "D, d M Y H:i:s"; $gmdate = gmdate($lastModifiedFormat, $lastModified); if (!$this->verbose) { $header = "Last-Modified: $gmdate GMT"; header($header); $this->fastTrackCache->addHeader($header); $this->fastTrackCache->setLastModified($lastModified); } foreach ($this->HTTPHeader as $key => $val) { $header = "$key: $val"; header($header); $this->fastTrackCache->addHeader($header); } if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModified) { if ($this->verbose) { $this->log("304 not modified"); $this->verboseOutput(); exit; } header("HTTP/1.0 304 Not Modified"); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 304"); } } else { $this->loadImageDetails($file); $mime = $this->getMimeType(); $size = filesize($file); if ($this->verbose) { $this->log("Last-Modified: " . $gmdate . " GMT"); $this->log("Content-type: " . $mime); $this->log("Content-length: " . $size); $this->verboseOutput(); if (is_null($this->verboseFileName)) { exit; } } $header = "Content-type: $mime"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $header = "Content-length: $size"; header($header); $this->fastTrackCache->addHeaderOnOutput($header); $this->fastTrackCache->setSource($file); $this->fastTrackCache->writeToCache(); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 200"); } readfile($file); } exit; } public function json($file = null) { $file = $file ? $file : $this->cacheFileName; $details = array(); clearstatcache(); $details['src'] = $this->imageSrc; $lastModified = filemtime($this->pathToImage); $details['srcGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $details['cache'] = basename($this->cacheFileName ?? ""); $lastModified = filemtime($this->cacheFileName ?? ""); $details['cacheGmdate'] = gmdate("D, d M Y H:i:s", $lastModified); $this->load($file); $details['filename'] = basename($file ?? ""); $details['mimeType'] = $this->getMimeType($this->fileType); $details['width'] = $this->width; $details['height'] = $this->height; $details['aspectRatio'] = round($this->width / $this->height, 3); $details['size'] = filesize($file ?? ""); $details['colors'] = $this->colorsTotal($this->image); $details['includedFiles'] = count(get_included_files()); $details['memoryPeek'] = round(memory_get_peak_usage()/1024/1024, 3) . " MB" ; $details['memoryCurrent'] = round(memory_get_usage()/1024/1024, 3) . " MB"; $details['memoryLimit'] = ini_get('memory_limit'); if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { $details['loadTime'] = (string) round((microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']), 3) . "s"; } if ($details['mimeType'] == 'image/png') { $details['pngType'] = $this->getPngTypeAsString(null, $file); } $options = null; if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; } return json_encode($details, $options); } public function setAsciiOptions($options = array()) { $this->asciiOptions = $options; } public function ascii($file = null) { $file = $file ? $file : $this->cacheFileName; $asciiArt = new CAsciiArt(); $asciiArt->setOptions($this->asciiOptions); return $asciiArt->createFromFile($file); } public function log($message) { if ($this->verbose) { $this->log[] = $message; } return $this; } public function setVerboseToFile($fileName) { $this->log("Setting verbose output to file."); $this->verboseFileName = $fileName; } private function verboseOutput() { $log = null; $this->log("### Summary of verbose log"); $this->log("As JSON: \n" . $this->json()); $this->log("Memory peak: " . round(memory_get_peak_usage() /1024/1024) . "M"); $this->log("Memory limit: " . ini_get('memory_limit')); $included = get_included_files(); $this->log("Included files: " . count($included)); foreach ($this->log as $val) { if (is_array($val)) { foreach ($val as $val1) { $log .= htmlentities($val1) . '
{$log}
EOD;
} } private function raiseError($message) { throw new Exception($message); } } class CCache { private $path; public function setDir($path) { if (!is_dir($path)) { throw new Exception("Cachedir is not a directory."); } $this->path = $path; return $this; } public function getPathToSubdir($subdir, $create = true) { $path = realpath($this->path . "/" . $subdir); if (is_dir($path)) { return $path; } if ($create && defined('WINDOWS2WSL')) { $path = $this->path . "/" . $subdir; if (mkdir($path)) { return realpath($path); } } if ($create && is_writable($this->path)) { $path = $this->path . "/" . $subdir; if (mkdir($path)) { return realpath($path); } } return false; } public function getStatusOfSubdir($subdir) { $path = realpath($this->path . "/" . $subdir); $exists = is_dir($path); $res = $exists ? "exists" : "does not exist"; if ($exists) { $res .= is_writable($path) ? ", writable" : ", not writable"; } return $res; } public function removeSubdir($subdir) { $path = realpath($this->path . "/" . $subdir); if (is_dir($path)) { return rmdir($path); } return null; } } class CFastTrackCache { private $enabled = false; private $path; private $filename; private $container; public function enable($enabled) { $this->enabled = $enabled; return $this; } public function setCacheDir($path) { if (!is_dir($path)) { throw new Exception("Cachedir is not a directory."); } $this->path = rtrim($path, "/"); return $this; } public function setFilename($clear) { $query = $_GET; foreach ($clear as $value) { unset($query[$value]); } arsort($query); $queryAsString = http_build_query($query); $this->filename = md5($queryAsString); if (CIMAGE_DEBUG) { $this->container["query-string"] = $queryAsString; } return $this->filename; } public function addHeader($header) { $this->container["header"][] = $header; return $this; } public function addHeaderOnOutput($header) { $this->container["header-output"][] = $header; return $this; } public function setSource($source) { $this->container["source"] = $source; return $this; } public function setLastModified($lastModified) { $this->container["last-modified"] = $lastModified; return $this; } public function getFilename() { return $this->path . "/" . $this->filename; } public function writeToCache() { if (!$this->enabled) { return false; } if (is_dir($this->path) && is_writable($this->path)) { $filename = $this->getFilename(); return file_put_contents($filename, json_encode($this->container)) !== false; } return false; } public function output() { $filename = $this->getFilename(); if (!is_readable($filename)) { return; } $item = json_decode(file_get_contents($filename), true); if (!is_readable($item["source"])) { return; } foreach ($item["header"] as $value) { header($value); } if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && strtotime($_SERVER["HTTP_IF_MODIFIED_SINCE"]) == $item["last-modified"]) { header("HTTP/1.0 304 Not Modified"); if (CIMAGE_DEBUG) { trace(__CLASS__ . " 304"); } exit; } foreach ($item["header-output"] as $value) { header($value); } if (CIMAGE_DEBUG) { trace(__CLASS__ . " 200"); } readfile($item["source"]); exit; } } set_exception_handler(function ($exception) { errorPage( "img.php: Uncaught exception:
" . $exception->getMessage() . "
" . $exception->getTraceAsString() . "", 500 ); }); $configFile = __DIR__.'/'.basename(__FILE__, '.php').'_config.php'; if (is_file($configFile)) { $config = require $configFile; } elseif (!isset($config)) { $config = array(); } if (!defined("CIMAGE_DEBUG")) { define("CIMAGE_DEBUG", false); } if (!defined("CIMAGE_BUNDLE")) { if (!isset($config["autoloader"])) { die("CImage: Missing autoloader."); } require $config["autoloader"]; } $verbose = getDefined(array('verbose', 'v'), true, false); $verboseFile = getDefined('vf', true, false); verbose("img.php version = " . CIMAGE_VERSION); $status = getDefined('status', true, false); $mode = getConfig('mode', 'production'); set_time_limit(20); ini_set('gd.jpeg_ignore_warning', 1); if (!extension_loaded('gd')) { errorPage("Extension gd is not loaded.", 500); } if ($mode == 'strict') { error_reporting(0); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; $status = false; $verboseFile = false; } elseif ($mode == 'production') { error_reporting(-1); ini_set('display_errors', 0); ini_set('log_errors', 1); $verbose = false; $status = false; $verboseFile = false; } elseif ($mode == 'development') { error_reporting(-1); ini_set('display_errors', 1); ini_set('log_errors', 0); $verboseFile = false; } elseif ($mode == 'test') { error_reporting(-1); ini_set('display_errors', 1); ini_set('log_errors', 0); } else { errorPage("Unknown mode: $mode", 500); } verbose("mode = $mode"); verbose("error log = " . ini_get('error_log')); $defaultTimezone = getConfig('default_timezone', null); if ($defaultTimezone) { date_default_timezone_set($defaultTimezone); } elseif (!ini_get('default_timezone')) { date_default_timezone_set('UTC'); } $pwdConfig = getConfig('password', false); $pwdAlways = getConfig('password_always', false); $pwdType = getConfig('password_type', 'text'); $pwd = get(array('password', 'pwd'), null); $passwordMatch = null; if ($pwd) { switch ($pwdType) { case 'md5': $passwordMatch = ($pwdConfig === md5($pwd)); break; case 'hash': $passwordMatch = password_verify($pwd, $pwdConfig); break; case 'text': $passwordMatch = ($pwdConfig === $pwd); break; default: $passwordMatch = false; } } if ($pwdAlways && $passwordMatch !== true) { errorPage("Password required and does not match or exists.", 403); } verbose("password match = $passwordMatch"); $allowHotlinking = getConfig('allow_hotlinking', true); $hotlinkingWhitelist = getConfig('hotlinking_whitelist', array()); $serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $refererHost = parse_url($referer ?? "", PHP_URL_HOST); if (!$allowHotlinking) { if ($passwordMatch) { ; verbose("Hotlinking since passwordmatch"); } elseif ($passwordMatch === false) { errorPage("Hotlinking/leeching not allowed when password missmatch.", 403); } elseif (!$referer) { errorPage("Hotlinking/leeching not allowed and referer is missing.", 403); } elseif (strcmp($serverName, $refererHost) == 0) { ; verbose("Hotlinking disallowed but serverName matches refererHost."); } elseif (!empty($hotlinkingWhitelist)) { $whitelist = new CWhitelist(); $allowedByWhitelist = $whitelist->check($refererHost, $hotlinkingWhitelist); if ($allowedByWhitelist) { verbose("Hotlinking/leeching allowed by whitelist."); } else { errorPage("Hotlinking/leeching not allowed by whitelist. Referer: $referer.", 403); } } else { errorPage("Hotlinking/leeching not allowed.", 403); } } verbose("allow_hotlinking = $allowHotlinking"); verbose("referer = $referer"); verbose("referer host = $refererHost"); $CImage = getConfig('CImage', 'CImage'); $img = new $CImage(); $img->setVerbose($verbose || $verboseFile); $CCache = getConfig('CCache', 'CCache'); $cachePath = getConfig('cache_path', __DIR__ . '/../cache/'); $cache = new $CCache(); $cache->setDir($cachePath); $useCache = getDefined(array('no-cache', 'nc'), false, true); verbose("use cache = $useCache"); $fastTrackCache = "fasttrack"; $allowFastTrackCache = getConfig('fast_track_allow', false); $CFastTrackCache = getConfig('CFastTrackCache', 'CFastTrackCache'); $ftc = new $CFastTrackCache(); $ftc->setCacheDir($cache->getPathToSubdir($fastTrackCache)) ->enable($allowFastTrackCache) ->setFilename(array('no-cache', 'nc')); $img->injectDependency("fastTrackCache", $ftc); if ($useCache && $allowFastTrackCache) { if (CIMAGE_DEBUG) { trace("img.php fast track cache enabled and used"); } $ftc->output(); } $allowRemote = getConfig('remote_allow', false); if ($allowRemote && $passwordMatch !== false) { $cacheRemote = $cache->getPathToSubdir("remote"); $pattern = getConfig('remote_pattern', null); $img->setRemoteDownload($allowRemote, $cacheRemote, $pattern); $whitelist = getConfig('remote_whitelist', null); $img->setRemoteHostWhitelist($whitelist); } $shortcut = get(array('shortcut', 'sc'), null); $shortcutConfig = getConfig('shortcut', array( 'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen", )); verbose("shortcut = $shortcut"); if (isset($shortcut) && isset($shortcutConfig[$shortcut])) { parse_str($shortcutConfig[$shortcut], $get); verbose("shortcut-constant = {$shortcutConfig[$shortcut]}"); $_GET = array_merge($_GET, $get); } $srcImage = urldecode(get('src', "")) or errorPage('Must set src-attribute.', 404); $srcAltImage = urldecode(get('src-alt', "")); $srcAltConfig = getConfig('src_alt', null); if (empty($srcAltImage)) { $srcAltImage = $srcAltConfig; } $imagePath = getConfig('image_path', __DIR__ . '/img/'); $imagePathConstraint = getConfig('image_path_constraint', true); $validFilename = getConfig('valid_filename', '#^[a-z0-9A-Z-/_ \.:]+$#'); $remoteSource = false; $dummyEnabled = getConfig('dummy_enabled', true); $dummyFilename = getConfig('dummy_filename', 'dummy'); $dummyImage = false; preg_match($validFilename, $srcImage) or errorPage('Source filename contains invalid characters.', 404); if ($dummyEnabled && $srcImage === $dummyFilename) { $dummyImage = true; } elseif ($allowRemote && $img->isRemoteSource($srcImage)) { $remoteSource = true; } else { $pathToImage = realpath($imagePath . $srcImage); if (!is_file($pathToImage) && !empty($srcAltImage)) { $srcImage = $srcAltImage; $pathToImage = realpath($imagePath . $srcImage); preg_match($validFilename, $srcImage) or errorPage('Source (alt) filename contains invalid characters.', 404); if ($dummyEnabled && $srcImage === $dummyFilename) { $dummyImage = true; } } if (!$dummyImage) { is_file($pathToImage) or errorPage( 'Source image is not a valid file, check the filename and that a matching file exists on the filesystem.', 404 ); } } if ($imagePathConstraint && !$dummyImage && !$remoteSource) { $imageDir = realpath($imagePath); substr_compare($imageDir, $pathToImage, 0, strlen($imageDir)) == 0 or errorPage( 'Security constraint: Source image is not below the directory "image_path" as specified in the config file img_config.php.', 404 ); } verbose("src = $srcImage"); $sizeConstant = getConfig('size_constant', function () { $sizes = array( 'w1' => 613, 'w2' => 630, ); $gridColumnWidth = 30; $gridGutterWidth = 10; $gridColumns = 24; for ($i = 1; $i <= $gridColumns; $i++) { $sizes['c' . $i] = ($gridColumnWidth + $gridGutterWidth) * $i - $gridGutterWidth; } return $sizes; }); $sizes = call_user_func($sizeConstant); $newWidth = get(array('width', 'w')); $maxWidth = getConfig('max_width', 2000); if (isset($sizes[$newWidth])) { $newWidth = $sizes[$newWidth]; } if ($newWidth && $newWidth[strlen($newWidth)-1] == '%') { is_numeric(substr($newWidth, 0, -1)) or errorPage('Width % not numeric.', 404); } else { is_null($newWidth) or ($newWidth > 10 && $newWidth <= $maxWidth) or errorPage('Width out of range.', 404); } verbose("new width = $newWidth"); $newHeight = get(array('height', 'h')); $maxHeight = getConfig('max_height', 2000); if (isset($sizes[$newHeight])) { $newHeight = $sizes[$newHeight]; } if ($newHeight && $newHeight[strlen($newHeight)-1] == '%') { is_numeric(substr($newHeight, 0, -1)) or errorPage('Height % out of range.', 404); } else { is_null($newHeight) or ($newHeight > 10 && $newHeight <= $maxHeight) or errorPage('Height out of range.', 404); } verbose("new height = $newHeight"); $aspectRatio = get(array('aspect-ratio', 'ar')); $aspectRatioConstant = getConfig('aspect_ratio_constant', function () { return array( '3:1' => 3/1, '3:2' => 3/2, '4:3' => 4/3, '8:5' => 8/5, '16:10' => 16/10, '16:9' => 16/9, 'golden' => 1.618, ); }); $aspectRatios = call_user_func($aspectRatioConstant); $negateAspectRatio = ($aspectRatio && $aspectRatio[0] == '!') ? true : false; $aspectRatio = $negateAspectRatio ? substr($aspectRatio, 1) : $aspectRatio; if (isset($aspectRatios[$aspectRatio])) { $aspectRatio = $aspectRatios[$aspectRatio]; } if ($negateAspectRatio) { $aspectRatio = 1 / $aspectRatio; } is_null($aspectRatio) or is_numeric($aspectRatio) or errorPage('Aspect ratio out of range', 404); verbose("aspect ratio = $aspectRatio"); $cropToFit = getDefined(array('crop-to-fit', 'cf'), true, false); verbose("crop to fit = $cropToFit"); $backgroundColor = getConfig('background_color', null); if ($backgroundColor) { $img->setDefaultBackgroundColor($backgroundColor); verbose("Using default background_color = $backgroundColor"); } $bgColor = get(array('bgColor', 'bg-color', 'bgc'), null); verbose("bgColor = $bgColor"); $resizeStrategy = getDefined(array('no-resample'), true, false); if ($resizeStrategy) { $img->setCopyResizeStrategy($img::RESIZE); verbose("Setting = Resize instead of resample"); } $fillToFit = get(array('fill-to-fit', 'ff'), null); verbose("fill-to-fit = $fillToFit"); if ($fillToFit !== null) { if (!empty($fillToFit)) { $bgColor = $fillToFit; verbose("fillToFit changed bgColor to = $bgColor"); } $fillToFit = true; verbose("fill-to-fit (fixed) = $fillToFit"); } $keepRatio = getDefined(array('no-ratio', 'nr', 'stretch'), false, true); verbose("keep ratio = $keepRatio"); $crop = get(array('crop', 'c')); verbose("crop = $crop"); $area = get(array('area', 'a')); verbose("area = $area"); $useOriginal = getDefined(array('skip-original', 'so'), false, true); $useOriginalDefault = getConfig('skip_original', false); if ($useOriginalDefault === true) { verbose("skip original is default ON"); $useOriginal = false; } verbose("use original = $useOriginal"); $quality = get(array('quality', 'q')); $qualityDefault = getConfig('jpg_quality', null); is_null($quality) or ($quality > 0 and $quality <= 100) or errorPage('Quality out of range', 404); if (is_null($quality) && !is_null($qualityDefault)) { $quality = $qualityDefault; } verbose("quality = $quality"); $compress = get(array('compress', 'co')); $compressDefault = getConfig('png_compression', null); is_null($compress) or ($compress > 0 and $compress <= 9) or errorPage('Compress out of range', 404); if (is_null($compress) && !is_null($compressDefault)) { $compress = $compressDefault; } verbose("compress = $compress"); $saveAs = get(array('save-as', 'sa')); verbose("save as = $saveAs"); $scale = get(array('scale', 's')); is_null($scale) or ($scale >= 0 and $scale <= 400) or errorPage('Scale out of range', 404); verbose("scale = $scale"); $palette = getDefined(array('palette', 'p'), true, false); verbose("palette = $palette"); $sharpen = getDefined('sharpen', true, null); verbose("sharpen = $sharpen"); $emboss = getDefined('emboss', true, null); verbose("emboss = $emboss"); $blur = getDefined('blur', true, null); verbose("blur = $blur"); $rotateBefore = get(array('rotateBefore', 'rotate-before', 'rb')); is_null($rotateBefore) or ($rotateBefore >= -360 and $rotateBefore <= 360) or errorPage('RotateBefore out of range', 404); verbose("rotateBefore = $rotateBefore"); $rotateAfter = get(array('rotateAfter', 'rotate-after', 'ra', 'rotate', 'r')); is_null($rotateAfter) or ($rotateAfter >= -360 and $rotateAfter <= 360) or errorPage('RotateBefore out of range', 404); verbose("rotateAfter = $rotateAfter"); $autoRotate = getDefined(array('autoRotate', 'auto-rotate', 'aro'), true, false); verbose("autoRotate = $autoRotate"); $filters = array(); $filter = get(array('filter', 'f')); if ($filter) { $filters[] = $filter; } for ($i = 0; $i < 10; $i++) { $filter = get(array("filter{$i}", "f{$i}")); if ($filter) { $filters[] = $filter; } } verbose("filters = " . print_r($filters, 1)); $outputFormat = getDefined('json', 'json', null); $outputFormat = getDefined('ascii', 'ascii', $outputFormat); verbose("outputformat = $outputFormat"); if ($outputFormat == 'ascii') { $defaultOptions = getConfig( 'ascii-options', array( "characterSet" => 'two', "scale" => 14, "luminanceStrategy" => 3, "customCharacterSet" => null, ) ); $options = get('ascii'); $options = explode(',', $options); if (isset($options[0]) && !empty($options[0])) { $defaultOptions['characterSet'] = $options[0]; } if (isset($options[1]) && !empty($options[1])) { $defaultOptions['scale'] = $options[1]; } if (isset($options[2]) && !empty($options[2])) { $defaultOptions['luminanceStrategy'] = $options[2]; } if (count($options) > 3) { unset($options[0]); unset($options[1]); unset($options[2]); $characterString = implode($options); $defaultOptions['customCharacterSet'] = $characterString; } $img->setAsciiOptions($defaultOptions); } $dpr = get(array('ppi', 'dpr', 'device-pixel-ratio'), 1); verbose("dpr = $dpr"); $convolve = get('convolve', null); $convolutionConstant = getConfig('convolution_constant', array()); if ($convolve && isset($convolutionConstant)) { $img->addConvolveExpressions($convolutionConstant); verbose("convolve constant = " . print_r($convolutionConstant, 1)); } verbose("convolve = " . print_r($convolve, 1)); $upscale = getDefined(array('no-upscale', 'nu'), false, true); verbose("upscale = $upscale"); $postProcessing = getConfig('postprocessing', array( 'png_lossy' => false, 'png_lossy_cmd' => '/usr/local/bin/pngquant --force --output', 'png_filter' => false, 'png_filter_cmd' => '/usr/local/bin/optipng -q', 'png_deflate' => false, 'png_deflate_cmd' => '/usr/local/bin/pngout -q', 'jpeg_optimize' => false, 'jpeg_optimize_cmd' => '/usr/local/bin/jpegtran -copy none -optimize', )); $lossy = getDefined(array('lossy'), true, null); verbose("lossy = $lossy"); $alias = get('alias', null); $aliasPath = getConfig('alias_path', null); $validAliasname = getConfig('valid_aliasname', '#^[a-z0-9A-Z-_]+$#'); $aliasTarget = null; if ($alias && $aliasPath && $passwordMatch) { $aliasTarget = $aliasPath . $alias; $useCache = false; is_writable($aliasPath) or errorPage("Directory for alias is not writable.", 403); preg_match($validAliasname, $alias) or errorPage('Filename for alias contains invalid characters. Do not add extension.', 404); } elseif ($alias) { errorPage('Alias is not enabled in the config file or password not matching.', 403); } verbose("alias = $alias"); $cacheControl = getConfig('cache_control', null); if ($cacheControl) { verbose("cacheControl = $cacheControl"); $img->addHTTPHeader("Cache-Control", $cacheControl); } $interlaceConfig = getConfig('interlace', null); $interlaceValue = getValue('interlace', null); $interlaceDefined = getDefined('interlace', true, null); $interlace = $interlaceValue ?? $interlaceDefined ?? $interlaceConfig; verbose("interlace (configfile) = ", $interlaceConfig); verbose("interlace = ", $interlace); if ($dummyImage === true) { $dummyDir = $cache->getPathToSubdir("dummy"); $img->setSaveFolder($dummyDir) ->setSource($dummyFilename, $dummyDir) ->setOptions( array( 'newWidth' => $newWidth, 'newHeight' => $newHeight, 'bgColor' => $bgColor, ) ) ->setJpegQuality($quality) ->setPngCompression($compress) ->createDummyImage() ->generateFilename(null, false) ->save(null, null, false); $srcImage = $img->getTarget(); $imagePath = null; verbose("src (updated) = $srcImage"); } $srgbDefault = getConfig('srgb_default', false); $srgbColorProfile = getConfig('srgb_colorprofile', __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc'); $srgb = getDefined('srgb', true, null); if ($srgb || $srgbDefault) { $filename = $img->convert2sRGBColorSpace( $srcImage, $imagePath, $cache->getPathToSubdir("srgb"), $srgbColorProfile, $useCache ); if ($filename) { $srcImage = $img->getTarget(); $imagePath = null; verbose("srgb conversion and saved to cache = $srcImage"); } else { verbose("srgb not op"); } } if ($status) { $text = "img.php version = " . CIMAGE_VERSION . "\n"; $text .= "PHP version = " . PHP_VERSION . "\n"; $text .= "Running on: " . $_SERVER['SERVER_SOFTWARE'] . "\n"; $text .= "Allow remote images = $allowRemote\n"; $res = $cache->getStatusOfSubdir(""); $text .= "Cache $res\n"; $res = $cache->getStatusOfSubdir("remote"); $text .= "Cache remote $res\n"; $res = $cache->getStatusOfSubdir("dummy"); $text .= "Cache dummy $res\n"; $res = $cache->getStatusOfSubdir("srgb"); $text .= "Cache srgb $res\n"; $res = $cache->getStatusOfSubdir($fastTrackCache); $text .= "Cache fasttrack $res\n"; $text .= "Alias path writable = " . is_writable($aliasPath) . "\n"; $no = extension_loaded('exif') ? null : 'NOT'; $text .= "Extension exif is $no loaded.
$textEOD; exit; } if ($verboseFile) { $img->setVerboseToFile("$cachePath/log.txt"); } $hookBeforeCImage = getConfig('hook_before_CImage', null); if (is_callable($hookBeforeCImage)) { verbose("hookBeforeCImage activated"); $allConfig = $hookBeforeCImage($img, array( 'newWidth' => $newWidth, 'newHeight' => $newHeight, 'aspectRatio' => $aspectRatio, 'keepRatio' => $keepRatio, 'cropToFit' => $cropToFit, 'fillToFit' => $fillToFit, 'crop' => $crop, 'area' => $area, 'upscale' => $upscale, 'scale' => $scale, 'rotateBefore' => $rotateBefore, 'autoRotate' => $autoRotate, 'bgColor' => $bgColor, 'palette' => $palette, 'filters' => $filters, 'sharpen' => $sharpen, 'emboss' => $emboss, 'blur' => $blur, 'convolve' => $convolve, 'rotateAfter' => $rotateAfter, 'interlace' => $interlace, 'outputFormat' => $outputFormat, 'dpr' => $dpr, 'postProcessing' => $postProcessing, 'lossy' => $lossy, )); verbose(print_r($allConfig, 1)); extract($allConfig); } if ($verbose) { $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); unset($query['verbose']); unset($query['v']); unset($query['nocache']); unset($query['nc']); unset($query['json']); $url1 = '?' . htmlentities(urldecode(http_build_query($query))); $url2 = '?' . urldecode(http_build_query($query)); echo <<
$url1=$description?>
The following images are used for this test.
The following testcases are used for each image.
=$tc?>
=$image . $tc?>
(json)
(verbose)
CImage.php through img.phpwider.jpg| Testcase: | Result: |
|---|---|
$key{$val['text']}".htmlentities($url)." |
higher.jpg| Testcase: | Result: |
|---|---|
$key{$val['text']}".htmlentities($url)." |
A collection of linkt to use to test various aspects of the cimage process.