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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } if (!is_null($this->verboseFileName)) { file_put_contents( $this->verboseFileName, str_replace("
", "\n", $log) ); } else { echo <<CImage Verbose Output
{$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 ===================================== [![Join the chat at https://gitter.im/mosbth/cimage](https://badges.gitter.im/Join%20Chat.svg)](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: http://dbwebb.se/opensource/cimage New website is being setup at [cimage.se](https://cimage.se), to improve documentation (work is ongoing). Requirements -------------------------------------- `CImage` and `img.php` supports GIF (with transparency), JPEG and PNG (8bit transparent, 24bit semi transparent) images. It requires PHP 5.3 and PHP GD. You optionally need the EXIF extension to support auto-rotation of JPEG-images. *Version v0.7.x will be the last version to support PHP 5.3. Coming version will require newer version of PHP.* Installation -------------------------------------- There are several ways of installing. You either install the whole project which uses the autoloader to include the various files, or you install the all-included bundle that -- for convenience -- contains all code in one script. ### Install source from GitHub The [sourcode is available on GitHub](https://github.com/mosbth/cimage). Clone, fork or [download as zip](https://github.com/mosbth/cimage/archive/master.zip). **Latest stable version is v0.7.18 released 2016-08-09.** I prefer cloning like this. Do switch to the latest stable version. ```bash git clone git://github.com/mosbth/cimage.git cd cimage git checkout v0.7.18 ``` Make the cache-directory writable by the webserver. ```bash chmod 777 cache ``` ### Install all-included bundle There are some all-included bundles of `img.php` that can be downloaded and used without dependency to the rest of the sourcecode. | Scriptname | Description | |------------|-------------| | `imgd.php` | Development mode with verbose error reporting and option `&verbose` enabled. | | `imgp.php` | Production mode logs all errors to file, giving server error 500 for bad usage, option `&verbose` disabled. | | `imgs.php` | Strict mode logs few errors to file, giving server error 500 for bad usage, option `&verbose` disabled. | Dowload the version of your choice like this. ```bash wget https://raw.githubusercontent.com/mosbth/cimage/v0.7.18/webroot/imgp.php ``` Open up the file in your editor and edit the array `$config`. Ensure that the paths to the image directory and the cache directory matches your environment, or create an own config-file for the script. ### Install from Packagist You can install the package [`mos/cimage` from Packagist](https://packagist.org/packages/mos/cimage) using composer. Use cases -------------------------------------- Lets take some use cases to let you know when and how `img.php` might be useful. ### Make a thumbnail 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.
`?w=300&h=150` | | | **Stretch.** Stretch the image so that the resulting image has the defined width and height.
`?w=300&h=150&stretch` | | | **Crop to fit.** Keep the aspect ratio and crop out the parts of the image that does not fit.
`?w=300&h=150&crop-to-fit` | | | **Fill to fit.** Keep the aspect ratio and fill then blank space with a background color.
`?w=300&h=150&fill-to-fit=006600` | | 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`
`r, rotate` | Rotate the image after its processed, send the angle as parameter `ra=45`. | | `sharpen` | Appy a convolution filter that sharpens the image. | | `emboss` | Appy a convolution filter with an emboss effect. | | `blur` | Appy a convolution filter with a blur effect. | | `convolve` | Appy custom convolution filter as a 3x3 matrix, a divisor and offset, `convolve=0,-1,0,-1,5,-1,0,-1,0,1,0` sharpens the image. | | `convolve` | Use predefined convolution expression as `convolve=sharpen-alt` or a serie of convolutions as `convolve=draw,mean,motion`. These are supported out of the box: `lighten`, `darken`, `sharpen`, `sharpen-alt`, `emboss`, `emboss-alt`, `blur`, `gblur`, `edge`, `edge-alt`, `draw`, `mean`, `motion`. Add your own, or overwrite existing, in `img_config.php`. | | `f, filter` | Apply filter to image, `f=colorize,0,255,0,0` makes image more green. Supports all filters as defined in [PHP GD `imagefilter()`](http://php.net/manual/en/function.imagefilter.php). | | `f0, f1-f9` | Same as `filter`, just add more filters. Applied in order `f`, `f0-f9`. | | `sc, shortcut` | Save longer expressions in `img_config.php`. One place to change your favorite processing options, use as `sc=sepia` which is a shortcut for `&f=grayscale&f0=brightness,-10&f1=contrast,-20`
`&f2=colorize,120,60,0,0&sharpen`. | #### Saving image, affecting quality and file size Options for saving the target image. | Parameter | Explained | |----------------|----------------------------------------------| | `q, quality` | Quality affects lossy compression and file size for JPEG images by setting the quality between 1-100, default is 60. Quality only affects JPEG. | | `co, compress` | For PNG images it defines the compression algorithm, values can be 0-9, default is defined by PHP GD. Compress only affects PNG. | | `p, palette` | Create a palette version of the image with up to 256 colors. | | `sa, save-as` | Save resulting image as JPEG, PNG or GIF, for example `?src=river.png&save-as=gif`. | | `alias` | Save resulting image as a copy in the alias-directory. | Carry on reading to view examples on how to use and combine the parameters to achieve desired effects and target images. The configuration settings in `_config.php` -------------------------------------- There are several configurations settings that can be used to change the behaviour of `img.php`. Here is an overview, listed as they appear in the default config-file. | Setting | Explained | |-------------------------|----------------------------------------------| | `mode` | Set to "development", "production" or "strict" to match the mode of your environment. It mainly affects the error reporting and if option `verbose` is enabled or not. | | `autoloader` | Path to the file containing the autoloader. | | `image_path` | Path to the directory-structure containing the images. | | `cache_path` | Path to the directory where all the cache-files are stored. | | `alias_path` | Path to where the alias, or copy, of the images are stored. | | `password` | Set the password to use. | | `password_always` | Always require the use of password and match with `password`. | | `remote_allow` | Allow remote download of images when `src=http://example.com/img.jpg`. | | `remote_pattern` | Pattern (regexp) to detect if a file is remote or not. | | `valid_filename` | A regular expression to test if a `src` filename is valid or not. | | `valid_aliasname` | A regular expression to test if a `alias` filename is valid or not. | | | `img_path_constraint` | Check that the target image is in a true subdirectory of `img-path` (disables symbolic linking to another part of the filesystem. | | `default_timezone` | Use to set the timezone if its not already set. | | `max_width` | Maximal width of the target image. Fails for larger values. | | `max_height` | Maximal height of the target image. Fails for larger values. | | `background_color` | Specify a default background color and overwrite the one proposed by `CImage`. | | `png_filter` | Use (or not) an external command for filter PNG images. | | `png_filter_cmd` | Path and options to the actual external command. | | `png_deflate` | Use (or not) an external command for deflating PNG images. | | `png_deflate_cmd` | Path and options to the actual external command. | | `jpeg_optimize` | Use (or not) an external command for optimizing JPEG images. | | `jpeg_optimize_cmd` | Path and options to the actual external command. | | `convolution_constant` | Constants for own defined convolution expressions. | | `allow_hotlinking` | Allow or disallow hotlinking7leeching of images. | | `hotlinking_whitelist` | Array of regular expressions that allow hotlinking (if hotlinking is disabled). | | `shortcut` | Define own shortcuts for more advanced combination of options to `img.php`. | | `size_constant` | Create an array with constant values to be used instead of `width` and `height`. | | `aspect_ratio_constant` | Create an array for constant values to be used with option 'aspect-ratio`. | Consult the file `webroot/img-config.php` for a complete list together with the default values for each configuration setting. There is an [appendix where you can see the default config-file](#img-config). ### Create and name the config file The file `img.php` looks for the config-file `img_config.php`, and uses it if its found. The three files where everything is included -- `imgd.php`, `imgp.php` and `imgs.php` -- includes an empty `$config`-array which can be overridden by saving a config-file in the same directory. If the script is `imgp.php` then name the config-file `imgp_config.php` and it will find it and use those settings. Debugging image processing -------------------------------------- You can visualize what happens during image processing by adding the `v, verbose` parameter. It will then display the resulting image together with a verbose output on what is actually happening behind the scene. 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 ================================================ CImage API Documentaion

\CAsciiArt

Create an ASCII version of an image.

Summary

Methods
Properties
Constants
__construct()
addCharacterSet()
setOptions()
createFromFile()
luminanceAreaAverage()
getLuminance()
luminance2character()
No public properties found
No constants found
No protected methods found
No protected properties found
N/A
No private methods found
$characterSet
$characters
$charCount
$scale
$luminanceStrategy
N/A

Properties

$characterSet

$characterSet : 

Character set to use.

Type

$characters

$characters : 

Current character set.

Type

$charCount

$charCount : 

Length of current character set.

Type

$scale

$scale : 

Scale of the area to swap to a character.

Type

$luminanceStrategy

$luminanceStrategy : 

Strategy to calculate luminance.

Type

Methods

__construct()

__construct() 

Constructor which sets default options.

addCharacterSet()

addCharacterSet(string  $key, string  $value) : $this

Add a custom character set.

Parameters

string $key

for the character set.

string $value

for the character set.

Returns

$this

setOptions()

setOptions(array  $options = array()) : $this

Set options for processing, defaults are available.

Parameters

array $options

to use as default settings.

Returns

$this

createFromFile()

createFromFile(string  $filename) : string

Create an Ascii image from an image file.

Parameters

string $filename

of the image to use.

Returns

string —

$ascii with the ASCII image.

luminanceAreaAverage()

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.

Parameters

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.

Returns

integer —

$luminance with a value between 0 and 100.

getLuminance()

getLuminance(integer  $red, integer  $green, integer  $blue) : float

Calculate luminance value with different strategies.

Parameters

integer $red

The color red.

integer $green

The color green.

integer $blue

The color blue.

Returns

float —

$luminance with a value between 0 and 1.

luminance2character()

luminance2character(  $luminance) : string

Translate the luminance value to a character.

Parameters

$luminance

Returns

string —

with the ascii character.

================================================ FILE: docs/api/classes/CHttpGet.html ================================================ CImage API Documentaion

\CHttpGet

Get a image from a remote server using HTTP GET and If-Modified-Since.

Summary

Methods
Properties
Constants
__construct()
buildUrl()
setUrl()
setHeader()
parseHeader()
doGet()
getStatus()
getLastModified()
getContentType()
getDate()
getMaxAge()
getBody()
No public properties found
No constants found
No protected methods found
No protected properties found
N/A
No private methods found
$request
$response
N/A

Properties

$request

$request : 

Type

$response

$response : 

Type

Methods

__construct()

__construct() 

Constructor

buildUrl()

buildUrl(string  $baseUrl, string  $merge) : string

Build an encoded url.

Parameters

string $baseUrl

This is the original url which will be merged.

string $merge

Thse parts should be merged into the baseUrl, the format is as parse_url.

Returns

string —

$url as the modified url.

setUrl()

setUrl(string  $url) : $this

Set the url for the request.

Parameters

string $url

Returns

$this

setHeader()

setHeader(string  $field, string  $value) : $this

Set custom header field for the request.

Parameters

string $field
string $value

Returns

$this

parseHeader()

parseHeader() : $this

Set header fields for the request.

Returns

$this

doGet()

doGet(boolean  $debug = false) : boolean

Perform the request.

Parameters

boolean $debug

set to true to dump headers.

Throws

\Exception

when curl fails to retrieve url.

Returns

boolean

getStatus()

getStatus() : integer

Get HTTP code of response.

Returns

integer —

as HTTP status code or null if not available.

getLastModified()

getLastModified() : integer

Get file modification time of response.

Returns

integer —

as timestamp.

getContentType()

getContentType() : string

Get content type.

Returns

string —

as the content type or null if not existing or invalid.

getDate()

getDate(mixed  $default = false) : integer

Get file modification time of response.

Parameters

mixed $default

as default value (int seconds) if date is missing in response header.

Returns

integer —

as timestamp or $default if Date is missing in response header.

getMaxAge()

getMaxAge(mixed  $default = false) : integer

Get max age of cachable item.

Parameters

mixed $default

as default value if date is missing in response header.

Returns

integer —

as timestamp or false if not available.

getBody()

getBody() : string

Get body of response.

Returns

string —

as body.

================================================ FILE: docs/api/classes/CImage.html ================================================ CImage API Documentaion

\CImage

Resize and crop images on the fly, store generated images in a cache.

Examples

** File not found : http://dbwebb.se/opensource/cimage **

Summary

Methods
Properties
Constants
__construct()
setVerbose()
setSaveFolder()
useCache()
createDummyImage()
setRemoteDownload()
isRemoteSource()
setRemoteHostWhitelist()
isRemoteSourceOnWhitelist()
downloadRemoteSource()
setSource()
setTarget()
getTarget()
setOptions()
loadImageDetails()
initDimensions()
calculateNewWidthAndHeight()
reCalculateDimensions()
setSaveAsExtension()
setJpegQuality()
setPngCompression()
useOriginalIfPossible()
generateFilename()
useCacheIfPossible()
load()
getPngType()
preResize()
setCopyResizeStrategy()
imageCopyResampled()
resize()
postResize()
rotate()
rotateExif()
trueColorToPalette()
sharpenImage()
embossImage()
blurImage()
createConvolveArguments()
addConvolveExpressions()
imageConvolution()
setDefaultBackgroundColor()
setPostProcessingOptions()
save()
linkToCacheFile()
addHTTPHeader()
output()
json()
setAsciiOptions()
ascii()
log()
setVerboseToFile()
$crop
$cropOrig
$keepRatio
$cropToFit
$crop_x
$crop_y
$filters
PNG_GREYSCALE
PNG_RGB
PNG_RGB_PALETTE
PNG_GREYSCALE_ALPHA
PNG_RGB_ALPHA
JPEG_QUALITY_DEFAULT
PNG_COMPRESSION_DEFAULT
UPSCALE_DEFAULT
RESIZE
RESAMPLE
getTargetImageExtension()
No protected properties found
N/A
checkFileExtension()
normalizeFileExtension()
mapFilter()
getPngTypeAsString()
colorsTotal()
getBackgroundColor()
createImageKeepTransparency()
verboseOutput()
raiseError()
$quality
$useQuality
$compress
$useCompress
$HTTPHeader
$bgColorDefault
$bgColor
$saveFolder
$image
$imageSrc
$pathToImage
$fileType
$extension
$outputFormat
$verbose
$log
$palette
$cacheFileName
$saveAs
$pngFilter
$pngFilterCmd
$pngDeflate
$pngDeflateCmd
$jpegOptimize
$jpegOptimizeCmd
$width
$height
$newWidth
$newWidthOrig
$newHeight
$newHeightOrig
$dpr
$upscale
$convolve
$convolves
$fillToFit
$scale
$rotateBefore
$rotateAfter
$autoRotate
$sharpen
$emboss
$blur
$offset
$fillWidth
$fillHeight
$allowRemote
$remotePattern
$useCache
$remoteHostWhitelist
$verboseFileName
$asciiOptions
$copyStrategy
$cropWidth
$cropHeight
$attr
N/A

Constants

PNG_GREYSCALE

PNG_GREYSCALE

Constants type of PNG image

PNG_RGB

PNG_RGB

PNG_RGB_PALETTE

PNG_RGB_PALETTE

PNG_GREYSCALE_ALPHA

PNG_GREYSCALE_ALPHA

PNG_RGB_ALPHA

PNG_RGB_ALPHA

JPEG_QUALITY_DEFAULT

JPEG_QUALITY_DEFAULT

Constant for default image quality when not set

PNG_COMPRESSION_DEFAULT

PNG_COMPRESSION_DEFAULT

Constant for default image quality when not set

UPSCALE_DEFAULT

UPSCALE_DEFAULT

Always upscale images, even if they are smaller than target image.

RESIZE

RESIZE

RESAMPLE

RESAMPLE

Properties

$crop

$crop : 

Array with details on how to crop, incoming as argument and calculated.

Type

$cropOrig

$cropOrig : 

Type

$keepRatio

$keepRatio : 

Properties, the class is mutable and the method setOptions() decides (partly) what properties are created.

Type

$cropToFit

$cropToFit : 

Type

$crop_x

$crop_x : 

Type

$crop_y

$crop_y : 

Type

$filters

$filters : 

Type

$quality

$quality : 

Quality level for JPEG images.

Type

$useQuality

$useQuality : 

Is the quality level set from external use (true) or is it default (false)?

Type

$compress

$compress : 

Compression level for PNG images.

Type

$useCompress

$useCompress : 

Is the compress level set from external use (true) or is it default (false)?

Type

$HTTPHeader

$HTTPHeader : 

Add HTTP headers for outputing image.

Type

$bgColorDefault

$bgColorDefault : 

Type

$bgColor

$bgColor : 

Background color to use, specified as part of options.

Type

$saveFolder

$saveFolder : 

Where to save the target file.

Type

$image

$image : 

The working image object.

Type

$imageSrc

$imageSrc : 

Image filename, may include subdirectory, relative from $imageFolder

Type

$pathToImage

$pathToImage : 

Actual path to the image, $imageFolder . '/' . $imageSrc

Type

$fileType

$fileType : 

File type for source image, as provided by getimagesize()

Type

$extension

$extension : 

File extension to use when saving image.

Type

$outputFormat

$outputFormat : 

Output format, supports null (image) or json.

Type

$verbose

$verbose : 

Verbose mode to print out a trace and display the created image

Type

$log

$log : 

Keep a log/trace on what happens

Type

$palette

$palette : 

Handle image as palette image

Type

$cacheFileName

$cacheFileName : 

Target filename, with path, to save resulting image in.

Type

$saveAs

$saveAs : 

Set a format to save image as, or null to use original format.

Type

$pngFilter

$pngFilter : 

Path to command for filter optimize, for example optipng or null.

Type

$pngFilterCmd

$pngFilterCmd : 

Type

$pngDeflate

$pngDeflate : 

Path to command for deflate optimize, for example pngout or null.

Type

$pngDeflateCmd

$pngDeflateCmd : 

Type

$jpegOptimize

$jpegOptimize : 

Path to command to optimize jpeg images, for example jpegtran or null.

Type

$jpegOptimizeCmd

$jpegOptimizeCmd : 

Type

$width

$width : 

Image dimensions, calculated from loaded image.

Type

$height

$height : 

Type

$newWidth

$newWidth : 

New image dimensions, incoming as argument or calculated.

Type

$newWidthOrig

$newWidthOrig : 

Type

$newHeight

$newHeight : 

Type

$newHeightOrig

$newHeightOrig : 

Type

$dpr

$dpr : 

Change target height & width when different dpr, dpr 2 means double image dimensions.

Type

$upscale

$upscale : 

Type

$convolve

$convolve : 

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.

Type

$convolves

$convolves : 

Custom convolution expressions, matrix 3x3, divisor and offset.

Type

$fillToFit

$fillToFit : 

Resize strategy to fill extra area with background color.

True or false.

Type

$scale

$scale : 

To store value for option scale.

Type

$rotateBefore

$rotateBefore : 

To store value for option.

Type

$rotateAfter

$rotateAfter : 

To store value for option.

Type

$autoRotate

$autoRotate : 

To store value for option.

Type

$sharpen

$sharpen : 

To store value for option.

Type

$emboss

$emboss : 

To store value for option.

Type

$blur

$blur : 

To store value for option.

Type

$offset

$offset : 

Used with option area to set which parts of the image to use.

Type

$fillWidth

$fillWidth : 

Calculate target dimension for image when using fill-to-fit resize strategy.

Type

$fillHeight

$fillHeight : 

Type

$allowRemote

$allowRemote : 

Allow remote file download, default is to disallow remote file download.

Type

$remotePattern

$remotePattern : 

Type

$useCache

$useCache : 

Use the cache if true, set to false to ignore the cached file.

Type

$remoteHostWhitelist

$remoteHostWhitelist : 

Type

$verboseFileName

$verboseFileName : 

Type

$asciiOptions

$asciiOptions : 

Type

$copyStrategy

$copyStrategy : 

Type

$cropWidth

$cropWidth : 

Type

$cropHeight

$cropHeight : 

Type

$attr

$attr : 

Type

Methods

__construct()

__construct(string  $imageSrc = null, string  $imageFolder = null, string  $saveFolder = null, string  $saveName = null) 

Constructor, can take arguments to init the object.

Parameters

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.

setVerbose()

setVerbose(boolean  $mode = true) : $this

Set verbose mode.

Parameters

boolean $mode

true or false to enable and disable verbose mode, default is true.

Returns

$this

setSaveFolder()

setSaveFolder(string  $path) : $this

Set save folder, base folder for saving cache files.

Parameters

string $path

where to store cached files.

Returns

$this

useCache()

useCache(boolean  $use = true) : $this

Use cache or not.

Parameters

boolean $use

true or false to use cache.

Returns

$this

createDummyImage()

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.

Parameters

integer $width

use specified width for image dimension.

integer $height

use specified width for image dimension.

Returns

$this

setRemoteDownload()

setRemoteDownload(boolean  $allow, string  $pattern = null) : $this

Allow or disallow remote image download.

Parameters

boolean $allow

true or false to enable and disable.

string $pattern

to use to detect if its a remote file.

Returns

$this

isRemoteSource()

isRemoteSource(string  $src) : boolean

Check if the image resource is a remote file or not.

Parameters

string $src

check if src is remote.

Returns

boolean —

true if $src is a remote file, else false.

setRemoteHostWhitelist()

setRemoteHostWhitelist(array  $whitelist = null) : $this

Set whitelist for valid hostnames from where remote source can be downloaded.

Parameters

array $whitelist

with regexp hostnames to allow download from.

Returns

$this

isRemoteSourceOnWhitelist()

isRemoteSourceOnWhitelist(string  $src) : boolean

Check if the hostname for the remote image, is on a whitelist, if the whitelist is defined.

Parameters

string $src

the remote source.

Returns

boolean —

true if hostname on $src is in the whitelist, else false.

downloadRemoteSource()

downloadRemoteSource(string  $src) : string

Download a remote image and return path to its local copy.

Parameters

string $src

remote path to image.

Returns

string —

as path to downloaded remote source.

setSource()

setSource(string  $src, string  $dir = null) : $this

Set source file to use as image source.

Parameters

string $src

of image.

string $dir

as optional base directory where images are.

Returns

$this

setTarget()

setTarget(string  $src = null, string  $dir = null) : $this

Set target file.

Parameters

string $src

of target image.

string $dir

as optional base directory where images are stored. Uses $this->saveFolder if null.

Returns

$this

getTarget()

getTarget() : Boolean|String

Get filename of target file.

Returns

Boolean|String —

as filename of target or false if not set.

setOptions()

setOptions(array  $args) : $this

Set options to use when processing image.

Parameters

array $args

used when processing image.

Returns

$this

loadImageDetails()

loadImageDetails(string  $file = null) : $this

Load image details from original image file.

Parameters

string $file

the file to load or null to use $this->pathToImage.

Throws

\Exception

Returns

$this

initDimensions()

initDimensions() : $this

Init new width and height and do some sanity checks on constraints, before any processing can be done.

Throws

\Exception

Returns

$this

calculateNewWidthAndHeight()

calculateNewWidthAndHeight() : $this

Calculate new width and height of image, based on settings.

Returns

$this

reCalculateDimensions()

reCalculateDimensions() : $this

Re-calculate image dimensions when original image dimension has changed.

Returns

$this

setSaveAsExtension()

setSaveAsExtension(  $saveAs = null) : $this

Set extension for filename to save as.

Parameters

$saveAs

Returns

$this

setJpegQuality()

setJpegQuality(integer  $quality = null) : $this

Set JPEG quality to use when saving image

Parameters

integer $quality

as the quality to set.

Returns

$this

setPngCompression()

setPngCompression(integer  $compress = null) : $this

Set PNG compressen algorithm to use when saving image

Parameters

integer $compress

as the algorithm to use.

Returns

$this

useOriginalIfPossible()

useOriginalIfPossible(boolean  $useOrig = true) : $this

Use original image if possible, check options which affects image processing.

Parameters

boolean $useOrig

default is to use original if possible, else set to false.

Returns

$this

generateFilename()

generateFilename(string  $base = null, boolean  $useSubdir = true) : $this

Generate filename to save file in cache.

Parameters

string $base

as optional basepath for storing file.

boolean $useSubdir

use or skip the subdir part when creating the filename.

Returns

$this

useCacheIfPossible()

useCacheIfPossible(boolean  $useCache = true) : $this

Use cached version of image, if possible.

Parameters

boolean $useCache

is default true, set to false to avoid using cached object.

Returns

$this

load()

load(string  $src = null, string  $dir = null) : $this

Load image from disk. Try to load image without verbose error message, if fail, load again and display error messages.

Parameters

string $src

of image.

string $dir

as base directory where images are.

Returns

$this

getPngType()

getPngType(string  $filename = null) : integer

Get the type of PNG image.

Parameters

string $filename

to use instead of default.

Returns

integer —

as the type of the png-image

preResize()

preResize() : $this

Preprocess image before rezising it.

Returns

$this

setCopyResizeStrategy()

setCopyResizeStrategy(integer  $strategy) : $this

Resize or resample the image while resizing.

Parameters

integer $strategy

as CImage::RESIZE or CImage::RESAMPLE

Returns

$this

imageCopyResampled()

imageCopyResampled(  $dst_image,   $src_image,   $dst_x,   $dst_y,   $src_x,   $src_y,   $dst_w,   $dst_h,   $src_w,   $src_h) : void

Resize and or crop the image.

Parameters

$dst_image
$src_image
$dst_x
$dst_y
$src_x
$src_y
$dst_w
$dst_h
$src_w
$src_h

resize()

resize() : $this

Resize and or crop the image.

Returns

$this

postResize()

postResize() : $this

Postprocess image after rezising image.

Returns

$this

rotate()

rotate(float  $angle,   $bgColor) : $this

Rotate image using angle.

Parameters

float $angle

to rotate image.

$bgColor

Returns

$this

rotateExif()

rotateExif() : $this

Rotate image using information in EXIF.

Returns

$this

sharpenImage()

sharpenImage() : $this

Sharpen image using image convolution.

Returns

$this

embossImage()

embossImage() : $this

Emboss image using image convolution.

Returns

$this

blurImage()

blurImage() : $this

Blur image using image convolution.

Returns

$this

createConvolveArguments()

createConvolveArguments(string  $expression) : array

Create convolve expression and return arguments for image convolution.

Parameters

string $expression

constant string which evaluates to a list of 11 numbers separated by komma or such a list.

Returns

array —

as $matrix (3x3), $divisor and $offset

addConvolveExpressions()

addConvolveExpressions(array  $options) : $this

Add custom expressions (or overwrite existing) for image convolution.

Parameters

array $options

Key value array with strings to be converted to convolution expressions.

Returns

$this

imageConvolution()

imageConvolution(string  $options = null) : $this

Image convolution.

Parameters

string $options

A string with 11 float separated by comma.

Returns

$this

setDefaultBackgroundColor()

setDefaultBackgroundColor(string  $color) : $this

Set default background color between 000000-FFFFFF or if using alpha 00000000-FFFFFF7F.

Parameters

string $color

as hex value.

Returns

$this

setPostProcessingOptions()

setPostProcessingOptions(array  $options) : $this

Set optimizing and post-processing options.

Parameters

array $options

with config for postprocessing with external tools.

Returns

$this

save()

save(string  $src = null, string  $base = null, boolean  $overwrite = true) : $this

Save image.

Parameters

string $src

as target filename.

string $base

as base directory where to store images.

boolean $overwrite

or not, default to always overwrite file.

Returns

$this —

or false if no folder is set.

linkToCacheFile()

linkToCacheFile(string  $alias) : $this

Create a hard link, as an alias, to the cached file.

Parameters

string $alias

where to store the link, filename without extension.

Returns

$this

addHTTPHeader()

addHTTPHeader(string  $type, string  $value) : void

Add HTTP header for putputting together with image.

Parameters

string $type

the header type such as "Cache-Control"

string $value

the value to use

output()

output(string  $file = null, string  $format = null) : void

Output image to browser using caching.

Parameters

string $file

to read and output, default is to use $this->cacheFileName

string $format

set to json to output file as json object with details

json()

json(string  $file = null) : string

Create a JSON object from the image details.

Parameters

string $file

the file to output.

Returns

string —

json-encoded representation of the image.

setAsciiOptions()

setAsciiOptions(array  $options = array()) : \void.

Set options for creating ascii version of image.

Parameters

array $options

empty to use default or set options to change.

Returns

\void.

ascii()

ascii(string  $file = null) : string

Create an ASCII version from the image details.

Parameters

string $file

the file to output.

Returns

string —

ASCII representation of the image.

log()

log(string  $message) : \this

Log an event if verbose mode.

Parameters

string $message

to log.

Returns

\this

setVerboseToFile()

setVerboseToFile(string  $fileName) : void

Do verbose output to a file.

Parameters

string $fileName

where to write the verbose output.

getTargetImageExtension()

getTargetImageExtension() : string

Find out the type (file extension) for the image to be saved.

Returns

string —

as image extension.

checkFileExtension()

checkFileExtension(string  $extension) : $this

Check if file extension is valid as a file extension.

Parameters

string $extension

of image file.

Returns

$this

normalizeFileExtension()

normalizeFileExtension(string  $extension = null) : string

Normalize the file extension.

Parameters

string $extension

of image file or skip to use internal.

Returns

string —

$extension as a normalized file extension.

mapFilter()

mapFilter(string  $name) : array

Map filter name to PHP filter and id.

Parameters

string $name

the name of the filter.

Throws

\Exception

Returns

array —

with filter settings

getPngTypeAsString()

getPngTypeAsString(  $pngType = null, string  $filename = null) : integer

Get the type of PNG image as a verbose string.

Parameters

$pngType
string $filename

to use instead of default.

Returns

integer —

as the type of the png-image

colorsTotal()

colorsTotal(resource  $im) : integer

Calculate number of colors in an image.

Parameters

resource $im

the image.

Returns

integer

getBackgroundColor()

getBackgroundColor(resource  $img = null) : \color

Get the background color.

Parameters

resource $img

the image to work with or null if using $this->image.

Returns

\color —

value or null if no background color is set.

createImageKeepTransparency()

createImageKeepTransparency(integer  $width, integer  $height) : \image

Create a image and keep transparency for png and gifs.

Parameters

integer $width

of the new image.

integer $height

of the new image.

Returns

\image —

resource.

verboseOutput()

verboseOutput() : void

Do verbose output and print out the log and the actual images.

raiseError()

raiseError(string  $message) : void

Raise error, enables to implement a selection of error methods.

Parameters

string $message

the error message to display.

Throws

\Exception
================================================ FILE: docs/api/classes/CImage_RemoteDownloadTest.html ================================================ CImage API Documentaion

Properties

$remote_whitelist

$remote_whitelist : 

Type

Methods

providerValidRemoteSource()

providerValidRemoteSource() : array

Provider for valid remote sources.

Returns

array

providerInvalidRemoteSource()

providerInvalidRemoteSource() : array

Provider for invalid remote sources.

Returns

array

testAllowRemoteDownloadDefaultPatternValid()

testAllowRemoteDownloadDefaultPatternValid(  $source) : void

Test

Parameters

$source

testAllowRemoteDownloadDefaultPatternInvalid()

testAllowRemoteDownloadDefaultPatternInvalid(  $source) : void

Test

Parameters

$source

providerHostnameMatch()

providerHostnameMatch() : array

Provider for hostname matching the whitelist.

Returns

array

testRemoteHostWhitelistMatch()

testRemoteHostWhitelistMatch(string  $hostname) : void

Test

Parameters

string $hostname

matches the whitelist

providerHostnameNoMatch()

providerHostnameNoMatch() : array

Provider for hostname not matching the whitelist.

Returns

array

testRemoteHostWhitelistNoMatch()

testRemoteHostWhitelistNoMatch(string  $hostname) : void

Test

Parameters

string $hostname

not matching the whitelist

================================================ FILE: docs/api/classes/CRemoteImage.html ================================================ CImage API Documentaion

\CRemoteImage

Get a image from a remote server using HTTP GET and If-Modified-Since.

Summary

Methods
Properties
Constants
getStatus()
getDetails()
setCache()
isCacheWritable()
useCache()
setHeaderFields()
save()
updateCacheDetails()
download()
loadCacheDetails()
getCachedSource()
No public properties found
No constants found
No protected methods found
No protected properties found
N/A
No private methods found
$saveFolder
$useCache
$http
$status
$defaultMaxAge
$url
$fileName
$fileJson
$cache
N/A

Properties

$saveFolder

$saveFolder : 

Path to cache files.

Type

$useCache

$useCache : 

Use cache or not.

Type

$http

$http : 

HTTP object to aid in download file.

Type

$status

$status : 

Status of the HTTP request.

Type

$defaultMaxAge

$defaultMaxAge : 

Defalt age for cached items 60*60*24*7.

Type

$url

$url : 

Url of downloaded item.

Type

$fileName

$fileName : 

Base name of cache file for downloaded item and name of image.

Type

$fileJson

$fileJson : 

Filename for json-file with details of cached item.

Type

$cache

$cache : 

Cache details loaded from file.

Type

Methods

getStatus()

getStatus() : integer

Get status of last HTTP request.

Returns

integer —

as status

getDetails()

getDetails() : array

Get JSON details for cache item.

Returns

array —

with json details on cache.

setCache()

setCache(  $path) : $this

Set the path to the cache directory.

Parameters

$path

Returns

$this

isCacheWritable()

isCacheWritable() : $this

Check if cache is writable or throw exception.

Throws

\Exception

if cahce folder is not writable.

Returns

$this

useCache()

useCache(boolean  $use = true) : $this

Decide if the cache should be used or not before trying to download a remote file.

Parameters

boolean $use

true to use the cache and false to ignore cache.

Returns

$this

setHeaderFields()

setHeaderFields() : $this

Set header fields.

Returns

$this

save()

save() : string

Save downloaded resource to cache.

Returns

string —

as path to saved file or false if not saved.

updateCacheDetails()

updateCacheDetails() : string

Got a 304 and updates cache with new age.

Returns

string —

as path to cached file.

download()

download(string  $url) : string

Download a remote file and keep a cache of downloaded files.

Parameters

string $url

a remote url.

Throws

\Exception

when status code does not match 200 or 304.

Returns

string —

as path to downloaded file or false if failed.

loadCacheDetails()

loadCacheDetails() : $this

Get the path to the cached image file if the cache is valid.

Returns

$this

getCachedSource()

getCachedSource() : string

Get the path to the cached image file if the cache is valid.

Returns

string —

as the path ot the image file or false if no cache.

================================================ FILE: docs/api/classes/CWhitelist.html ================================================ CImage API Documentaion

\CWhitelist

Act as whitelist (or blacklist).

Summary

Methods
Properties
Constants
set()
check()
No public properties found
No constants found
No protected methods found
No protected properties found
N/A
No private methods found
$whitelist
N/A

Properties

$whitelist

$whitelist : 

Array to contain the whitelist options.

Type

Methods

set()

set(array  $whitelist = array()) : $this

Set the whitelist from an array of strings, each item in the whitelist should be a regexp without the surrounding / or #.

Parameters

array $whitelist

with all valid options, default is to clear the whitelist.

Returns

$this

check()

check(string  $item, array  $whitelist = null) : boolean

Check if item exists in the whitelist.

Parameters

string $item

string to check.

array $whitelist

optional with all valid options, default is null.

Returns

boolean —

true if item is in whitelist, else false.

================================================ FILE: docs/api/classes/CWhitelistTest.html ================================================ CImage API Documentaion

\CWhitelistTest

A testclass

Summary

Methods
Properties
Constants
providerHostnameMatch()
providerHostnameNoMatch()
testRemoteHostWhitelistMatch()
testRemoteHostWhitelistNoMatch()
No public properties found
No constants found
No protected methods found
No protected properties found
N/A
No private methods found
$remote_whitelist
N/A

Properties

$remote_whitelist

$remote_whitelist : 

Type

Methods

providerHostnameMatch()

providerHostnameMatch() : array

Provider for hostname matching the whitelist.

Returns

array

providerHostnameNoMatch()

providerHostnameNoMatch() : array

Provider for hostname not matching the whitelist.

Returns

array

testRemoteHostWhitelistMatch()

testRemoteHostWhitelistMatch(string  $hostname) : void

Test

Parameters

string $hostname

matches the whitelist

testRemoteHostWhitelistNoMatch()

testRemoteHostWhitelistNoMatch(string  $hostname) : void

Test

Parameters

string $hostname

not matching the whitelist

================================================ FILE: docs/api/css/jquery.iviewer.css ================================================ .viewer { -ms-touch-action: none; } .iviewer_common { position:absolute; bottom:10px; border: 1px solid #000; height: 28px; z-index: 5000; } .iviewer_cursor { cursor: url(../images/iviewer/hand.cur) 6 8, pointer; } .iviewer_drag_cursor { cursor: url(../images/iviewer/grab.cur) 6 8, pointer; } .iviewer_button { width: 28px; cursor: pointer; background-position: center center; background-repeat: no-repeat; } .iviewer_zoom_in { left: 20px; background: url(../images/iviewer/iviewer.zoom_in.png); } .iviewer_zoom_out { left: 55px; background: url(../images/iviewer/iviewer.zoom_out.png); } .iviewer_zoom_zero { left: 90px; background: url(../images/iviewer/iviewer.zoom_zero.png); } .iviewer_zoom_fit { left: 125px; background: url(../images/iviewer/iviewer.zoom_fit.png); } .iviewer_zoom_status { left: 160px; font: 1em/28px Sans; color: #000; background-color: #fff; text-align: center; width: 60px; } .iviewer_rotate_left { left: 227px; background: #fff url(../images/iviewer/iviewer.rotate_left.png) center center no-repeat; } .iviewer_rotate_right { left: 262px; background: #fff url(../images/iviewer/iviewer.rotate_right.png) center center no-repeat; } ================================================ FILE: docs/api/css/phpdocumentor-clean-icons/Read Me.txt ================================================ To modify your generated font, use the *dev.svg* file, located in the *fonts* folder in this package. You can import this dev.svg file to the IcoMoon app. All the tags (class names) and the Unicode points of your glyphs are saved in this file. See the documentation for more info on how to use this package: http://icomoon.io/#docs/font-face ================================================ FILE: docs/api/css/phpdocumentor-clean-icons/lte-ie7.js ================================================ /* Load this script using conditional IE comments if you need to support IE 7 and IE 6. */ window.onload = function() { function addIcon(el, entity) { var html = el.innerHTML; el.innerHTML = '' + entity + '' + html; } var icons = { 'icon-trait' : '', 'icon-interface' : '', 'icon-class' : '' }, els = document.getElementsByTagName('*'), i, attr, html, c, el; for (i = 0; ; i += 1) { el = els[i]; if(!el) { break; } attr = el.getAttribute('data-icon'); if (attr) { addIcon(el, attr); } c = el.className; c = c.match(/icon-[^\s'"]+/); if (c && icons[c[0]]) { addIcon(el, icons[c[0]]); } } }; ================================================ FILE: docs/api/css/phpdocumentor-clean-icons/style.css ================================================ @font-face { font-family: 'phpdocumentor-clean-icons'; src:url('fonts/phpdocumentor-clean-icons.eot'); src:url('fonts/phpdocumentor-clean-icons.eot?#iefix') format('embedded-opentype'), url('fonts/phpdocumentor-clean-icons.woff') format('woff'), url('fonts/phpdocumentor-clean-icons.ttf') format('truetype'), url('fonts/phpdocumentor-clean-icons.svg#phpdocumentor-clean-icons') format('svg'); font-weight: normal; font-style: normal; } /* Use the following CSS code if you want to use data attributes for inserting your icons */ [data-icon]:before { font-family: 'phpdocumentor-clean-icons'; content: attr(data-icon); speak: none; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; } /* Use the following CSS code if you want to have a class per icon */ /* Instead of a list of all class selectors, you can use the generic selector below, but it's slower: [class*="icon-"] { */ .icon-trait, .icon-interface, .icon-class { font-family: 'phpdocumentor-clean-icons'; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; } .icon-trait:before { content: "\e000"; } .icon-interface:before { content: "\e001"; } .icon-class:before { content: "\e002"; } ================================================ FILE: docs/api/css/prism.css ================================================ /** * prism.js default theme for JavaScript, CSS and HTML * Based on dabblet (http://dabblet.com) * @author Lea Verou */ code[class*="language-"], pre[class*="language-"] { color: black; text-shadow: 0 1px white; font-family: Consolas, Monaco, 'Andale Mono', monospace; direction: ltr; text-align: left; white-space: pre; word-spacing: normal; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } ::-moz-selection { text-shadow: none; background: #b3d4fc; } ::selection { text-shadow: none; background: #b3d4fc; } @media print { code[class*="language-"], pre[class*="language-"] { text-shadow: none; } } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: .5em 0; overflow: auto; } :not(pre) > code[class*="language-"], pre[class*="language-"] { background: #f5f2f0; } /* Inline code */ :not(pre) > code[class*="language-"] { padding: .1em; border-radius: .3em; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: slategray; } .token.punctuation { color: #999; } .namespace { opacity: .7; } .token.property, .token.tag, .token.boolean, .token.number { color: #905; } .token.selector, .token.attr-name, .token.string { color: #690; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #a67f59; background: hsla(0,0%,100%,.5); } .token.atrule, .token.attr-value, .token.keyword { color: #07a; } .token.regex, .token.important { color: #e90; } .token.important { font-weight: bold; } .token.entity { cursor: help; } pre[data-line] { position: relative; padding: 1em 0 1em 3em; } .line-highlight { position: absolute; left: 0; right: 0; padding: inherit 0; margin-top: 1em; /* Same as .prism’s padding-top */ background: hsla(24, 20%, 50%,.08); background: -moz-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); background: -webkit-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); background: -o-linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); background: linear-gradient(left, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); pointer-events: none; line-height: inherit; white-space: pre; } .line-highlight:before, .line-highlight[data-end]:after { content: attr(data-start); position: absolute; top: .4em; left: .6em; min-width: 1em; padding: 0 .5em; background-color: hsla(24, 20%, 50%,.4); color: hsl(24, 20%, 95%); font: bold 65%/1.5 sans-serif; text-align: center; vertical-align: .3em; border-radius: 999px; text-shadow: none; box-shadow: 0 1px white; } .line-highlight[data-end]:after { content: attr(data-end); top: auto; bottom: .4em; } pre.line-numbers { position: relative; padding-left: 3.8em; counter-reset: linenumber; } pre.line-numbers > code { position: relative; } .line-numbers .line-numbers-rows { position: absolute; pointer-events: none; top: 0; font-size: 100%; left: -3.8em; width: 3em; /* works for line-numbers below 1000 lines */ letter-spacing: -1px; border-right: 1px solid #999; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .line-numbers-rows > span { pointer-events: none; display: block; counter-increment: linenumber; } .line-numbers-rows > span:before { content: counter(linenumber); color: #999; display: block; padding-right: 0.8em; text-align: right; } ================================================ FILE: docs/api/css/template.css ================================================ @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro); @import url('phpdocumentor-clean-icons/style.css'); body { padding-top: 40px; background-color: #333333; } a { color: #6495ed; } a.anchor { height: 40px; margin-top: -40px; display: block; } h1, h2, h3, h4, h5, h6, .brand { font-family: 'Source Sans Pro', sans-serif; font-weight: normal; letter-spacing: 0.05em; } h2, h3, .detailsbar h1 { overflow: hidden; white-space: nowrap; margin: 30px 0 20px 0; } h2:after, h3:after, .detailsbar h1:after { content: ''; display: inline-block; vertical-align: middle; width: 100%; height: 2px; margin-left: 1em; background: silver; } h3 { margin: 10px 0 20px 0; } h4 { margin: 20px 0 10px 0; color: gray; font-size: 18.5px; } h3.public, h3.protected, h3.private { padding-left: 10px; text-overflow: ellipsis; } .table tr:first-of-type th, .table tr:first-of-type td { border-top: none; } .detailsbar { color: #eeeeee; background-color: #333333; font-size: 0.9em; overflow: hidden; border-left: 2px solid gray; } .detailsbar h1 { font-size: 1.5em; margin-bottom: 20px; margin-top: 0; } .detailsbar h2 { font-size: 1.2em; margin: 0; padding: 0; } .detailsbar h1:after { background: gray; } .detailsbar h2:after, .detailsbar h3:after { background: transparent; } .detailsbar dt { font-variant: small-caps; text-transform: lowercase; font-size: 1.1em; letter-spacing: 0.1em; color: silver; } .hierarchy div:nth-of-type(2) { margin-left: 11px; } .hierarchy div:nth-of-type(3) { margin-left: 22px; } .hierarchy div:nth-of-type(4) { margin-left: 33px; } .hierarchy div:nth-of-type(5) { margin-left: 44px; } .hierarchy div:nth-of-type(6) { margin-left: 55px; } .hierarchy div:nth-of-type(7) { margin-left: 66px; } .hierarchy div:nth-of-type(8) { margin-left: 77px; } .hierarchy div:nth-of-type(9) { margin-left: 88px; } .hierarchy div:before { content: "\f0da"; font-family: FontAwesome; margin-right: 5px; } .row-fluid { background-color: white; overflow: hidden; } footer.row-fluid, footer.row-fluid * { background-color: #333333; color: white; } footer.row-fluid { border-top: 2px dashed #555; margin-top: 2px; } .footer-sections .span4 { border: 2px solid #555; text-align: center; border-radius: 10px; margin-top: 70px; margin-bottom: 20px; background: #373737; } .footer-sections .span4 h1 { background: transparent; margin-top: -30px; margin-bottom: 20px; font-size: 5em; } .footer-sections .span4 h1 * { background: transparent; } .footer-sections .span4 div { border-bottom-right-radius: 6px; border-bottom-left-radius: 6px; padding: 10px; min-height: 40px; } .footer-sections .span4 div, .footer-sections .span4 div * { background-color: #555; } .footer-sections .span4 ul { text-align: left; list-style: none; margin: 0; padding: 0; } .content { background-color: white; padding-right: 20px; } .content nav { text-align: center; border-bottom: 1px solid silver; margin: 5px 0 20px 0; padding-bottom: 5px; } .content > h1 { padding-bottom: 15px; } .content > h1 small { display: block; padding-bottom: 8px; font-size: 0.6em; } .deprecated { text-decoration: line-through; } .method { margin-bottom: 20px; } .method .signature .argument { color: maroon; font-weight: bold; } .class #summary section.row-fluid { overflow: hidden } .class #summary .heading { font-weight: bold; text-align: center; } .class #summary section .span4 { padding: 3px; overflow: hidden; margin-bottom: -9999px; padding-bottom: 9999px; white-space: nowrap; text-overflow: ellipsis; border-left: 5px solid transparent; } .class #summary section.public .span4:first-of-type:before, .class #summary section.public .span6:first-of-type:before, h3.public:before { font-family: FontAwesome; content: "\f046"; color: green; display: inline-block; width: 1.2em; } .class #summary section .span4:first-of-type, .class #summary section .span6:first-of-type { padding-left: 21px; } .class #summary section .span4:first-of-type:before, .class #summary section .span6:first-of-type:before { margin-left: -21px; } .class #summary section.protected .span4:first-of-type:before, .class #summary section.protected .span6:first-of-type:before, h3.protected:before { font-family: FontAwesome; content: "\f132"; color: orange; display: inline-block; width: 1.2em; } .class #summary section.private .span4:first-of-type:before, .class #summary section.private .span6:first-of-type:before, h3.private:before { font-family: FontAwesome; content: "\f023"; color: red; display: inline-block; width: 1.2em; } .class #summary section em { font-size: 0.9em; color: silver; } .class #summary .inherited { color: gray; font-style: italic; } .accordion-group { border: none; } .accordion { margin-bottom: 0; } .accordion a:hover { text-decoration: none; background: #333333; color: #eeeeee; } .accordion-heading .accordion-toggle:before { content: "\f078"; font-family: FontAwesome; margin-right: 5px; } .accordion-heading .accordion-toggle.collapsed:before { content: "\f054"; } .accordion-heading .accordion-toggle { float: left; width: 16px; height: 16px; padding: 4px 2px 4px 12px; } .accordion-heading a { display: block; padding: 4px 12px; } .accordion-inner a { display: block; padding: 4px 12px; } .accordion-inner > ul a:before { font-family: 'phpdocumentor-clean-icons'; content: "\e001"; margin-right: 5px; } .accordion-inner li.class a:before { content: "\e002"; } .accordion-inner li.interface a:before { content: "\e001"; } .accordion-inner li.trait a:before { content: "\e000"; } .accordion-inner { padding: 4px 0 4px 12px; } .accordion-inner ul { list-style: none; padding: 0; margin: 0; } .row-fluid .span2 { width: 16.5%; } body .modal { width: 90%; /* desired relative width */ left: 5%; /* (100%-width)/2 */ /* place center */ margin-left:auto; margin-right:auto; } .side-nav.nav-list li a { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } @media (min-width: 767px) { .sidebar { position: fixed; top: 40px; bottom: 0; background-color: #f3f3f3; left: 0; border-right: 1px solid #e9e9e9; overflow-y: scroll; overflow-x: hidden; padding-top: 10px; } .sidebar::-webkit-scrollbar { width: 10px; } .sidebar::-webkit-scrollbar-thumb { background: #cccccc; background-clip: padding-box; border: 3px solid #f3f3f3; border-radius: 5px; } .sidebar::-webkit-scrollbar-button { display: none; } .sidebar::-webkit-scrollbar-track { background: #f3f3f3; } } @media (max-width: 979px) { body { padding-top: 0; } } @media (max-width: 767px) { .class #summary .heading { display: none; } .detailsbar h1 { display: none; } body { background-color: white; } footer.row-fluid, footer.row-fluid * { background-color: white; } .footer-sections .span4 h1 { color: #ccccd9; margin-top: 0; } .detailsbar { background-color: white; color: #333; border: none; } .row-fluid .span2 { width: 100%; } } @media (min-width: 767px) { .detailsbar { min-height: 100%; margin-bottom: -99999px; padding-bottom: 99999px; padding-left: 20px; padding-top: 10px; } } @media (min-width: 1200px) { .row-fluid .span2 { width: 16.5%; } } ================================================ FILE: docs/api/files/CAsciiArt.html ================================================ CImage API Documentaion

CAsciiArt.php

Classes

CAsciiArt Create an ASCII version of an image.
================================================ FILE: docs/api/files/CAsciiArt.php.txt ================================================ "#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: docs/api/files/CHttpGet.html ================================================ CImage API Documentaion

CHttpGet.php

Classes

CHttpGet Get a image from a remote server using HTTP GET and If-Modified-Since.
================================================ FILE: docs/api/files/CHttpGet.php.txt ================================================ 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'] : 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 ================================================ CImage API Documentaion

CImage.php

Classes

CImage Resize and crop images on the fly, store generated images in a cache.
================================================ FILE: docs/api/files/CImage.php.txt ================================================ 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; /** * 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; private $pngFilterCmd; /** * Path to command for deflate optimize, for example pngout or null. */ 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; /** * 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; /* * 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(); /* * 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); } /** * 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 $pattern to use to detect if its a remote file. * * @return $this */ public function setRemoteDownload($allow, $pattern = null) { $this->allowRemote = $allow; $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'); 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 = null) { $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(); $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 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, // Output format 'outputFormat' => null, 'dpr' => 1, ); // 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->fileType, $this->attr) = getimagesize($file); if (empty($info)) { throw new Exception("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: " . image_type_to_mime_type($this->fileType)); } 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)."); } 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->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. * * @return $this */ public function generateFilename($base = null, $useSubdir = true) { $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; $saveAs = $this->normalizeFileExtension(); $saveAs = $saveAs ? "_$saveAs" : null; $copyStrat = null; if ($this->copyStrategy === self::RESIZE) { $copyStrat = "_rs"; } $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; $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 = $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 . $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($this->pathToImage); $this->image = imagecreatefromstring(file_get_contents($this->pathToImage)); 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."); $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $posX = 0; $posY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); } 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->fillWidth - $this->width) / 2); $posY = round(($this->fillHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } 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); } //$this->log("posX=$posX, posY=$posY, cropX=$cropX, cropY=$cropY."); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); $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 (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); } else { 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; } 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': $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 '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 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; } /** * 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 putputting 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 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); $gmdate = gmdate("D, d M Y H:i:s", $lastModified); if (!$this->verbose) { header('Last-Modified: ' . $gmdate . " GMT"); } foreach($this->HTTPHeader as $key => $val) { header("$key: $val"); } 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"); } else { // Get details on image $info = getimagesize($file); !empty($info) or $this->raiseError("The file doesn't seem to be an image."); $mime = $info['mime']; $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("Content-length: $size"); 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'] = image_type_to_mime_type($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("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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } if (!is_null($this->verboseFileName)) { file_put_contents( $this->verboseFileName, str_replace("
", "\n", $log) ); } else { echo <<CImage Verbose Output
{$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 ================================================ CImage API Documentaion

CRemoteImage.php

Classes

CRemoteImage Get a image from a remote server using HTTP GET and If-Modified-Since.
================================================ FILE: docs/api/files/CRemoteImage.php.txt ================================================ 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; } /** * Set header fields. * * @return $this */ public function setHeaderFields() { $this->http->setHeader("User-Agent", "CImage/0.7.2 (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: docs/api/files/CWhitelist.html ================================================ CImage API Documentaion

CWhitelist.php

Classes

CWhitelist Act as whitelist (or blacklist).
================================================ FILE: docs/api/files/CWhitelist.php.txt ================================================ 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: docs/api/files/autoload.html ================================================ CImage API Documentaion

autoload.php

Autoloader for CImage and related class files.

================================================ FILE: docs/api/files/autoload.php.txt ================================================ setRemoteDownload(true); $res = $img->isRemoteSource($source); $this->assertTrue($res, "Should be a valid remote source: '$source'."); } /** * Test * * @return void * * @dataProvider providerInvalidRemoteSource */ public function testAllowRemoteDownloadDefaultPatternInvalid($source) { $img = new CImage(); $img->setRemoteDownload(true); $res = $img->isRemoteSource($source); $this->assertFalse($res, "Should not be a valid remote source: '$source'."); } /** * Provider for hostname matching the whitelist. * * @return array */ public function providerHostnameMatch() { return [ [ "any.facebook.com", "images.ak.instagram.com", "google.com", ], ]; } /** * Test * * @param string $hostname matches the whitelist * * @return void * * @dataProvider providerHostnameMatch * */ public function testRemoteHostWhitelistMatch($hostname) { $img = new CImage(); $img->setRemoteHostWhitelist($this->remote_whitelist); $res = $img->isRemoteSourceOnWhitelist("http://$hostname/img.jpg"); $this->assertTrue($res, "Should be a valid hostname on the whitelist: '$hostname'."); } /** * Provider for hostname not matching the whitelist. * * @return array */ public function providerHostnameNoMatch() { return [ [ "example.com", ".com", "img.jpg", ], ]; } /** * Test * * @param string $hostname not matching the whitelist * * @return void * * @dataProvider providerHostnameNoMatch * */ public function testRemoteHostWhitelistNoMatch($hostname) { $img = new CImage(); $img->setRemoteHostWhitelist($this->remote_whitelist); $res = $img->isRemoteSourceOnWhitelist("http://$hostname/img.jpg"); $this->assertFalse($res, "Should not be a valid hostname on the whitelist: '$hostname'."); } } ================================================ FILE: docs/api/files/test%2FCWhitelistTest.php.txt ================================================ set($this->remote_whitelist); $res = $whitelist->check($hostname); $this->assertTrue($res, "Should be a valid hostname on the whitelist: '$hostname'."); } /** * Test * * @param string $hostname not matching the whitelist * * @return void * * @dataProvider providerHostnameNoMatch * */ public function testRemoteHostWhitelistNoMatch($hostname) { $whitelist = new CWhitelist(); $whitelist->set($this->remote_whitelist); $res = $whitelist->check($hostname); $this->assertFalse($res, "Should not be a valid hostname on the whitelist: '$hostname'."); } } ================================================ FILE: docs/api/files/test%2Fconfig.php.txt ================================================ CImage API Documentaion

testCImage_RemoteDownloadTest.php

Classes

CImage_RemoteDownloadTest A testclass
================================================ FILE: docs/api/files/test.CWhitelistTest.html ================================================ CImage API Documentaion

testCWhitelistTest.php

Classes

CWhitelistTest A testclass
================================================ FILE: docs/api/files/test.config.html ================================================ CImage API Documentaion

testconfig.php

================================================ FILE: docs/api/files/webroot/img.php.txt ================================================ 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, $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.
"; $no = extension_loaded('curl') ? null : 'NOT'; $text .= "Extension curl is $no loaded.
"; $no = extension_loaded('gd') ? null : 'NOT'; $text .= "Extension gd is $no loaded.
"; if (!$no) { $text .= print_r(gd_info(), 1); } echo << CImage status
$text
EOD; 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 << CImage verbose output $url1




EOD;
}



/**
 * Load, process and output the image
 */
$img->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();



================================================
FILE: docs/api/files/webroot/img_config.php.txt
================================================
 'production', // 'development', 'strict'



    /**
     * Where are the sources for the classfiles.
     *
     * Default values:
     *  autoloader:  null     // used from v0.6.2
     *  cimage_class: null    // used until v0.6.1
     */
    'autoloader'   =>  __DIR__ . '/../autoload.php',
    //'cimage_class' =>  __DIR__ . '/../CImage.php',



    /**
     * Paths, where are the images stored and where is the cache.
     * End all paths with a slash.
     *
     * Default values:
     *  image_path: __DIR__ . '/img/'
     *  cache_path: __DIR__ . '/../cache/'
     *  alias_path: null
     */
    'image_path'   =>  __DIR__ . '/img/',
    'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',



    /**
     * Use password to protect from missusage, send &pwd=... or &password=..
     * with the request to match the password or set to false to disable.
     * Passwords are only used together with options for remote download
     * and aliasing.
     *
     * Create a passwords like this, depending on the type used:
     *  text: 'my_password'
     *  md5:  md5('my_password')
     *  hash: password_hash('my_password', PASSWORD_DEFAULT)
     *
     * Default values.
     *  password_always: false  // do not always require password,
     *  password:        false  // as in do not use password
     *  password_type:   'text' // use plain password, not encoded,
     */
    //'password_always' => false, // always require password,
    //'password'        => false, // "secret-password",
    //'password_type'   => 'text', // supports 'text', 'md5', 'hash',



    /**
     * Allow or disallow downloading of remote images available on
     * remote servers. Default is to disallow remote download.
     *
     * When enabling remote download, the default is to allow download any
     * link starting with http or https. This can be changed using
     * remote_pattern.
     *
     * When enabling remote_whitelist a check is made that the hostname of the
     * source to download matches the whitelist. By default the check is
     * disabled and thereby allowing download from any hosts.
     *
     * Default values.
     *  remote_allow:     false
     *  remote_pattern:   null  // use default values from CImage which is to
     *                          // allow download from any http- and
     *                          // https-source.
     *  remote_whitelist: null  // use default values from CImage which is to
     *                          // allow download from any hosts.
     */
    //'remote_allow'     => true,
    //'remote_pattern'   => '#^https?://#',
    //'remote_whitelist' => array(
    //    '\.facebook\.com$',
    //    '^(?:images|photos-[a-z])\.ak\.instagram\.com$',
    //    '\.google\.com$'
    //),



    /**
     * A regexp for validating characters in the image or alias filename.
     *
     * Default value:
     *  valid_filename:  '#^[a-z0-9A-Z-/_ \.:]+$#'
     *  valid_aliasname: '#^[a-z0-9A-Z-_]+$#'
     */
     //'valid_filename'  => '#^[a-z0-9A-Z-/_ \.:]+$#',
     //'valid_aliasname' => '#^[a-z0-9A-Z-_]+$#',



     /**
      * Change the default values for CImage quality and compression used
      * when saving images.
      *
      * Default value:
      *  jpg_quality:     null, integer between 0-100
      *  png_compression: null, integer between 0-9
      */
      //'jpg_quality'  => 75,
      //'png_compression' => 1,



      /**
       * A function (hook) can be called after img.php has processed all
       * configuration options and before processing the image using CImage.
       * The function receives the $img variabel and an array with the
       * majority of current settings.
       *
       * Default value:
       *  hook_before_CImage:     null
       */
       /*'hook_before_CImage' => function (CImage $img, Array $allConfig) {
           if ($allConfig['newWidth'] > 10) {
               $allConfig['newWidth'] *= 2;
           }
           return $allConfig;
       },*/



       /**
        * Add header for cache control when outputting images.
        *
        * Default value:
        *  cache_control: null, or set to string
        */
        //'cache_control' => "max-age=86400",



     /**
      * The name representing a dummy image which is automatically created
      * and stored at the defined path. The dummy image can then be used
      * inplace of an original image as a placeholder.
      * The dummy_dir must be writable and it defaults to a subdir of the
      * cache directory.
      * Write protect the dummy_dir to prevent creation of new dummy images,
      * but continue to use the existing ones.
      *
      * Default value:
      *  dummy_enabled:  true as default, disable dummy feature by setting
      *                  to false.
      *  dummy_filename: 'dummy' use this as ?src=dummy to create a dummy image.
      *  dummy_dir:      Defaults to subdirectory of 'cache_path',
      *                  named the same as 'dummy_filename'
      */
      //'dummy_enabled' => true,
      //'dummy_filename' => 'dummy',
      //'dummy_dir' => 'some writable directory',



     /**
     * Check that the imagefile is a file below 'image_path' using realpath().
     * Security constraint to avoid reaching images outside image_path.
     * This means that symbolic links to images outside the image_path will fail.
     *
     * Default value:
     *  image_path_constraint: true
     */
     //'image_path_constraint' => false,



     /**
     * Set default timezone.
     *
     * Default values.
     *  default_timezone: ini_get('default_timezone') or 'UTC'
     */
    //'default_timezone' => 'UTC',



    /**
     * Max image dimensions, larger dimensions results in 404.
     * This is basically a security constraint to avoid using resources on creating
     * large (unwanted) images.
     *
     * Default values.
     *  max_width:  2000
     *  max_height: 2000
     */
    //'max_width'     => 2000,
    //'max_height'    => 2000,



    /**
     * Set default background color for all images. Override it using
     * option bgColor.
     * Colorvalue is 6 digit hex string between 000000-FFFFFF
     * or 8 digit hex string if using the alpha channel where
     * the alpha value is between 00 (opaqe) and 7F (transparent),
     * that is between 00000000-FFFFFF7F.
     *
     * Default values.
     *  background_color: As specified by CImage
     */
    //'background_color' => "FFFFFF",
    //'background_color' => "FFFFFF7F",



    /**
     * Post processing of images using external tools, set to true or false
     * and set command to be executed.
     *
     * Default values.
     *
     *  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'
     */
    /*
    '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',
    ),
    */



    /**
     * Create custom convolution expressions, matrix 3x3, divisor and
     * offset.
     *
     * Default values.
     *  convolution_constant: array()
     */
    /*
    'convolution_constant' => array(
        //'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',
    ),
    */



    /**
     * Prevent leeching of images by controlling the hostname of those who
     * can access the images. Default is to allow hotlinking.
     *
     * Password apply when hotlinking is disallowed, use password to allow
     * hotlinking.
     *
     * The whitelist is an array of regexpes for allowed hostnames that can
     * hotlink images.
     *
     * Default values.
     *  allow_hotlinking:     true
     *  hotlinking_whitelist: array()
     */
     /*
    'allow_hotlinking' => false,
    'hotlinking_whitelist' => array(
        '^dbwebb\.se$',
    ),
    */


    /**
     * Create custom shortcuts for more advanced expressions.
     *
     * Default values.
     *  shortcut: array(
     *      'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
     *  )
     */
     /*
    'shortcut' => array(
        'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
    ),*/



    /**
     * Predefined size constants.
     *
     * These can be used together with &width or &height to create a constant value
     * for a width or height where can be changed in one place.
     * Useful when your site changes its layout or if you have a grid to fit images into.
     *
     * Example:
     *  &width=w1  // results in width=613
     *  &width=c2  // results in spanning two columns with a gutter, 30*2+10=70
     *  &width=c24 // results in spanning whole grid 24*30+((24-1)*10)=950
     *
     * Default values.
     *  size_constant: As specified by the function below.
     */
    /*
    '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;
    },*/



    /**
     * Predefined aspect ratios.
     *
     * Default values.
     *  aspect_ratio_constant: As the function below.
     */
    /*'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,
        );
    },*/



    /**
     * default options for ascii image.
     *
     * Default values as specified below in the array.
     *  ascii-options:
     *   characterSet:       Choose any character set available in CAsciiArt.
     *   scale:              How many pixels should each character
     *                       translate to.
     *   luminanceStrategy:  Choose any strategy available in CAsciiArt.
     *   customCharacterSet: Define your own character set.
     */
    /*'ascii-options' => array(
            "characterSet" => 'two',
            "scale" => 14,
            "luminanceStrategy" => 3,
            "customCharacterSet" => null,
        );
    },*/
);



================================================
FILE: docs/api/files/webroot%2Fcheck_system.php.txt
================================================

'; echo 'Running on: ' . $_SERVER['SERVER_SOFTWARE'] . '

'; $no = extension_loaded('gd') ? null : 'NOT'; echo "Extension gd is $no loaded.
"; $no = extension_loaded('exif') ? null : 'NOT'; echo "Extension exif is $no loaded.
"; if (!$no) { echo "
", var_dump(gd_info()), "
"; } ================================================ FILE: docs/api/files/webroot%2Fcompare%2Fcompare-test.php.txt ================================================

Compare images

Add link to images and visually compare them. Change the link och press return to load the image. Read more...






Image 1

    
Image 2

    
Image 3

    
Image 4

    
================================================ FILE: docs/api/files/webroot%2Fimg.php.txt ================================================ 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); $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




EOD;
}



/**
 * Get the cachepath from config.
 */
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');



/**
 * Load, process and output the image
 */
$img->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();



================================================
FILE: docs/api/files/webroot%2Fimg_config.php.txt
================================================
 'development',
    //'mode' => 'production', // 'development', 'strict'



    /**
     * Where are the sources for the classfiles.
     *
     * Default values:
     *  autoloader:  null     // used from v0.6.2
     *  cimage_class: null    // used until v0.6.1
     */
    'autoloader'   =>  __DIR__ . '/../autoload.php',
    //'cimage_class' =>  __DIR__ . '/../CImage.php',



    /**
     * Paths, where are the images stored and where is the cache.
     * End all paths with a slash.
     *
     * Default values:
     *  image_path: __DIR__ . '/img/'
     *  cache_path: __DIR__ . '/../cache/'
     *  alias_path: null
     */
    'image_path'   =>  __DIR__ . '/img/',
    'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',



    /**
    * Use password to protect from missusage, send &pwd=... or &password=..
    * with the request to match the password or set to false to disable.
    * Passwords are only used together with the options for remote download
    * and aliasing.
    *
    * Default values.
    *  password:        false // as in do not use password
    *  password_always: false // do not always require password,
    */
    //'password'        => false, // "secret-password",
    //'password_always' => false, // always require password,



    /**
     * Allow or disallow downloading of remote images available on
     * remote servers. Default is to disallow remote download. 
     * 
     * When enabling remote download, the default is to allow download any
     * link starting with http or https. This can be changed using 
     * remote_pattern. 
     *
     * When enabling remote_whitelist a check is made that the hostname of the 
     * source to download matches the whitelist. By default the check is 
     * disabled and thereby allowing download from any hosts.
     *
     * Default values.
     *  remote_allow:     false
     *  remote_pattern:   null  // use default values from CImage which is to
     *                          // allow download from any http- and 
     *                          // https-source.
     *  remote_whitelist: null  // use default values from CImage which is to 
     *                          // allow download from any hosts.
     */
    //'remote_allow'     => true,
    //'remote_pattern'   => '#^https?://#',
    //'remote_whitelist' => array(
    //    '\.facebook\.com$',
    //    '^(?:images|photos-[a-z])\.ak\.instagram\.com$',
    //    '\.google\.com$'
    //),



    /**
     * A regexp for validating characters in the image or alias filename.
     *
     * Default value:
     *  valid_filename:  '#^[a-z0-9A-Z-/_\.:]+$#'
     *  valid_aliasname: '#^[a-z0-9A-Z-_]+$#'
     */
     //'valid_filename'  => '#^[a-z0-9A-Z-/_\.:]+$#',
     //'valid_aliasname' => '#^[a-z0-9A-Z-_]+$#',



     /**
     * Check that the imagefile is a file below 'image_path' using realpath().
     * Security constraint to avoid reaching images outside image_path.
     * This means that symbolic links to images outside the image_path will fail.
     *
     * Default value:
     *  image_path_constraint: true
     */
     //'image_path_constraint' => false,



     /**
     * Set default timezone.
     *
     * Default values.
     *  default_timezone: ini_get('default_timezone') or 'UTC'
     */
    //'default_timezone' => 'UTC',



    /**
     * Max image dimensions, larger dimensions results in 404.
     * This is basically a security constraint to avoid using resources on creating
     * large (unwanted) images.
     *
     * Default values.
     *  max_width:  2000
     *  max_height: 2000
     */
    //'max_width'     => 2000,
    //'max_height'    => 2000,



    /**
     * Set default background color for all images. Override it using
     * option bgColor.
     * Colorvalue is 6 digit hex string between 000000-FFFFFF
     * or 8 digit hex string if using the alpha channel where
     * the alpha value is between 00 (opaqe) and 7F (transparent),
     * that is between 00000000-FFFFFF7F.
     *
     * Default values.
     *  background_color: As specified by CImage
     */
    //'background_color' => "FFFFFF",
    //'background_color' => "FFFFFF7F",



    /**
     * Post processing of images using external tools, set to true or false
     * and set command to be executed.
     *
     * Default values.
     *
     *  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'
     */
    /*
    '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',
    ),
    */



    /**
     * Create custom convolution expressions, matrix 3x3, divisor and
     * offset.
     *
     * Default values.
     *  convolution_constant: array()
     */
    /*
    'convolution_constant' => array(
        //'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',
    ),
    */



    /**
     * Prevent leeching of images by controlling who can access them from where.
     * Default it to allow hotlinking.
     * Password apply when hotlinking is disallowed, use password to allow.
     * The whitelist is an array of regexpes for allowed hostnames that can
     * hotlink images.
     *
     * Default values.
     *  allow_hotlinking:     true
     *  hotlinking_whitelist: array()
     */
     /*
    'allow_hotlinking' => false,
    'hotlinking_whitelist' => array(
        '#^localhost$#',
        '#^dbwebb\.se$#',
    ),
    */



    /**
     * Create custom shortcuts for more advanced expressions.
     *
     * Default values.
     *  shortcut: array(
     *      'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
     *  )
     */
     /*
    'shortcut' => array(
        'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
    ),*/



    /**
     * Predefined size constants.
     *
     * These can be used together with &width or &height to create a constant value
     * for a width or height where can be changed in one place.
     * Useful when your site changes its layout or if you have a grid to fit images into.
     *
     * Example:
     *  &width=w1  // results in width=613
     *  &width=c2  // results in spanning two columns with a gutter, 30*2+10=70
     *  &width=c24 // results in spanning whole grid 24*30+((24-1)*10)=950
     *
     * Default values.
     *  size_constant: As specified by the function below.
     */
    /*
    '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;
    },*/



    /**
     * Predefined aspect ratios.
     *
     * Default values.
     *  aspect_ratio_constant: As the function below.
     */
    /*'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,
        );
    },*/
);



================================================
FILE: docs/api/files/webroot%2Fimg_header.php.txt
================================================
 'production',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);



================================================
FILE: docs/api/files/webroot%2Fimgd.php.txt
================================================
 'development',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);



/**
 * Get a image from a remote server using HTTP GET and If-Modified-Since.
 *
 */
class CHttpGet
{
    private $request  = array();
    private $response = array();



    /**
    * Constructor
    *
    */
    public function __construct()
    {
        $this->request['header'] = array();
    }



    /**
     * Set the url for the request.
     *
     * @param string $url
     *
     * @return $this
     */
    public function setUrl($url)
    {
        $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"));
        $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.
     *
     * @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,
        );

        $ch = curl_init();
        curl_setopt_array($ch, $options);
        $response = curl_exec($ch);

        if (!$response) {
            return false;
        }

        $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'] : 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.
"); switch ($this->fileExtension) { case 'jpg': case 'jpeg': $this->image = imagecreatefromjpeg($this->pathToImage); break; case 'gif': $this->image = imagecreatefromgif($this->pathToImage); break; case 'png': $this->image = imagecreatefrompng($this->pathToImage); break; } exit(); } /** * Load image from disk. * * @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->log("Opening file as {$this->fileExtension}."); switch ($this->fileExtension) { case 'jpg': case 'jpeg': $this->image = @imagecreatefromjpeg($this->pathToImage); $this->image or $this->failedToLoad(); break; case 'gif': $this->image = @imagecreatefromgif($this->pathToImage); $this->image or $this->failedToLoad(); break; case 'png': $this->image = @imagecreatefrompng($this->pathToImage); $this->image or $this->failedToLoad(); $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; } break; default: $this->image = false; throw new Exception('No support for this file extension.'); } if ($this->verbose) { $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. * * @return int as the type of the png-image * */ private function getPngType() { $pngType = ord(file_get_contents($this->pathToImage, false, null, 25, 1)); switch ($pngType) { case self::PNG_GREYSCALE: $this->log("PNG is type 0, Greyscale."); break; case self::PNG_RGB: $this->log("PNG is type 2, RGB"); break; case self::PNG_RGB_PALETTE: $this->log("PNG is type 3, RGB with palette"); break; case self::PNG_GREYSCALE_ALPHA: $this->Log("PNG is type 4, Greyscale with alpha channel"); break; case self::PNG_RGB_ALPHA: $this->Log("PNG is type 6, RGB with alpha channel (PNG 32-bit)"); break; default: $this->Log("PNG is UNKNOWN type, is it really a PNG image?"); } return $pngType; } /** * 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 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."); $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $posX = 0; $posY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); } 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); 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; } else if ($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->fillWidth - $this->width) / 2); $posY = round(($this->fillHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); 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; } else if (!($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); } else if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } else if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } //$this->log("posX=$posX, posY=$posY, cropX=$cropX, cropY=$cropY."); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); 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->fileExtension, array('jpg', 'jpeg'))) { $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 = imagecolortransparent($this->image); 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 (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; } /** * Save image. * * @param string $src as target filename. * @param string $base as base directory where to store images. * * @return $this or false if no folder is set. */ public function save($src = null, $base = null) { if (isset($src)) { $this->setTarget($src, $base); } is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); switch(strtolower($this->extension)) { case 'jpeg': case 'jpg': $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 'png': $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 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; default: $this->RaiseError('No support for this file extension.'); break; } if ($this->verbose) { clearstatcache(); $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; } /** * 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; } $alias = $alias . "." . $this->extension; 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; } /** * 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 format is: $format"); if (!$this->verbose && $format == 'json') { header('Content-type: application/json'); echo $this->json($file); exit; } $this->log("Outputting image: $file"); // Get image modification time clearstatcache(); $lastModified = filemtime($file); $gmdate = gmdate("D, d M Y H:i:s", $lastModified); if (!$this->verbose) { header('Last-Modified: ' . $gmdate . " GMT"); } 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"); } else { if ($this->verbose) { $this->log("Last modified: " . $gmdate . " GMT"); $this->verboseOutput(); exit; } // Get details on image $info = getimagesize($file); !empty($info) or $this->raiseError("The file doesn't seem to be an image."); $mime = $info['mime']; header('Content-type: ' . $mime); 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->loadImageDetails($file); $details['filename'] = basename($file); $details['width'] = $this->width; $details['height'] = $this->height; $details['aspectRatio'] = round($this->width / $this->height, 3); $details['size'] = filesize($file); $this->load($file); $details['colors'] = $this->colorsTotal($this->image); $options = null; if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; } return json_encode($details, $options); } /** * 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 and print out the log and the actual images. * * @return void */ private function verboseOutput() { $log = null; $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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } echo << CImage verbose output

CImage Verbose Output

{$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




EOD;
}



/**
 * Get the cachepath from config.
 */
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');



/**
 * Load, process and output the image
 */
$img->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();






================================================
FILE: docs/api/files/webroot%2Fimgp.php.txt
================================================
 'production',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);



/**
 * Get a image from a remote server using HTTP GET and If-Modified-Since.
 *
 */
class CHttpGet
{
    private $request  = array();
    private $response = array();



    /**
    * Constructor
    *
    */
    public function __construct()
    {
        $this->request['header'] = array();
    }



    /**
     * Set the url for the request.
     *
     * @param string $url
     *
     * @return $this
     */
    public function setUrl($url)
    {
        $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"));
        $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.
     *
     * @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,
        );

        $ch = curl_init();
        curl_setopt_array($ch, $options);
        $response = curl_exec($ch);

        if (!$response) {
            return false;
        }

        $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'] : 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.
"); switch ($this->fileExtension) { case 'jpg': case 'jpeg': $this->image = imagecreatefromjpeg($this->pathToImage); break; case 'gif': $this->image = imagecreatefromgif($this->pathToImage); break; case 'png': $this->image = imagecreatefrompng($this->pathToImage); break; } exit(); } /** * Load image from disk. * * @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->log("Opening file as {$this->fileExtension}."); switch ($this->fileExtension) { case 'jpg': case 'jpeg': $this->image = @imagecreatefromjpeg($this->pathToImage); $this->image or $this->failedToLoad(); break; case 'gif': $this->image = @imagecreatefromgif($this->pathToImage); $this->image or $this->failedToLoad(); break; case 'png': $this->image = @imagecreatefrompng($this->pathToImage); $this->image or $this->failedToLoad(); $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; } break; default: $this->image = false; throw new Exception('No support for this file extension.'); } if ($this->verbose) { $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. * * @return int as the type of the png-image * */ private function getPngType() { $pngType = ord(file_get_contents($this->pathToImage, false, null, 25, 1)); switch ($pngType) { case self::PNG_GREYSCALE: $this->log("PNG is type 0, Greyscale."); break; case self::PNG_RGB: $this->log("PNG is type 2, RGB"); break; case self::PNG_RGB_PALETTE: $this->log("PNG is type 3, RGB with palette"); break; case self::PNG_GREYSCALE_ALPHA: $this->Log("PNG is type 4, Greyscale with alpha channel"); break; case self::PNG_RGB_ALPHA: $this->Log("PNG is type 6, RGB with alpha channel (PNG 32-bit)"); break; default: $this->Log("PNG is UNKNOWN type, is it really a PNG image?"); } return $pngType; } /** * 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 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."); $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $posX = 0; $posY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); } 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); 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; } else if ($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->fillWidth - $this->width) / 2); $posY = round(($this->fillHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); 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; } else if (!($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); } else if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } else if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } //$this->log("posX=$posX, posY=$posY, cropX=$cropX, cropY=$cropY."); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); 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->fileExtension, array('jpg', 'jpeg'))) { $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 = imagecolortransparent($this->image); 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 (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; } /** * Save image. * * @param string $src as target filename. * @param string $base as base directory where to store images. * * @return $this or false if no folder is set. */ public function save($src = null, $base = null) { if (isset($src)) { $this->setTarget($src, $base); } is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); switch(strtolower($this->extension)) { case 'jpeg': case 'jpg': $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 'png': $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 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; default: $this->RaiseError('No support for this file extension.'); break; } if ($this->verbose) { clearstatcache(); $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; } /** * 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; } $alias = $alias . "." . $this->extension; 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; } /** * 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 format is: $format"); if (!$this->verbose && $format == 'json') { header('Content-type: application/json'); echo $this->json($file); exit; } $this->log("Outputting image: $file"); // Get image modification time clearstatcache(); $lastModified = filemtime($file); $gmdate = gmdate("D, d M Y H:i:s", $lastModified); if (!$this->verbose) { header('Last-Modified: ' . $gmdate . " GMT"); } 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"); } else { if ($this->verbose) { $this->log("Last modified: " . $gmdate . " GMT"); $this->verboseOutput(); exit; } // Get details on image $info = getimagesize($file); !empty($info) or $this->raiseError("The file doesn't seem to be an image."); $mime = $info['mime']; header('Content-type: ' . $mime); 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->loadImageDetails($file); $details['filename'] = basename($file); $details['width'] = $this->width; $details['height'] = $this->height; $details['aspectRatio'] = round($this->width / $this->height, 3); $details['size'] = filesize($file); $this->load($file); $details['colors'] = $this->colorsTotal($this->image); $options = null; if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; } return json_encode($details, $options); } /** * 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 and print out the log and the actual images. * * @return void */ private function verboseOutput() { $log = null; $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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } echo << CImage verbose output

CImage Verbose Output

{$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




EOD;
}



/**
 * Get the cachepath from config.
 */
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');



/**
 * Load, process and output the image
 */
$img->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();






================================================
FILE: docs/api/files/webroot%2Fimgs.php.txt
================================================
 'development',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);



/**
 * Get a image from a remote server using HTTP GET and If-Modified-Since.
 *
 */
class CHttpGet
{
    private $request  = array();
    private $response = array();



    /**
    * Constructor
    *
    */
    public function __construct()
    {
        $this->request['header'] = array();
    }



    /**
     * Set the url for the request.
     *
     * @param string $url
     *
     * @return $this
     */
    public function setUrl($url)
    {
        $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"));
        $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.
     *
     * @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,
        );

        $ch = curl_init();
        curl_setopt_array($ch, $options);
        $response = curl_exec($ch);

        if (!$response) {
            return false;
        }

        $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'] : 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.
"); switch ($this->fileExtension) { case 'jpg': case 'jpeg': $this->image = imagecreatefromjpeg($this->pathToImage); break; case 'gif': $this->image = imagecreatefromgif($this->pathToImage); break; case 'png': $this->image = imagecreatefrompng($this->pathToImage); break; } exit(); } /** * Load image from disk. * * @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->log("Opening file as {$this->fileExtension}."); switch ($this->fileExtension) { case 'jpg': case 'jpeg': $this->image = @imagecreatefromjpeg($this->pathToImage); $this->image or $this->failedToLoad(); break; case 'gif': $this->image = @imagecreatefromgif($this->pathToImage); $this->image or $this->failedToLoad(); break; case 'png': $this->image = @imagecreatefrompng($this->pathToImage); $this->image or $this->failedToLoad(); $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; } break; default: $this->image = false; throw new Exception('No support for this file extension.'); } if ($this->verbose) { $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. * * @return int as the type of the png-image * */ private function getPngType() { $pngType = ord(file_get_contents($this->pathToImage, false, null, 25, 1)); switch ($pngType) { case self::PNG_GREYSCALE: $this->log("PNG is type 0, Greyscale."); break; case self::PNG_RGB: $this->log("PNG is type 2, RGB"); break; case self::PNG_RGB_PALETTE: $this->log("PNG is type 3, RGB with palette"); break; case self::PNG_GREYSCALE_ALPHA: $this->Log("PNG is type 4, Greyscale with alpha channel"); break; case self::PNG_RGB_ALPHA: $this->Log("PNG is type 6, RGB with alpha channel (PNG 32-bit)"); break; default: $this->Log("PNG is UNKNOWN type, is it really a PNG image?"); } return $pngType; } /** * 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 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."); $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $posX = 0; $posY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); } 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); 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; } else if ($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->fillWidth - $this->width) / 2); $posY = round(($this->fillHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); 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; } else if (!($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); } else if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } else if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } //$this->log("posX=$posX, posY=$posY, cropX=$cropX, cropY=$cropY."); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->newWidth, $this->newHeight); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); 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->fileExtension, array('jpg', 'jpeg'))) { $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 = imagecolortransparent($this->image); 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 (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; } /** * Save image. * * @param string $src as target filename. * @param string $base as base directory where to store images. * * @return $this or false if no folder is set. */ public function save($src = null, $base = null) { if (isset($src)) { $this->setTarget($src, $base); } is_writable($this->saveFolder) or $this->raiseError('Target directory is not writable.'); switch(strtolower($this->extension)) { case 'jpeg': case 'jpg': $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 'png': $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 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; default: $this->RaiseError('No support for this file extension.'); break; } if ($this->verbose) { clearstatcache(); $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; } /** * 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; } $alias = $alias . "." . $this->extension; 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; } /** * 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 format is: $format"); if (!$this->verbose && $format == 'json') { header('Content-type: application/json'); echo $this->json($file); exit; } $this->log("Outputting image: $file"); // Get image modification time clearstatcache(); $lastModified = filemtime($file); $gmdate = gmdate("D, d M Y H:i:s", $lastModified); if (!$this->verbose) { header('Last-Modified: ' . $gmdate . " GMT"); } 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"); } else { if ($this->verbose) { $this->log("Last modified: " . $gmdate . " GMT"); $this->verboseOutput(); exit; } // Get details on image $info = getimagesize($file); !empty($info) or $this->raiseError("The file doesn't seem to be an image."); $mime = $info['mime']; header('Content-type: ' . $mime); 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->loadImageDetails($file); $details['filename'] = basename($file); $details['width'] = $this->width; $details['height'] = $this->height; $details['aspectRatio'] = round($this->width / $this->height, 3); $details['size'] = filesize($file); $this->load($file); $details['colors'] = $this->colorsTotal($this->image); $options = null; if (defined("JSON_PRETTY_PRINT") && defined("JSON_UNESCAPED_SLASHES")) { $options = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; } return json_encode($details, $options); } /** * 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 and print out the log and the actual images. * * @return void */ private function verboseOutput() { $log = null; $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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } echo << CImage verbose output

CImage Verbose Output

{$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




EOD;
}



/**
 * Get the cachepath from config.
 */
$cachePath = getConfig('cache_path', __DIR__ . '/../cache/');



/**
 * Load, process and output the image
 */
$img->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();






================================================
FILE: docs/api/files/webroot%2Ftest%2Fconfig.php.txt
================================================


  
  <?=$title?>
  
  





Images used in test

The following images are used for this test.

(json) (verbose)








Testcases used for each image

The following testcases are used for each image.


For each image, apply all testcases

. Using source image

(json) (verbose)





Testcase :

(json) (verbose)









================================================
FILE: docs/api/files/webroot%2Ftest%2Ftest.php.txt
================================================


  
  Testing img resizing using CImage.php


Testing CImage.php through img.php

Testcases

'Original image', 'query'=>''), array('text'=>'Crop out a rectangle of 100x100, start by position 200x200.', 'query'=>'&crop=100,100,200,200'), array('text'=>'Crop out a full width rectangle with height of 200, start by position 0x100.', 'query'=>'&crop=0,200,0,100'), array('text'=>'Max width 200.', 'query'=>'&w=200'), array('text'=>'Max height 200.', 'query'=>'&h=200'), array('text'=>'Max width 200 and max height 200.', 'query'=>'&w=200&h=200'), array('text'=>'No-ratio makes image fit in area of width 200 and height 200.', 'query'=>'&w=200&h=200&no-ratio'), array('text'=>'Crop to fit in width 200 and height 200.', 'query'=>'&w=200&h=200&crop-to-fit'), array('text'=>'Crop to fit in width 200 and height 100.', 'query'=>'&w=200&h=100&crop-to-fit'), array('text'=>'Crop to fit in width 100 and height 200.', 'query'=>'&w=100&h=200&crop-to-fit'), array('text'=>'Quality 70', 'query'=>'&w=200&h=200&quality=70'), array('text'=>'Quality 40', 'query'=>'&w=200&h=200&quality=40'), array('text'=>'Quality 10', 'query'=>'&w=200&h=200&quality=10'), array('text'=>'Filter: Negate', 'query'=>'&w=200&h=200&f=negate'), array('text'=>'Filter: Grayscale', 'query'=>'&w=200&h=200&f=grayscale'), array('text'=>'Filter: Brightness 90', 'query'=>'&w=200&h=200&f=brightness,90'), array('text'=>'Filter: Contrast 50', 'query'=>'&w=200&h=200&f=contrast,50'), array('text'=>'Filter: Colorize 0,255,0,0', 'query'=>'&w=200&h=200&f=colorize,0,255,0,0'), array('text'=>'Filter: Edge detect', 'query'=>'&w=200&h=200&f=edgedetect'), array('text'=>'Filter: Emboss', 'query'=>'&w=200&h=200&f=emboss'), array('text'=>'Filter: Gaussian blur', 'query'=>'&w=200&h=200&f=gaussian_blur'), array('text'=>'Filter: Selective blur', 'query'=>'&w=200&h=200&f=selective_blur'), array('text'=>'Filter: Mean removal', 'query'=>'&w=200&h=200&f=mean_removal'), array('text'=>'Filter: Smooth 2', 'query'=>'&w=200&h=200&f=smooth,2'), array('text'=>'Filter: Pixelate 10,10', 'query'=>'&w=200&h=200&f=pixelate,10,10'), array('text'=>'Multiple filter: Negate, Grayscale and Pixelate 10,10', 'query'=>'&w=200&h=200&&f=negate&f0=grayscale&f1=pixelate,10,10'), array('text'=>'Crop with width & height and crop-to-fit with quality and filter', 'query'=>'&crop=100,100,100,100&w=200&h=200&crop-to-fit&q=70&f0=grayscale'), ); ?>

Test case with image wider.jpg

$val) { $url = "../img.php?src=wider.jpg{$val['query']}"; echo ""; } ?>
Test case with image wider.jpg
Testcase:Result:
$key
{$val['text']}
".htmlentities($url)."

Test case with image higher.jpg

$val) { $url = "../img.php?src=higher.jpg{$val['query']}"; echo ""; } ?>
Test case with image higher.jpg
Testcase:Result:
$key
{$val['text']}
".htmlentities($url)."
================================================ FILE: docs/api/files/webroot%2Ftest%2Ftest_issue29.php.txt ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Apply testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: docs/api/files/webroot%2Ftest%2Ftest_issue36_rb-ra-270.php.txt ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Apply testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: docs/api/files/webroot%2Ftest%2Ftest_issue36_rb-ra-45.php.txt ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Apply testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: docs/api/files/webroot%2Ftest%2Ftest_issue36_rb-ra-90.php.txt ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Applu testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: docs/api/files/webroot%2Ftest%2Ftest_issue38.php.txt ================================================ CImage API Documentaion

webrootcheck_system.php

================================================ FILE: docs/api/files/webroot.compare.compare-test.html ================================================ CImage API Documentaion

webroot/comparecompare-test.php

================================================ FILE: docs/api/files/webroot.compare.compare.html ================================================ CImage API Documentaion

webroot/comparecompare.php

================================================ FILE: docs/api/files/webroot.img.html ================================================ CImage API Documentaion

webrootimg.php

Resize and crop images on the fly, store generated images in a cache.

Functions

errorPage()

errorPage(string  $msg) : void

Display error message.

Parameters

string $msg

to display.

get()

get(mixed  $key, mixed  $default = null) : mixed

Get input from query string or return default value if not set.

Parameters

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.

Returns

mixed —

value from $_GET or default value.

getDefined()

getDefined(mixed  $key, mixed  $defined, mixed  $undefined) : mixed

Get input from query string and set to $defined if defined or else $undefined.

Parameters

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.

Returns

mixed —

value as $defined or $undefined.

getConfig()

getConfig(string  $key, mixed  $default) : mixed

Get value from config array or default if key is not set in config array.

Parameters

string $key

the key in the config array.

mixed $default

value to be default if $key is not set in config.

Returns

mixed —

value as $config[$key] or $default.

verbose()

verbose(string  $msg = null) : void

Log when verbose mode, when used without argument it returns the result.

Parameters

string $msg

to log.

================================================ FILE: docs/api/files/webroot.img_config.html ================================================ CImage API Documentaion

webrootimg_config.php

Configuration for img.php, name the config file the same as your img.php and append _config. If you are testing out some in imgtest.php then label that config-file imgtest_config.php.

================================================ FILE: docs/api/files/webroot.img_header.html ================================================ CImage API Documentaion

webrootimg_header.php

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.

================================================ FILE: docs/api/files/webroot.imgd.html ================================================ CImage API Documentaion

webrootimgd.php

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.

Classes

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.

Functions

errorPage()

errorPage(string  $msg) : void

Default configuration options, can be overridden in own config-file.

Parameters

string $msg

to display.

get()

get(mixed  $key, mixed  $default = null) : mixed

Get input from query string or return default value if not set.

Parameters

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.

Returns

mixed —

value from $_GET or default value.

getDefined()

getDefined(mixed  $key, mixed  $defined, mixed  $undefined) : mixed

Get input from query string and set to $defined if defined or else $undefined.

Parameters

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.

Returns

mixed —

value as $defined or $undefined.

getConfig()

getConfig(string  $key, mixed  $default) : mixed

Get value from config array or default if key is not set in config array.

Parameters

string $key

the key in the config array.

mixed $default

value to be default if $key is not set in config.

Returns

mixed —

value as $config[$key] or $default.

verbose()

verbose(string  $msg = null) : void

Log when verbose mode, when used without argument it returns the result.

Parameters

string $msg

to log.

================================================ FILE: docs/api/files/webroot.imgp.html ================================================ CImage API Documentaion

webrootimgp.php

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.

Classes

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.

Functions

errorPage()

errorPage(string  $msg) : void

Default configuration options, can be overridden in own config-file.

Parameters

string $msg

to display.

get()

get(mixed  $key, mixed  $default = null) : mixed

Get input from query string or return default value if not set.

Parameters

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.

Returns

mixed —

value from $_GET or default value.

getDefined()

getDefined(mixed  $key, mixed  $defined, mixed  $undefined) : mixed

Get input from query string and set to $defined if defined or else $undefined.

Parameters

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.

Returns

mixed —

value as $defined or $undefined.

getConfig()

getConfig(string  $key, mixed  $default) : mixed

Get value from config array or default if key is not set in config array.

Parameters

string $key

the key in the config array.

mixed $default

value to be default if $key is not set in config.

Returns

mixed —

value as $config[$key] or $default.

verbose()

verbose(string  $msg = null) : void

Log when verbose mode, when used without argument it returns the result.

Parameters

string $msg

to log.

================================================ FILE: docs/api/files/webroot.imgs.html ================================================ CImage API Documentaion

webrootimgs.php

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.

Classes

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.

Functions

errorPage()

errorPage(string  $msg) : void

Default configuration options, can be overridden in own config-file.

Parameters

string $msg

to display.

get()

get(mixed  $key, mixed  $default = null) : mixed

Get input from query string or return default value if not set.

Parameters

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.

Returns

mixed —

value from $_GET or default value.

getDefined()

getDefined(mixed  $key, mixed  $defined, mixed  $undefined) : mixed

Get input from query string and set to $defined if defined or else $undefined.

Parameters

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.

Returns

mixed —

value as $defined or $undefined.

getConfig()

getConfig(string  $key, mixed  $default) : mixed

Get value from config array or default if key is not set in config array.

Parameters

string $key

the key in the config array.

mixed $default

value to be default if $key is not set in config.

Returns

mixed —

value as $config[$key] or $default.

verbose()

verbose(string  $msg = null) : void

Log when verbose mode, when used without argument it returns the result.

Parameters

string $msg

to log.

================================================ FILE: docs/api/files/webroot.test.config.html ================================================ CImage API Documentaion

webroot/testconfig.php

================================================ FILE: docs/api/files/webroot.test.template.html ================================================ CImage API Documentaion

webroot/testtemplate.php

================================================ FILE: docs/api/files/webroot.test.test.html ================================================ CImage API Documentaion

webroot/testtest.php

================================================ FILE: docs/api/files/webroot.test.test_issue29.html ================================================ CImage API Documentaion

webroot/testtest_issue29.php

================================================ FILE: docs/api/files/webroot.test.test_issue36_aro.html ================================================ CImage API Documentaion

webroot/testtest_issue36_aro.php

================================================ FILE: docs/api/files/webroot.test.test_issue36_rb-ra-180.html ================================================ CImage API Documentaion

webroot/testtest_issue36_rb-ra-180.php

================================================ FILE: docs/api/files/webroot.test.test_issue36_rb-ra-270.html ================================================ CImage API Documentaion

webroot/testtest_issue36_rb-ra-270.php

================================================ FILE: docs/api/files/webroot.test.test_issue36_rb-ra-45.html ================================================ CImage API Documentaion

webroot/testtest_issue36_rb-ra-45.php

================================================ FILE: docs/api/files/webroot.test.test_issue36_rb-ra-90.html ================================================ CImage API Documentaion

webroot/testtest_issue36_rb-ra-90.php

================================================ FILE: docs/api/files/webroot.test.test_issue38.html ================================================ CImage API Documentaion

webroot/testtest_issue38.php

================================================ FILE: docs/api/files/webroot.test.test_issue40.html ================================================ CImage API Documentaion

webroot/testtest_issue40.php

================================================ FILE: docs/api/files/webroot.test.test_issue49.html ================================================ CImage API Documentaion

webroot/testtest_issue49.php

================================================ FILE: docs/api/files/webroot.test.test_issue52-cf.html ================================================ CImage API Documentaion

webroot/testtest_issue52-cf.php

================================================ FILE: docs/api/files/webroot.test.test_issue52-stretch.html ================================================ CImage API Documentaion

webroot/testtest_issue52-stretch.php

================================================ FILE: docs/api/files/webroot.test.test_issue52.html ================================================ CImage API Documentaion

webroot/testtest_issue52.php

================================================ FILE: docs/api/files/webroot.test.test_issue58.html ================================================ CImage API Documentaion

webroot/testtest_issue58.php

================================================ FILE: docs/api/files/webroot.test.test_issue60.html ================================================ CImage API Documentaion

webroot/testtest_issue60.php

================================================ FILE: docs/api/files/webroot.test.test_option-crop.html ================================================ CImage API Documentaion

webroot/testtest_option-crop.php

================================================ FILE: docs/api/files/webroot.test.test_option-no-upscale.html ================================================ CImage API Documentaion

webroot/testtest_option-no-upscale.php

================================================ FILE: docs/api/files/webroot.test.test_option-save-as.html ================================================ CImage API Documentaion

webroot/testtest_option-save-as.php

================================================ FILE: docs/api/graphs/class.html ================================================ CImage API Documentaion
================================================ FILE: docs/api/index.html ================================================ CImage API Documentaion

\

Classes

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).

Functions

errorPage()

errorPage(string  $msg) : void

Display error message.

Parameters

string $msg

to display.

get()

get(mixed  $key, mixed  $default = null) : mixed

Get input from query string or return default value if not set.

Parameters

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.

Returns

mixed —

value from $_GET or default value.

getConfig()

getConfig(string  $key, mixed  $default) : mixed

Get value from config array or default if key is not set in config array.

Parameters

string $key

the key in the config array.

mixed $default

value to be default if $key is not set in config.

Returns

mixed —

value as $config[$key] or $default.

getDefined()

getDefined(mixed  $key, mixed  $defined, mixed  $undefined) : mixed

Get input from query string and set to $defined if defined or else $undefined.

Parameters

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.

Returns

mixed —

value as $defined or $undefined.

verbose()

verbose(string  $msg = null) : void

Log when verbose mode, when used without argument it returns the result.

Parameters

string $msg

to log.

================================================ FILE: docs/api/js/html5.js ================================================ /* HTML5 Shiv v3.7.0 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed */ (function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/[\w\-]+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x"; c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a");var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode|| "undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:"3.7.0",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a,b){a||(a=f); if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d 1 ) { return this.each( function() { $(this).dotdotdot( o ); } ); } var $dot = this; if ( $dot.data( 'dotdotdot' ) ) { $dot.trigger( 'destroy.dot' ); } $dot.data( 'dotdotdot-style', $dot.attr( 'style' ) ); $dot.css( 'word-wrap', 'break-word' ); $dot.bind_events = function() { $dot.bind( 'update.dot', function( e, c ) { e.preventDefault(); e.stopPropagation(); opts.maxHeight = ( typeof opts.height == 'number' ) ? opts.height : getTrueInnerHeight( $dot ); opts.maxHeight += opts.tolerance; if ( typeof c != 'undefined' ) { if ( typeof c == 'string' || c instanceof HTMLElement ) { c = $('
').append( c ).contents(); } if ( c instanceof $ ) { orgContent = c; } } $inr = $dot.wrapInner( '
' ).children(); $inr.empty() .append( orgContent.clone( true ) ) .css({ 'height' : 'auto', 'width' : 'auto', 'border' : 'none', 'padding' : 0, 'margin' : 0 }); var after = false, trunc = false; if ( conf.afterElement ) { after = conf.afterElement.clone( true ); conf.afterElement.remove(); } if ( test( $inr, opts ) ) { if ( opts.wrap == 'children' ) { trunc = children( $inr, opts, after ); } else { trunc = ellipsis( $inr, $dot, $inr, opts, after ); } } $inr.replaceWith( $inr.contents() ); $inr = null; if ( $.isFunction( opts.callback ) ) { opts.callback.call( $dot[ 0 ], trunc, orgContent ); } conf.isTruncated = trunc; return trunc; } ).bind( 'isTruncated.dot', function( e, fn ) { e.preventDefault(); e.stopPropagation(); if ( typeof fn == 'function' ) { fn.call( $dot[ 0 ], conf.isTruncated ); } return conf.isTruncated; } ).bind( 'originalContent.dot', function( e, fn ) { e.preventDefault(); e.stopPropagation(); if ( typeof fn == 'function' ) { fn.call( $dot[ 0 ], orgContent ); } return orgContent; } ).bind( 'destroy.dot', function( e ) { e.preventDefault(); e.stopPropagation(); $dot.unwatch() .unbind_events() .empty() .append( orgContent ) .attr( 'style', $dot.data( 'dotdotdot-style' ) ) .data( 'dotdotdot', false ); } ); return $dot; }; // /bind_events $dot.unbind_events = function() { $dot.unbind('.dot'); return $dot; }; // /unbind_events $dot.watch = function() { $dot.unwatch(); if ( opts.watch == 'window' ) { var $window = $(window), _wWidth = $window.width(), _wHeight = $window.height(); $window.bind( 'resize.dot' + conf.dotId, function() { if ( _wWidth != $window.width() || _wHeight != $window.height() || !opts.windowResizeFix ) { _wWidth = $window.width(); _wHeight = $window.height(); if ( watchInt ) { clearInterval( watchInt ); } watchInt = setTimeout( function() { $dot.trigger( 'update.dot' ); }, 10 ); } } ); } else { watchOrg = getSizes( $dot ); watchInt = setInterval( function() { var watchNew = getSizes( $dot ); if ( watchOrg.width != watchNew.width || watchOrg.height != watchNew.height ) { $dot.trigger( 'update.dot' ); watchOrg = getSizes( $dot ); } }, 100 ); } return $dot; }; $dot.unwatch = function() { $(window).unbind( 'resize.dot' + conf.dotId ); if ( watchInt ) { clearInterval( watchInt ); } return $dot; }; var orgContent = $dot.contents(), opts = $.extend( true, {}, $.fn.dotdotdot.defaults, o ), conf = {}, watchOrg = {}, watchInt = null, $inr = null; conf.afterElement = getElement( opts.after, $dot ); conf.isTruncated = false; conf.dotId = dotId++; $dot.data( 'dotdotdot', true ) .bind_events() .trigger( 'update.dot' ); if ( opts.watch ) { $dot.watch(); } return $dot; }; // public $.fn.dotdotdot.defaults = { 'ellipsis' : '... ', 'wrap' : 'word', 'lastCharacter': { 'remove' : [ ' ', ',', ';', '.', '!', '?' ], 'noEllipsis' : [] }, 'tolerance' : 0, 'callback' : null, 'after' : null, 'height' : null, 'watch' : false, 'windowResizeFix': true, 'debug' : false }; // private var dotId = 1; function children( $elem, o, after ) { var $elements = $elem.children(), isTruncated = false; $elem.empty(); for ( var a = 0, l = $elements.length; a < l; a++ ) { var $e = $elements.eq( a ); $elem.append( $e ); if ( after ) { $elem.append( after ); } if ( test( $elem, o ) ) { $e.remove(); isTruncated = true; break; } else { if ( after ) { after.remove(); } } } return isTruncated; } function ellipsis( $elem, $d, $i, o, after ) { var $elements = $elem.contents(), isTruncated = false; $elem.empty(); var notx = 'table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, select, optgroup, option, textarea, script, style'; for ( var a = 0, l = $elements.length; a < l; a++ ) { if ( isTruncated ) { break; } var e = $elements[ a ], $e = $(e); if ( typeof e == 'undefined' ) { continue; } $elem.append( $e ); if ( after ) { $elem[ ( $elem.is( notx ) ) ? 'after' : 'append' ]( after ); } if ( e.nodeType == 3 ) { if ( test( $i, o ) ) { isTruncated = ellipsisElement( $e, $d, $i, o, after ); } } else { isTruncated = ellipsis( $e, $d, $i, o, after ); } if ( !isTruncated ) { if ( after ) { after.remove(); } } } return isTruncated; } function ellipsisElement( $e, $d, $i, o, after ) { var isTruncated = false, e = $e[ 0 ]; if ( typeof e == 'undefined' ) { return false; } var seporator = ( o.wrap == 'letter' ) ? '' : ' ', textArr = getTextContent( e ).split( seporator ), position = -1, midPos = -1, startPos = 0, endPos = textArr.length - 1; while ( startPos <= endPos ) { var m = Math.floor( ( startPos + endPos ) / 2 ); if ( m == midPos ) { break; } midPos = m; setTextContent( e, textArr.slice( 0, midPos + 1 ).join( seporator ) + o.ellipsis ); if ( !test( $i, o ) ) { position = midPos; startPos = midPos; } else { endPos = midPos; } } if ( position != -1 && !( textArr.length == 1 && textArr[ 0 ].length == 0 ) ) { var txt = addEllipsis( textArr.slice( 0, position + 1 ).join( seporator ), o ); isTruncated = true; setTextContent( e, txt ); } else { var $w = $e.parent(); $e.remove(); var afterLength = ( after ) ? after.length : 0 ; if ( $w.contents().size() > afterLength ) { var $n = $w.contents().eq( -1 - afterLength ); isTruncated = ellipsisElement( $n, $d, $i, o, after ); } else { var $p = $w.prev() var e = $p.contents().eq( -1 )[ 0 ]; if ( typeof e != 'undefined' ) { var txt = addEllipsis( getTextContent( e ), o ); setTextContent( e, txt ); if ( after ) { $p.append( after ); } $w.remove(); isTruncated = true; } } } return isTruncated; } function test( $i, o ) { return $i.innerHeight() > o.maxHeight; } function addEllipsis( txt, o ) { while( $.inArray( txt.slice( -1 ), o.lastCharacter.remove ) > -1 ) { txt = txt.slice( 0, -1 ); } if ( $.inArray( txt.slice( -1 ), o.lastCharacter.noEllipsis ) < 0 ) { txt += o.ellipsis; } return txt; } function getSizes( $d ) { return { 'width' : $d.innerWidth(), 'height': $d.innerHeight() }; } function setTextContent( e, content ) { if ( e.innerText ) { e.innerText = content; } else if ( e.nodeValue ) { e.nodeValue = content; } else if (e.textContent) { e.textContent = content; } } function getTextContent( e ) { if ( e.innerText ) { return e.innerText; } else if ( e.nodeValue ) { return e.nodeValue; } else if ( e.textContent ) { return e.textContent; } else { return ""; } } function getElement( e, $i ) { if ( typeof e == 'undefined' ) { return false; } if ( !e ) { return false; } if ( typeof e == 'string' ) { e = $(e, $i); return ( e.length ) ? e : false; } if ( typeof e == 'object' ) { return ( typeof e.jquery == 'undefined' ) ? false : e; } return false; } function getTrueInnerHeight( $el ) { var h = $el.innerHeight(), a = [ 'paddingTop', 'paddingBottom' ]; for ( var z = 0, l = a.length; z < l; z++ ) { var m = parseInt( $el.css( a[ z ] ), 10 ); if ( isNaN( m ) ) { m = 0; } h -= m; } return h; } function debug( d, m ) { if ( !d ) { return false; } if ( typeof m == 'string' ) { m = 'dotdotdot: ' + m; } else { m = [ 'dotdotdot:', m ]; } if ( typeof window.console != 'undefined' ) { if ( typeof window.console.log != 'undefined' ) { window.console.log( m ); } } return false; } // override jQuery.html var _orgHtml = $.fn.html; $.fn.html = function( str ) { if ( typeof str != 'undefined' ) { if ( this.data( 'dotdotdot' ) ) { if ( typeof str != 'function' ) { return this.trigger( 'update', [ str ] ); } } return _orgHtml.call( this, str ); } return _orgHtml.call( this ); }; // override jQuery.text var _orgText = $.fn.text; $.fn.text = function( str ) { if ( typeof str != 'undefined' ) { if ( this.data( 'dotdotdot' ) ) { var temp = $( '
' ); temp.text( str ); str = temp.html(); temp.remove(); return this.trigger( 'update', [ str ] ); } return _orgText.call( this, str ); } return _orgText.call( this ); }; })( jQuery ); ================================================ FILE: docs/api/js/jquery.iviewer.js ================================================ /* * iviewer Widget for jQuery UI * https://github.com/can3p/iviewer * * Copyright (c) 2009 - 2012 Dmitry Petrov * Dual licensed under the MIT and GPL licenses. * - http://www.opensource.org/licenses/mit-license.php * - http://www.gnu.org/copyleft/gpl.html * * Author: Dmitry Petrov * Version: 0.7.7 */ ( function( $, undefined ) { //this code was taken from the https://github.com/furf/jquery-ui-touch-punch var mouseEvents = { touchstart: 'mousedown', touchmove: 'mousemove', touchend: 'mouseup' }, gesturesSupport = 'ongesturestart' in document.createElement('div'); /** * Convert a touch event to a mouse-like */ function makeMouseEvent (event) { var touch = event.originalEvent.changedTouches[0]; return $.extend(event, { type: mouseEvents[event.type], which: 1, pageX: touch.pageX, pageY: touch.pageY, screenX: touch.screenX, screenY: touch.screenY, clientX: touch.clientX, clientY: touch.clientY, isTouchEvent: true }); } var mouseProto = $.ui.mouse.prototype, _mouseInit = $.ui.mouse.prototype._mouseInit; mouseProto._mouseInit = function() { var self = this; self._touchActive = false; this.element.bind( 'touchstart.' + this.widgetName, function(event) { if (gesturesSupport && event.originalEvent.touches.length > 1) { return; } self._touchActive = true; return self._mouseDown(makeMouseEvent(event)); }) var self = this; // these delegates are required to keep context this._mouseMoveDelegate = function(event) { if (gesturesSupport && event.originalEvent.touches && event.originalEvent.touches.length > 1) { return; } if (self._touchActive) { return self._mouseMove(makeMouseEvent(event)); } }; this._mouseUpDelegate = function(event) { if (self._touchActive) { self._touchActive = false; return self._mouseUp(makeMouseEvent(event)); } }; $(document) .bind('touchmove.'+ this.widgetName, this._mouseMoveDelegate) .bind('touchend.' + this.widgetName, this._mouseUpDelegate); _mouseInit.apply(this); } /** * Simple implementation of jQuery like getters/setters * var val = something(); * something(val); */ var setter = function(setter, getter) { return function(val) { if (arguments.length === 0) { return getter.apply(this); } else { setter.apply(this, arguments); } } }; /** * Internet explorer rotates image relative left top corner, so we should * shift image when it's rotated. */ var ieTransforms = { '0': { marginLeft: 0, marginTop: 0, filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=1, M12=0, M21=0, M22=1, SizingMethod="auto expand")' }, '90': { marginLeft: -1, marginTop: 1, filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=0, M12=-1, M21=1, M22=0, SizingMethod="auto expand")' }, '180': { marginLeft: 0, marginTop: 0, filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=-1, M12=0, M21=0, M22=-1, SizingMethod="auto expand")' }, '270': { marginLeft: -1, marginTop: 1, filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=0, M12=1, M21=-1, M22=0, SizingMethod="auto expand")' } }, // this test is the inversion of the css filters test from the modernizr project useIeTransforms = function() { var modElem = document.createElement('modernizr'), mStyle = modElem.style, omPrefixes = 'Webkit Moz O ms', domPrefixes = omPrefixes.toLowerCase().split(' '), props = ("transform" + ' ' + domPrefixes.join("Transform ") + "Transform").split(' '); for ( var i in props ) { var prop = props[i]; if ( !$.contains(prop, "-") && mStyle[prop] !== undefined ) { return false; } } return true; }(); $.widget( "ui.iviewer", $.ui.mouse, { widgetEventPrefix: "iviewer", options : { /** * start zoom value for image, not used now * may be equal to "fit" to fit image into container or scale in % **/ zoom: "fit", /** * base value to scale image **/ zoom_base: 100, /** * maximum zoom **/ zoom_max: 800, /** * minimum zoom **/ zoom_min: 25, /** * base of rate multiplier. * zoom is calculated by formula: zoom_base * zoom_delta^rate **/ zoom_delta: 1.4, /** * whether the zoom should be animated. */ zoom_animation: true, /** * if true plugin doesn't add its own controls **/ ui_disabled: false, /** * If false mousewheel will be disabled */ mousewheel: true, /** * if false, plugin doesn't bind resize event on window and this must * be handled manually **/ update_on_resize: true, /** * event is triggered when zoom value is changed * @param int new zoom value * @return boolean if false zoom action is aborted **/ onZoom: jQuery.noop, /** * event is triggered when zoom value is changed after image is set to the new dimensions * @param int new zoom value * @return boolean if false zoom action is aborted **/ onAfterZoom: jQuery.noop, /** * event is fired on drag begin * @param object coords mouse coordinates on the image * @return boolean if false is returned, drag action is aborted **/ onStartDrag: jQuery.noop, /** * event is fired on drag action * @param object coords mouse coordinates on the image **/ onDrag: jQuery.noop, /** * event is fired on drag stop * @param object coords mouse coordinates on the image **/ onStopDrag: jQuery.noop, /** * event is fired when mouse moves over image * @param object coords mouse coordinates on the image **/ onMouseMove: jQuery.noop, /** * mouse click event * @param object coords mouse coordinates on the image **/ onClick: jQuery.noop, /** * event is fired when image starts to load */ onStartLoad: null, /** * event is fired, when image is loaded and initially positioned */ onFinishLoad: null, /** * event is fired when image load error occurs */ onErrorLoad: null }, _create: function() { var me = this; //drag variables this.dx = 0; this.dy = 0; /* object containing actual information about image * @img_object.object - jquery img object * @img_object.orig_{width|height} - original dimensions * @img_object.display_{width|height} - actual dimensions */ this.img_object = {}; this.zoom_object = {}; //object to show zoom status this._angle = 0; this.current_zoom = this.options.zoom; if(this.options.src === null){ return; } this.container = this.element; this._updateContainerInfo(); //init container this.container.css("overflow","hidden"); if (this.options.update_on_resize == true) { $(window).resize(function() { me.update(); }); } this.img_object = new $.ui.iviewer.ImageObject(this.options.zoom_animation); if (this.options.mousewheel) { this.container.bind('mousewheel.iviewer', function(ev, delta) { //this event is there instead of containing div, because //at opera it triggers many times on div var zoom = (delta > 0)?1:-1, container_offset = me.container.offset(), mouse_pos = { //jquery.mousewheel 3.1.0 uses strange MozMousePixelScroll event //which is not being fixed by jQuery.Event x: (ev.pageX || ev.originalEvent.pageX) - container_offset.left, y: (ev.pageY || ev.originalEvent.pageX) - container_offset.top }; me.zoom_by(zoom, mouse_pos); return false; }); if (gesturesSupport) { var gestureThrottle = +new Date(); var originalScale, originalCenter; this.img_object.object() // .bind('gesturestart', function(ev) { .bind('touchstart', function(ev) { originalScale = me.current_zoom; var touches = ev.originalEvent.touches, container_offset; if (touches.length == 2) { container_offset = me.container.offset(); originalCenter = { x: (touches[0].pageX + touches[1].pageX) / 2 - container_offset.left, y: (touches[0].pageY + touches[1].pageY) / 2 - container_offset.top }; } else { originalCenter = null; } }).bind('gesturechange', function(ev) { //do not want to import throttle function from underscore var d = +new Date(); if ((d - gestureThrottle) < 50) { return; } gestureThrottle = d; var zoom = originalScale * ev.originalEvent.scale; me.set_zoom(zoom, originalCenter); ev.preventDefault(); }).bind('gestureend', function(ev) { originalCenter = null; }); } } //init object this.img_object.object() //bind mouse events .click(function(e){return me._click(e)}) .prependTo(this.container); this.container.bind('mousemove', function(ev) { me._handleMouseMove(ev); }); this.loadImage(this.options.src); if(!this.options.ui_disabled) { this.createui(); } this._mouseInit(); }, destroy: function() { $.Widget.prototype.destroy.call( this ); this._mouseDestroy(); this.img_object.object().remove(); this.container.off('.iviewer'); this.container.css('overflow', ''); //cleanup styles on destroy }, _updateContainerInfo: function() { this.options.height = this.container.height(); this.options.width = this.container.width(); }, update: function() { this._updateContainerInfo() this.setCoords(this.img_object.x(), this.img_object.y()); }, loadImage: function( src ) { this.current_zoom = this.options.zoom; var me = this; this._trigger('onStartLoad', 0, src); this.container.addClass("iviewer_loading"); this.img_object.load(src, function() { me._imageLoaded(src); }, function() { me._trigger("onErrorLoad", 0, src); }); }, _imageLoaded: function(src) { this.container.removeClass("iviewer_loading"); this.container.addClass("iviewer_cursor"); if(this.options.zoom == "fit"){ this.fit(true); } else { this.set_zoom(this.options.zoom, true); } this._trigger('onFinishLoad', 0, src); }, /** * fits image in the container * * @param {boolean} skip_animation **/ fit: function(skip_animation) { var aspect_ratio = this.img_object.orig_width() / this.img_object.orig_height(); var window_ratio = this.options.width / this.options.height; var choose_left = (aspect_ratio > window_ratio); var new_zoom = 0; if(choose_left){ new_zoom = this.options.width / this.img_object.orig_width() * 100; } else { new_zoom = this.options.height / this.img_object.orig_height() * 100; } this.set_zoom(new_zoom, skip_animation); }, /** * center image in container **/ center: function() { this.setCoords(-Math.round((this.img_object.display_width() - this.options.width)/2), -Math.round((this.img_object.display_height() - this.options.height)/2)); }, /** * move a point in container to the center of display area * @param x a point in container * @param y a point in container **/ moveTo: function(x, y) { var dx = x-Math.round(this.options.width/2); var dy = y-Math.round(this.options.height/2); var new_x = this.img_object.x() - dx; var new_y = this.img_object.y() - dy; this.setCoords(new_x, new_y); }, /** * Get container offset object. */ getContainerOffset: function() { return jQuery.extend({}, this.container.offset()); }, /** * set coordinates of upper left corner of image object **/ setCoords: function(x,y) { //do nothing while image is being loaded if(!this.img_object.loaded()) { return; } var coords = this._correctCoords(x,y); this.img_object.x(coords.x); this.img_object.y(coords.y); }, _correctCoords: function( x, y ) { x = parseInt(x, 10); y = parseInt(y, 10); //check new coordinates to be correct (to be in rect) if(y > 0){ y = 0; } if(x > 0){ x = 0; } if(y + this.img_object.display_height() < this.options.height){ y = this.options.height - this.img_object.display_height(); } if(x + this.img_object.display_width() < this.options.width){ x = this.options.width - this.img_object.display_width(); } if(this.img_object.display_width() <= this.options.width){ x = -(this.img_object.display_width() - this.options.width)/2; } if(this.img_object.display_height() <= this.options.height){ y = -(this.img_object.display_height() - this.options.height)/2; } return { x: x, y:y }; }, /** * convert coordinates on the container to the coordinates on the image (in original size) * * @return object with fields x,y according to coordinates or false * if initial coords are not inside image **/ containerToImage : function (x,y) { var coords = { x : x - this.img_object.x(), y : y - this.img_object.y() }; coords = this.img_object.toOriginalCoords(coords); return { x : util.descaleValue(coords.x, this.current_zoom), y : util.descaleValue(coords.y, this.current_zoom) }; }, /** * convert coordinates on the image (in original size, and zero angle) to the coordinates on the container * * @return object with fields x,y according to coordinates **/ imageToContainer : function (x,y) { var coords = { x : util.scaleValue(x, this.current_zoom), y : util.scaleValue(y, this.current_zoom) }; return this.img_object.toRealCoords(coords); }, /** * get mouse coordinates on the image * @param e - object containing pageX and pageY fields, e.g. mouse event object * * @return object with fields x,y according to coordinates or false * if initial coords are not inside image **/ _getMouseCoords : function(e) { var containerOffset = this.container.offset(); coords = this.containerToImage(e.pageX - containerOffset.left, e.pageY - containerOffset.top); return coords; }, /** * set image scale to the new_zoom * * @param {number} new_zoom image scale in % * @param {boolean} skip_animation * @param {x: number, y: number} Coordinates of point the should not be moved on zoom. The default is the center of image. **/ set_zoom: function(new_zoom, skip_animation, zoom_center) { if (this._trigger('onZoom', 0, new_zoom) == false) { return; } //do nothing while image is being loaded if(!this.img_object.loaded()) { return; } zoom_center = zoom_center || { x: Math.round(this.options.width/2), y: Math.round(this.options.height/2) } if(new_zoom < this.options.zoom_min) { new_zoom = this.options.zoom_min; } else if(new_zoom > this.options.zoom_max) { new_zoom = this.options.zoom_max; } /* we fake these values to make fit zoom properly work */ if(this.current_zoom == "fit") { var old_x = zoom_center.x + Math.round(this.img_object.orig_width()/2); var old_y = zoom_center.y + Math.round(this.img_object.orig_height()/2); this.current_zoom = 100; } else { var old_x = -this.img_object.x() + zoom_center.x; var old_y = -this.img_object.y() + zoom_center.y } var new_width = util.scaleValue(this.img_object.orig_width(), new_zoom); var new_height = util.scaleValue(this.img_object.orig_height(), new_zoom); var new_x = util.scaleValue( util.descaleValue(old_x, this.current_zoom), new_zoom); var new_y = util.scaleValue( util.descaleValue(old_y, this.current_zoom), new_zoom); new_x = zoom_center.x - new_x; new_y = zoom_center.y - new_y; new_width = Math.floor(new_width); new_height = Math.floor(new_height); new_x = Math.floor(new_x); new_y = Math.floor(new_y); this.img_object.display_width(new_width); this.img_object.display_height(new_height); var coords = this._correctCoords( new_x, new_y ), self = this; this.img_object.setImageProps(new_width, new_height, coords.x, coords.y, skip_animation, function() { self._trigger('onAfterZoom', 0, new_zoom ); }); this.current_zoom = new_zoom; this.update_status(); }, /** * changes zoom scale by delta * zoom is calculated by formula: zoom_base * zoom_delta^rate * @param Integer delta number to add to the current multiplier rate number * @param {x: number, y: number=} Coordinates of point the should not be moved on zoom. **/ zoom_by: function(delta, zoom_center) { var closest_rate = this.find_closest_zoom_rate(this.current_zoom); var next_rate = closest_rate + delta; var next_zoom = this.options.zoom_base * Math.pow(this.options.zoom_delta, next_rate) if(delta > 0 && next_zoom < this.current_zoom) { next_zoom *= this.options.zoom_delta; } if(delta < 0 && next_zoom > this.current_zoom) { next_zoom /= this.options.zoom_delta; } this.set_zoom(next_zoom, undefined, zoom_center); }, /** * Rotate image * @param {num} deg Degrees amount to rotate. Positive values rotate image clockwise. * Currently 0, 90, 180, 270 and -90, -180, -270 values are supported * * @param {boolean} abs If the flag is true if, the deg parameter will be considered as * a absolute value and relative otherwise. * @return {num|null} Method will return current image angle if called without any arguments. **/ angle: function(deg, abs) { if (arguments.length === 0) { return this.img_object.angle(); } if (deg < -270 || deg > 270 || deg % 90 !== 0) { return; } if (!abs) { deg += this.img_object.angle(); } if (deg < 0) { deg += 360; } if (deg >= 360) { deg -= 360; } if (deg === this.img_object.angle()) { return; } this.img_object.angle(deg); //the rotate behavior is different in all editors. For now we just center the //image. However, it will be better to try to keep the position. this.center(); this._trigger('angle', 0, { angle: this.img_object.angle() }); }, /** * finds closest multiplier rate for value * basing on zoom_base and zoom_delta values from settings * @param Number value zoom value to examine **/ find_closest_zoom_rate: function(value) { if(value == this.options.zoom_base) { return 0; } function div(val1,val2) { return val1 / val2 }; function mul(val1,val2) { return val1 * val2 }; var func = (value > this.options.zoom_base)?mul:div; var sgn = (value > this.options.zoom_base)?1:-1; var mltplr = this.options.zoom_delta; var rate = 1; while(Math.abs(func(this.options.zoom_base, Math.pow(mltplr,rate)) - value) > Math.abs(func(this.options.zoom_base, Math.pow(mltplr,rate+1)) - value)) { rate++; } return sgn * rate; }, /* update scale info in the container */ update_status: function() { if(!this.options.ui_disabled) { var percent = Math.round(100*this.img_object.display_height()/this.img_object.orig_height()); if(percent) { this.zoom_object.html(percent + "%"); } } }, /** * Get some information about the image. * Currently orig_(width|height), display_(width|height), angle, zoom and src params are supported. * * @param {string} parameter to check * @param {boolean} withoutRotation if param is orig_width or orig_height and this flag is set to true, * method will return original image width without considering rotation. * */ info: function(param, withoutRotation) { if (!param) { return; } switch (param) { case 'orig_width': case 'orig_height': if (withoutRotation) { return (this.img_object.angle() % 180 === 0 ? this.img_object[param]() : param === 'orig_width' ? this.img_object.orig_height() : this.img_object.orig_width()); } else { return this.img_object[param](); } case 'display_width': case 'display_height': case 'angle': return this.img_object[param](); case 'zoom': return this.current_zoom; case 'src': return this.img_object.object().attr('src'); case 'coords': return { x: this.img_object.x(), y: this.img_object.y() }; } }, /** * callback for handling mousdown event to start dragging image **/ _mouseStart: function( e ) { $.ui.mouse.prototype._mouseStart.call(this, e); if (this._trigger('onStartDrag', 0, this._getMouseCoords(e)) === false) { return false; } /* start drag event*/ this.container.addClass("iviewer_drag_cursor"); //#10: fix movement quirks for ipad this._dragInitialized = !(e.originalEvent.changedTouches && e.originalEvent.changedTouches.length==1); this.dx = e.pageX - this.img_object.x(); this.dy = e.pageY - this.img_object.y(); return true; }, _mouseCapture: function( e ) { return true; }, /** * Handle mouse move if needed. User can avoid using this callback, because * he can get the same information through public methods. * @param {jQuery.Event} e */ _handleMouseMove: function(e) { this._trigger('onMouseMove', e, this._getMouseCoords(e)); }, /** * callback for handling mousemove event to drag image **/ _mouseDrag: function(e) { $.ui.mouse.prototype._mouseDrag.call(this, e); //#10: imitate mouseStart, because we can get here without it on iPad for some reason if (!this._dragInitialized) { this.dx = e.pageX - this.img_object.x(); this.dy = e.pageY - this.img_object.y(); this._dragInitialized = true; } var ltop = e.pageY - this.dy; var lleft = e.pageX - this.dx; this.setCoords(lleft, ltop); this._trigger('onDrag', e, this._getMouseCoords(e)); return false; }, /** * callback for handling stop drag **/ _mouseStop: function(e) { $.ui.mouse.prototype._mouseStop.call(this, e); this.container.removeClass("iviewer_drag_cursor"); this._trigger('onStopDrag', 0, this._getMouseCoords(e)); }, _click: function(e) { this._trigger('onClick', 0, this._getMouseCoords(e)); }, /** * create zoom buttons info box **/ createui: function() { var me=this; $("
", { 'class': "iviewer_zoom_in iviewer_common iviewer_button"}) .bind('mousedown touchstart',function(){me.zoom_by(1); return false;}) .appendTo(this.container); $("
", { 'class': "iviewer_zoom_out iviewer_common iviewer_button"}) .bind('mousedown touchstart',function(){me.zoom_by(- 1); return false;}) .appendTo(this.container); $("
", { 'class': "iviewer_zoom_zero iviewer_common iviewer_button"}) .bind('mousedown touchstart',function(){me.set_zoom(100); return false;}) .appendTo(this.container); $("
", { 'class': "iviewer_zoom_fit iviewer_common iviewer_button"}) .bind('mousedown touchstart',function(){me.fit(this); return false;}) .appendTo(this.container); this.zoom_object = $("
").addClass("iviewer_zoom_status iviewer_common") .appendTo(this.container); $("
", { 'class': "iviewer_rotate_left iviewer_common iviewer_button"}) .bind('mousedown touchstart',function(){me.angle(-90); return false;}) .appendTo(this.container); $("
", { 'class': "iviewer_rotate_right iviewer_common iviewer_button" }) .bind('mousedown touchstart',function(){me.angle(90); return false;}) .appendTo(this.container); this.update_status(); //initial status update } } ); /** * @class $.ui.iviewer.ImageObject Class represents image and provides public api without * extending image prototype. * @constructor * @param {boolean} do_anim Do we want to animate image on dimension changes? */ $.ui.iviewer.ImageObject = function(do_anim) { this._img = $("") //this is needed, because chromium sets them auto otherwise .css({ position: "absolute", top :"0px", left: "0px"}); this._loaded = false; this._swapDimensions = false; this._do_anim = do_anim || false; this.x(0, true); this.y(0, true); this.angle(0); }; /** @lends $.ui.iviewer.ImageObject.prototype */ (function() { /** * Restore initial object state. * * @param {number} w Image width. * @param {number} h Image height. */ this._reset = function(w, h) { this._angle = 0; this._swapDimensions = false; this.x(0); this.y(0); this.orig_width(w); this.orig_height(h); this.display_width(w); this.display_height(h); }; /** * Check if image is loaded. * * @return {boolean} */ this.loaded = function() { return this._loaded; }; /** * Load image. * * @param {string} src Image url. * @param {Function=} loaded Function will be called on image load. */ this.load = function(src, loaded, error) { var self = this; loaded = loaded || jQuery.noop; this._loaded = false; //If we assign new image url to the this._img IE9 fires onload event and image width and //height are set to zero. So, we create another image object and load image through it. var img = new Image(); img.onload = function() { self._loaded = true; self._reset(this.width, this.height); self._img .removeAttr("width") .removeAttr("height") .removeAttr("style") //max-width is reset, because plugin breaks in the twitter bootstrap otherwise .css({ position: "absolute", top :"0px", left: "0px", maxWidth: "none"}) self._img[0].src = src; loaded(); }; img.onerror = error; //we need this because sometimes internet explorer 8 fires onload event //right after assignment (synchronously) setTimeout(function() { img.src = src; }, 0); this.angle(0); }; this._dimension = function(prefix, name) { var horiz = '_' + prefix + '_' + name, vert = '_' + prefix + '_' + (name === 'height' ? 'width' : 'height'); return setter(function(val) { this[this._swapDimensions ? horiz: vert] = val; }, function() { return this[this._swapDimensions ? horiz: vert]; }); }; /** * Getters and setter for common image dimensions. * display_ means real image tag dimensions * orig_ means physical image dimensions. * Note, that dimensions are swapped if image is rotated. It necessary, * because as little as possible code should know about rotation. */ this.display_width = this._dimension('display', 'width'), this.display_height = this._dimension('display', 'height'), this.display_diff = function() { return Math.floor( this.display_width() - this.display_height() ) }; this.orig_width = this._dimension('orig', 'width'), this.orig_height = this._dimension('orig', 'height'), /** * Setter for X coordinate. If image is rotated we need to additionaly shift an * image to map image coordinate to the visual position. * * @param {number} val Coordinate value. * @param {boolean} skipCss If true, we only set the value and do not touch the dom. */ this.x = setter(function(val, skipCss) { this._x = val; if (!skipCss) { this._finishAnimation(); this._img.css("left",this._x + (this._swapDimensions ? this.display_diff() / 2 : 0) + "px"); } }, function() { return this._x; }); /** * Setter for Y coordinate. If image is rotated we need to additionaly shift an * image to map image coordinate to the visual position. * * @param {number} val Coordinate value. * @param {boolean} skipCss If true, we only set the value and do not touch the dom. */ this.y = setter(function(val, skipCss) { this._y = val; if (!skipCss) { this._finishAnimation(); this._img.css("top",this._y - (this._swapDimensions ? this.display_diff() / 2 : 0) + "px"); } }, function() { return this._y; }); /** * Perform image rotation. * * @param {number} deg Absolute image angle. The method will work with values 0, 90, 180, 270 degrees. */ this.angle = setter(function(deg) { var prevSwap = this._swapDimensions; this._angle = deg; this._swapDimensions = deg % 180 !== 0; if (prevSwap !== this._swapDimensions) { var verticalMod = this._swapDimensions ? -1 : 1; this.x(this.x() - verticalMod * this.display_diff() / 2, true); this.y(this.y() + verticalMod * this.display_diff() / 2, true); }; var cssVal = 'rotate(' + deg + 'deg)', img = this._img; jQuery.each(['', '-webkit-', '-moz-', '-o-', '-ms-'], function(i, prefix) { img.css(prefix + 'transform', cssVal); }); if (useIeTransforms) { jQuery.each(['-ms-', ''], function(i, prefix) { img.css(prefix + 'filter', ieTransforms[deg].filter); }); img.css({ marginLeft: ieTransforms[deg].marginLeft * this.display_diff() / 2, marginTop: ieTransforms[deg].marginTop * this.display_diff() / 2 }); } }, function() { return this._angle; }); /** * Map point in the container coordinates to the point in image coordinates. * You will get coordinates of point on image with respect to rotation, * but will be set as if image was not rotated. * So, if image was rotated 90 degrees, it's (0,0) point will be on the * top right corner. * * @param {{x: number, y: number}} point Point in container coordinates. * @return {{x: number, y: number}} */ this.toOriginalCoords = function(point) { switch (this.angle()) { case 0: return { x: point.x, y: point.y } case 90: return { x: point.y, y: this.display_width() - point.x } case 180: return { x: this.display_width() - point.x, y: this.display_height() - point.y } case 270: return { x: this.display_height() - point.y, y: point.x } } }; /** * Map point in the image coordinates to the point in container coordinates. * You will get coordinates of point on container with respect to rotation. * Note, if image was rotated 90 degrees, it's (0,0) point will be on the * top right corner. * * @param {{x: number, y: number}} point Point in container coordinates. * @return {{x: number, y: number}} */ this.toRealCoords = function(point) { switch (this.angle()) { case 0: return { x: this.x() + point.x, y: this.y() + point.y } case 90: return { x: this.x() + this.display_width() - point.y, y: this.y() + point.x} case 180: return { x: this.x() + this.display_width() - point.x, y: this.y() + this.display_height() - point.y} case 270: return { x: this.x() + point.y, y: this.y() + this.display_height() - point.x} } }; /** * @return {jQuery} Return image node. this is needed to add event handlers. */ this.object = setter(jQuery.noop, function() { return this._img; }); /** * Change image properties. * * @param {number} disp_w Display width; * @param {number} disp_h Display height; * @param {number} x * @param {number} y * @param {boolean} skip_animation If true, the animation will be skiped despite the * value set in constructor. * @param {Function=} complete Call back will be fired when zoom will be complete. */ this.setImageProps = function(disp_w, disp_h, x, y, skip_animation, complete) { complete = complete || jQuery.noop; this.display_width(disp_w); this.display_height(disp_h); this.x(x, true); this.y(y, true); var w = this._swapDimensions ? disp_h : disp_w; var h = this._swapDimensions ? disp_w : disp_h; var params = { width: w, height: h, top: y - (this._swapDimensions ? this.display_diff() / 2 : 0) + "px", left: x + (this._swapDimensions ? this.display_diff() / 2 : 0) + "px" }; if (useIeTransforms) { jQuery.extend(params, { marginLeft: ieTransforms[this.angle()].marginLeft * this.display_diff() / 2, marginTop: ieTransforms[this.angle()].marginTop * this.display_diff() / 2 }); } var swapDims = this._swapDimensions, img = this._img; //here we come: another IE oddness. If image is rotated 90 degrees with a filter, than //width and height getters return real width and height of rotated image. The bad news //is that to set height you need to set a width and vice versa. Fuck IE. //So, in this case we have to animate width and height manually. if(useIeTransforms && swapDims) { var ieh = this._img.width(), iew = this._img.height(), iedh = params.height - ieh; iedw = params.width - iew; delete params.width; delete params.height; } if (this._do_anim && !skip_animation) { this._img.stop(true) .animate(params, { duration: 200, complete: complete, step: function(now, fx) { if(useIeTransforms && swapDims && (fx.prop === 'top')) { var percent = (now - fx.start) / (fx.end - fx.start); img.height(ieh + iedh * percent); img.width(iew + iedw * percent); img.css('top', now); } } }); } else { this._img.css(params); setTimeout(complete, 0); //both if branches should behave equally. } }; //if we set image coordinates we need to be sure that no animation is active atm this._finishAnimation = function() { this._img.stop(true, true); } }).apply($.ui.iviewer.ImageObject.prototype); var util = { scaleValue: function(value, toZoom) { return value * toZoom / 100; }, descaleValue: function(value, fromZoom) { return value * 100 / fromZoom; } }; } )( jQuery, undefined ); ================================================ FILE: docs/api/js/jquery.mousewheel.js ================================================ /*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh) * Licensed under the MIT License (LICENSE.txt). * * Version: 3.1.9 * * Requires: jQuery 1.2.2+ */ (function (factory) { if ( typeof define === 'function' && define.amd ) { // AMD. Register as an anonymous module. define(['jquery'], factory); } else if (typeof exports === 'object') { // Node/CommonJS style for Browserify module.exports = factory; } else { // Browser globals factory(jQuery); } }(function ($) { var toFix = ['wheel', 'mousewheel', 'DOMMouseScroll', 'MozMousePixelScroll'], toBind = ( 'onwheel' in document || document.documentMode >= 9 ) ? ['wheel'] : ['mousewheel', 'DomMouseScroll', 'MozMousePixelScroll'], slice = Array.prototype.slice, nullLowestDeltaTimeout, lowestDelta; if ( $.event.fixHooks ) { for ( var i = toFix.length; i; ) { $.event.fixHooks[ toFix[--i] ] = $.event.mouseHooks; } } var special = $.event.special.mousewheel = { version: '3.1.9', setup: function() { if ( this.addEventListener ) { for ( var i = toBind.length; i; ) { this.addEventListener( toBind[--i], handler, false ); } } else { this.onmousewheel = handler; } // Store the line height and page height for this particular element $.data(this, 'mousewheel-line-height', special.getLineHeight(this)); $.data(this, 'mousewheel-page-height', special.getPageHeight(this)); }, teardown: function() { if ( this.removeEventListener ) { for ( var i = toBind.length; i; ) { this.removeEventListener( toBind[--i], handler, false ); } } else { this.onmousewheel = null; } }, getLineHeight: function(elem) { return parseInt($(elem)['offsetParent' in $.fn ? 'offsetParent' : 'parent']().css('fontSize'), 10); }, getPageHeight: function(elem) { return $(elem).height(); }, settings: { adjustOldDeltas: true } }; $.fn.extend({ mousewheel: function(fn) { return fn ? this.bind('mousewheel', fn) : this.trigger('mousewheel'); }, unmousewheel: function(fn) { return this.unbind('mousewheel', fn); } }); function handler(event) { var orgEvent = event || window.event, args = slice.call(arguments, 1), delta = 0, deltaX = 0, deltaY = 0, absDelta = 0; event = $.event.fix(orgEvent); event.type = 'mousewheel'; // Old school scrollwheel delta if ( 'detail' in orgEvent ) { deltaY = orgEvent.detail * -1; } if ( 'wheelDelta' in orgEvent ) { deltaY = orgEvent.wheelDelta; } if ( 'wheelDeltaY' in orgEvent ) { deltaY = orgEvent.wheelDeltaY; } if ( 'wheelDeltaX' in orgEvent ) { deltaX = orgEvent.wheelDeltaX * -1; } // Firefox < 17 horizontal scrolling related to DOMMouseScroll event if ( 'axis' in orgEvent && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { deltaX = deltaY * -1; deltaY = 0; } // Set delta to be deltaY or deltaX if deltaY is 0 for backwards compatabilitiy delta = deltaY === 0 ? deltaX : deltaY; // New school wheel delta (wheel event) if ( 'deltaY' in orgEvent ) { deltaY = orgEvent.deltaY * -1; delta = deltaY; } if ( 'deltaX' in orgEvent ) { deltaX = orgEvent.deltaX; if ( deltaY === 0 ) { delta = deltaX * -1; } } // No change actually happened, no reason to go any further if ( deltaY === 0 && deltaX === 0 ) { return; } // Need to convert lines and pages to pixels if we aren't already in pixels // There are three delta modes: // * deltaMode 0 is by pixels, nothing to do // * deltaMode 1 is by lines // * deltaMode 2 is by pages if ( orgEvent.deltaMode === 1 ) { var lineHeight = $.data(this, 'mousewheel-line-height'); delta *= lineHeight; deltaY *= lineHeight; deltaX *= lineHeight; } else if ( orgEvent.deltaMode === 2 ) { var pageHeight = $.data(this, 'mousewheel-page-height'); delta *= pageHeight; deltaY *= pageHeight; deltaX *= pageHeight; } // Store lowest absolute delta to normalize the delta values absDelta = Math.max( Math.abs(deltaY), Math.abs(deltaX) ); if ( !lowestDelta || absDelta < lowestDelta ) { lowestDelta = absDelta; // Adjust older deltas if necessary if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { lowestDelta /= 40; } } // Adjust older deltas if necessary if ( shouldAdjustOldDeltas(orgEvent, absDelta) ) { // Divide all the things by 40! delta /= 40; deltaX /= 40; deltaY /= 40; } // Get a whole, normalized value for the deltas delta = Math[ delta >= 1 ? 'floor' : 'ceil' ](delta / lowestDelta); deltaX = Math[ deltaX >= 1 ? 'floor' : 'ceil' ](deltaX / lowestDelta); deltaY = Math[ deltaY >= 1 ? 'floor' : 'ceil' ](deltaY / lowestDelta); // Add information to the event object event.deltaX = deltaX; event.deltaY = deltaY; event.deltaFactor = lowestDelta; // Go ahead and set deltaMode to 0 since we converted to pixels // Although this is a little odd since we overwrite the deltaX/Y // properties with normalized deltas. event.deltaMode = 0; // Add event and delta to the front of the arguments args.unshift(event, delta, deltaX, deltaY); // Clearout lowestDelta after sometime to better // handle multiple device types that give different // a different lowestDelta // Ex: trackpad = 3 and mouse wheel = 120 if (nullLowestDeltaTimeout) { clearTimeout(nullLowestDeltaTimeout); } nullLowestDeltaTimeout = setTimeout(nullLowestDelta, 200); return ($.event.dispatch || $.event.handle).apply(this, args); } function nullLowestDelta() { lowestDelta = null; } function shouldAdjustOldDeltas(orgEvent, absDelta) { // If this is an older event and the delta is divisable by 120, // then we are assuming that the browser is treating this as an // older mouse wheel event and that we should divide the deltas // by 40 to try and get a more usable deltaFactor. // Side note, this actually impacts the reported scroll distance // in older browsers and can cause scrolling to be slower than native. // Turn this off by setting $.event.special.mousewheel.settings.adjustOldDeltas to false. return special.settings.adjustOldDeltas && orgEvent.type === 'mousewheel' && absDelta % 120 === 0; } })); ================================================ FILE: docs/api/js/jquery.smooth-scroll.js ================================================ $(document).ready(function() { function filterPath(string) { return string .replace(/^\//,'') .replace(/(index|default).[a-zA-Z]{3,4}$/,'') .replace(/\/$/,''); } var locationPath = filterPath(location.pathname); $('a[href*=#]').each(function() { var thisPath = filterPath(this.pathname) || locationPath; if ( locationPath == thisPath && (location.hostname == this.hostname || !this.hostname) && this.hash.replace(/#/,'') ) { var $target = $(this.hash), target = this.hash; if (target) { $(this).click(function(event) { if (!$(this.hash).offset()) { return; } event.preventDefault(); position = $(this.hash).offset().top; $('html,body').animate({scrollTop: position}, 400, function() { location.hash = target; }); }); } } }); }); ================================================ FILE: docs/api/namespaces/default.html ================================================ CImage API Documentaion

\

Classes

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).

Functions

errorPage()

errorPage(string  $msg) : void

Display error message.

Parameters

string $msg

to display.

get()

get(mixed  $key, mixed  $default = null) : mixed

Get input from query string or return default value if not set.

Parameters

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.

Returns

mixed —

value from $_GET or default value.

getConfig()

getConfig(string  $key, mixed  $default) : mixed

Get value from config array or default if key is not set in config array.

Parameters

string $key

the key in the config array.

mixed $default

value to be default if $key is not set in config.

Returns

mixed —

value as $config[$key] or $default.

getDefined()

getDefined(mixed  $key, mixed  $defined, mixed  $undefined) : mixed

Get input from query string and set to $defined if defined or else $undefined.

Parameters

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.

Returns

mixed —

value as $defined or $undefined.

verbose()

verbose(string  $msg = null) : void

Log when verbose mode, when used without argument it returns the result.

Parameters

string $msg

to log.

================================================ FILE: docs/api/reports/deprecated.html ================================================ » Deprecated elements
No deprecated elements have been found in this project.
================================================ FILE: docs/api/reports/errors.html ================================================ » Compilation errors

CRemoteImage.php 1

Type Line Description
error 0 No summary was found for this file

CImage.php 34

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

CAsciiArt.php 1

Type Line Description
error 0 No summary was found for this file

CHttpGet.php 3

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

CWhitelist.php 1

Type Line Description
error 0 No summary was found for this file
================================================ FILE: docs/api/reports/markers.html ================================================ » Markers

CImage.php 2

Type Line Description
TODO 467 clean up how $this->saveFolder is used in other methods.
TODO 414 Clean up these and check if and how they are used
================================================ FILE: functions.php ================================================ "; list($command) = explode(" ", $commandString); $no = is_executable($command) ? null : 'NOT'; $text .= "The command for $what is $no an executable.
"; return $text; } ================================================ FILE: phpcs.xml ================================================ Custom rule set. . test autoload.php docs/* coverage/* webroot/imgs.php webroot/imgp.php webroot/imgd.php webroot/test/* webroot/js/* webroot/compare/* ================================================ FILE: phpdoc.xml ================================================ CImage API Documentaion docs/api docs/api ./*.php webroot/img.php webroot/img_config.php ================================================ FILE: phpunit.xml ================================================ test *.php ================================================ FILE: test/CCacheTest.php ================================================ setDir(CACHE_PATH); $exp = "exists, writable"; $res = $cache->getStatusOfSubdir(""); $this->assertEquals($exp, $res, "Status of cache dir missmatch."); } /** * Test * * @expectedException Exception * * @return void */ public function testSetWrongCacheDir() { $cache = new CCache(); $cache->setDir(CACHE_PATH . "/NO_EXISTS"); } /** * Test * * @return void */ public function testCreateSubdir() { $cache = new CCache(); $cache->setDir(CACHE_PATH); $subdir = "__test__"; $cache->removeSubdir($subdir); $exp = "does not exist"; $res = $cache->getStatusOfSubdir($subdir, false); $this->assertEquals($exp, $res, "Subdir should not be created."); $res = $cache->getPathToSubdir($subdir); $exp = realpath(CACHE_PATH . "/$subdir"); $this->assertEquals($exp, $res, "Subdir path missmatch."); $exp = "exists, writable"; $res = $cache->getStatusOfSubdir($subdir); $this->assertEquals($exp, $res, "Subdir should exist."); $res = $cache->removeSubdir($subdir); $this->assertTrue($res, "Remove subdir."); } } ================================================ FILE: test/CImageDummyTest.php ================================================ setDir(CACHE_PATH); $this->cachepath = $cache->getPathToSubdir(self::DUMMY); } /** * Clean up cache dir content. * * @return void */ protected function removeFilesInCacheDir() { $files = glob($this->cachepath . "/*"); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } } /** * Teardown environment * * @return void */ protected function tearDown() { $cache = new CCache(); $cache->setDir(CACHE_PATH); $this->removeFilesInCacheDir(); $cache->removeSubdir(self::DUMMY); } /** * Test * * @return void */ public function testCreate1() { $img = new CImage(); $img->setSaveFolder($this->cachepath); $img->setSource(self::DUMMY, $this->cachepath); $img->createDummyImage(); $img->generateFilename(null, false); $img->save(null, null, false); $filename = $img->getTarget(); $this->assertEquals(basename($filename), self::DUMMY . "_100_100", "Filename not as expected on dummy image."); } /** * Test * * @return void */ public function testCreate2() { $img = new CImage(); $img->setSaveFolder($this->cachepath); $img->setSource(self::DUMMY, $this->cachepath); $img->createDummyImage(200, 400); $img->generateFilename(null, false); $img->save(null, null, false); $filename = $img->getTarget(); $this->assertEquals(basename($filename), self::DUMMY . "_200_400", "Filename not as expected on dummy image."); } } ================================================ FILE: test/CImageRemoteDownloadTest.php ================================================ setRemoteDownload(true, ""); $res = $img->isRemoteSource($source); $this->assertTrue($res, "Should be a valid remote source: '$source'."); } /** * Test * * @return void * * @dataProvider providerInvalidRemoteSource */ public function testAllowRemoteDownloadDefaultPatternInvalid($source) { $img = new CImage(); $img->setRemoteDownload(true, ""); $res = $img->isRemoteSource($source); $this->assertFalse($res, "Should not be a valid remote source: '$source'."); } /** * Provider for hostname matching the whitelist. * * @return array */ public function providerHostnameMatch() { return [ [ "any.facebook.com", "images.ak.instagram.com", "google.com", ], ]; } /** * Test * * @param string $hostname matches the whitelist * * @return void * * @dataProvider providerHostnameMatch * */ public function testRemoteHostWhitelistMatch($hostname) { $img = new CImage(); $img->setRemoteHostWhitelist($this->remote_whitelist); $res = $img->isRemoteSourceOnWhitelist("http://$hostname/img.jpg"); $this->assertTrue($res, "Should be a valid hostname on the whitelist: '$hostname'."); } /** * Provider for hostname not matching the whitelist. * * @return array */ public function providerHostnameNoMatch() { return [ [ "example.com", ".com", "img.jpg", ], ]; } /** * Test * * @param string $hostname not matching the whitelist * * @return void * * @dataProvider providerHostnameNoMatch * */ public function testRemoteHostWhitelistNoMatch($hostname) { $img = new CImage(); $img->setRemoteHostWhitelist($this->remote_whitelist); $res = $img->isRemoteSourceOnWhitelist("http://$hostname/img.jpg"); $this->assertFalse($res, "Should not be a valid hostname on the whitelist: '$hostname'."); } /** * Test * * @return void * */ public function testRemoteHostWhitelistNotConfigured() { $img = new CImage(); $res = $img->isRemoteSourceOnWhitelist(null); $this->assertTrue($res, "Should allow when whitelist not configured."); } } ================================================ FILE: test/CImageSRGBTest.php ================================================ srgbColorProfile = __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc'; $this->cache = CACHE_PATH . "/" . $this->srgbDir; if (!is_writable($this->cache)) { mkdir($this->cache); } } /** * Test * * @return void */ public function testCreate1() { $img = new CImage(); $filename = $img->convert2sRGBColorSpace( 'car.png', IMAGE_PATH, $this->cache, $this->srgbColorProfile ); if (class_exists("Imagick")) { $this->assertEquals("srgb_car.png", basename($filename), "Filename not as expected on image."); } else { $this->assertFalse($filename, "ImageMagick not installed, silent fail"); } } /** * Test * * @return void */ public function testCreate2() { $img = new CImage(); $filename = $img->convert2sRGBColorSpace( 'car.jpg', IMAGE_PATH, $this->cache, $this->srgbColorProfile ); $this->assertFalse($filename); } } ================================================ FILE: test/CWhitelistTest.php ================================================ set($this->remote_whitelist); $res = $whitelist->check($hostname); $this->assertTrue($res, "Should be a valid hostname on the whitelist: '$hostname'."); } /** * Test * * @param string $hostname not matching the whitelist * * @return void * * @dataProvider providerHostnameNoMatch * */ public function testRemoteHostWhitelistNoMatch($hostname) { $whitelist = new CWhitelist(); $whitelist->set($this->remote_whitelist); $res = $whitelist->check($hostname); $this->assertFalse($res, "Should not be a valid hostname on the whitelist: '$hostname'."); } /** * Test * * @expectedException Exception * * @return void * */ public function testInvalidFormat() { $whitelist = new CWhitelist(); $whitelist->set("should fail"); } /** * Test * * @return void * */ public function testCheckEmpty() { $whitelist = new CWhitelist(); $whitelist->set($this->remote_whitelist); $hostname = ""; $res = $whitelist->check($hostname); $this->assertFalse($res, "Should not be a valid hostname on the whitelist: '$hostname'."); } } ================================================ FILE: test/config.php ================================================
'; echo 'Running on: ' . htmlentities($_SERVER['SERVER_SOFTWARE']) . '

'; $no = extension_loaded('exif') ? null : 'NOT'; echo "Extension exif is $no loaded.
"; $no = extension_loaded('curl') ? null : 'NOT'; echo "Extension curl is $no loaded.
"; $no = extension_loaded('imagick') ? null : 'NOT'; echo "Extension imagick is $no loaded.
"; $no = extension_loaded('gd') ? null : 'NOT'; echo "Extension gd is $no loaded.
"; if (!$no) { echo "
", var_dump(gd_info()), "
"; } echo "Checking path for postprocessing tools"; echo "
pngquant: "; system("which pngquant"); echo "
optipng: "; system("which optipng"); echo "
pngout: "; system("which pngout"); echo "
jpegtran: "; system("which jpegtran"); ================================================ FILE: webroot/compare/compare-test.php ================================================

Compare images

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 1
Image 2
Image 3
Image 4
Image 5
Image 6
================================================ FILE: webroot/compare/issue117-PNG24.php ================================================ 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.
"; $no = extension_loaded('curl') ? null : 'NOT'; $text .= "Extension curl is $no loaded.
"; $no = extension_loaded('imagick') ? null : 'NOT'; $text .= "Extension imagick is $no loaded.
"; $no = extension_loaded('gd') ? null : 'NOT'; $text .= "Extension gd is $no loaded.
"; $text .= checkExternalCommand("PNG LOSSY", $postProcessing["png_lossy"], $postProcessing["png_lossy_cmd"]); $text .= checkExternalCommand("PNG FILTER", $postProcessing["png_filter"], $postProcessing["png_filter_cmd"]); $text .= checkExternalCommand("PNG DEFLATE", $postProcessing["png_deflate"], $postProcessing["png_deflate_cmd"]); $text .= checkExternalCommand("JPEG OPTIMIZE", $postProcessing["jpeg_optimize"], $postProcessing["jpeg_optimize_cmd"]); if (!$no) { $text .= print_r(gd_info(), 1); } echo << CImage status
$text
EOD; 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 << CImage verbose output $url1




EOD;
}



/**
 * Load, process and output the image
 */
$img->log("PHP version: " . phpversion())
    ->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,

            // Postprocessing using external tools
            'lossy' => $lossy,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();


================================================
FILE: webroot/img_config.php
================================================
 'production',
     'mode' => 'development',
     //'mode' => 'strict',



    /**
     * Where to find the autoloader.
     *
     * Default values:
     *  autoloader:  null
     */
    'autoloader'   =>  __DIR__ . '/../autoload.php',



    /**
     * Paths, where are the images stored and where is the cache.
     * End all paths with a slash.
     *
     * Default values:
     *  image_path:     __DIR__ . '/img/'
     *  cache_path:     __DIR__ . '/../cache/'
     *  alias_path:     null
     */
    'image_path'        =>  __DIR__ . '/img/',
    'cache_path'        =>  __DIR__ . '/../cache/',
    'alias_path'   =>  __DIR__ . '/img/alias/',



    /**
     * Fast track cache. Save a json representation of the image as a
     * fast track to the cached version of the image. This avoids some
     * processing and allows for quicker load times of cached images.
     *
     * Default values:
     *  fast_track_allow: false
     */
    //'fast_track_allow' => true,



    /**
     * Class names to use, to ease dependency injection. You can change Class
     * name if you want to use your own class instead. This is a way to extend
     * the codebase.
     *
     * Default values:
     *  CImage: CImage
     *  CCache: CCache
     *  CFastTrackCache: CFastTrackCache
     */
     //'CImage' => 'CImage',
     //'CCache' => 'CCache',
     //'CFastTrackCache' => 'CFastTrackCache',



    /**
     * Use password to protect from missusage, send &pwd=... or &password=..
     * with the request to match the password or set to false to disable.
     * Passwords are only used together with options for remote download
     * and aliasing.
     *
     * Create a passwords like this, depending on the type used:
     *  text: 'my_password'
     *  md5:  md5('my_password')
     *  hash: password_hash('my_password', PASSWORD_DEFAULT)
     *
     * Default values.
     *  password_always: false  // do not always require password,
     *  password:        false  // as in do not use password
     *  password_type:   'text' // use plain password, not encoded,
     */
    //'password_always' => false, // always require password,
    //'password'        => "moped", // "secret-password",
    //'password_type'   => 'text', // supports 'text', 'md5', 'hash',



    /**
     * Allow or disallow downloading of remote images available on
     * remote servers. Default is to disallow remote download.
     *
     * When enabling remote download, the default is to allow download any
     * link starting with http or https. This can be changed using
     * remote_pattern.
     *
     * When enabling remote_whitelist a check is made that the hostname of the
     * source to download matches the whitelist. By default the check is
     * disabled and thereby allowing download from any hosts.
     *
     * Default values.
     *  remote_allow:     false
     *  remote_pattern:   null  // use default values from CImage which is to
     *                          // allow download from any http- and
     *                          // https-source.
     *  remote_whitelist: null  // use default values from CImage which is to
     *                          // allow download from any hosts.
     */
    //'remote_allow'     => true,
    //'remote_pattern'   => '#^https?://#',
    //'remote_whitelist' => array(
    //    '\.facebook\.com$',
    //    '^(?:images|photos-[a-z])\.ak\.instagram\.com$',
    //    '\.google\.com$'
    //),



    /**
     * Use backup image if src-image is not found on disk. The backup image
     * is only available for local images and based on wether the original
     * image is found on disk or not. The backup image must be a local image
     * or the dummy image.
     *
     * Default value:
     *  src_alt:  null //disabled by default
     */
     //'src_alt' => 'car.png',
     //'src_alt' => 'dummy',



    /**
     * A regexp for validating characters in the image or alias filename.
     *
     * Default value:
     *  valid_filename:  '#^[a-z0-9A-Z-/_ \.:]+$#'
     *  valid_aliasname: '#^[a-z0-9A-Z-_]+$#'
     */
     //'valid_filename'  => '#^[a-z0-9A-Z-/_ \.:]+$#',
     //'valid_aliasname' => '#^[a-z0-9A-Z-_]+$#',



     /**
      * Change the default values for CImage quality and compression used
      * when saving images.
      *
      * Default value:
      *  jpg_quality:     null, integer between 0-100
      *  png_compression: null, integer between 0-9
      */
      //'jpg_quality'  => 75,
      //'png_compression' => 1,



      /**
       * Convert the image to srgb before processing. Saves the converted
       * image in a cache subdir 'srgb'. This option is default false but can
       * be changed to default true to do this conversion for all images.
       * This option requires PHP extension imagick and will silently fail
       * if that is not installed.
       *
       * Default value:
       *  srgb_default:      false
       *  srgb_colorprofile: __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc'
       */
       //'srgb_default' => false,
       //'srgb_colorprofile' => __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc',



       /**
        * Set skip-original to true to always process the image and use
        * the cached version. Default is false and to use the original
        * image when its no processing needed.
        *
        * Default value:
        *  skip_original: false
        */
        //'skip_original' => true,



      /**
       * A function (hook) can be called after img.php has processed all
       * configuration options and before processing the image using CImage.
       * The function receives the $img variabel and an array with the
       * majority of current settings.
       *
       * Default value:
       *  hook_before_CImage:     null
       */
       /*'hook_before_CImage' => function (CImage $img, Array $allConfig) {
           if ($allConfig['newWidth'] > 10) {
               $allConfig['newWidth'] *= 2;
           }
           return $allConfig;
       },*/



       /**
        * Add header for cache control when outputting images.
        *
        * Default value:
        *  cache_control: null, or set to string
        */
        //'cache_control' => "max-age=86400",



     /**
      * The name representing a dummy image which is automatically created
      * and stored as a image in the dir CACHE_PATH/dummy. The dummy image
      * can then be used as a placeholder image.
      * The dir CACHE_PATH/dummy is automatically created when needed.
      * Write protect the CACHE_PATH/dummy to prevent creation of new
      * dummy images, but continue to use the existing ones.
      *
      * Default value:
      *  dummy_enabled:  true as default, disable dummy feature by setting
      *                  to false.
      *  dummy_filename: 'dummy' use this as ?src=dummy to create a dummy image.
      */
      //'dummy_enabled' => true,
      //'dummy_filename' => 'dummy',



     /**
     * Check that the imagefile is a file below 'image_path' using realpath().
     * Security constraint to avoid reaching images outside image_path.
     * This means that symbolic links to images outside the image_path will
     * fail.
     *
     * Default value:
     *  image_path_constraint: true
     */
     //'image_path_constraint' => false,



     /**
     * Set default timezone.
     *
     * Default values.
     *  default_timezone: ini_get('default_timezone') or 'UTC'
     */
    //'default_timezone' => 'UTC',



    /**
     * Max image dimensions, larger dimensions results in 404.
     * This is basically a security constraint to avoid using resources on creating
     * large (unwanted) images.
     *
     * Default values.
     *  max_width:  2000
     *  max_height: 2000
     */
    //'max_width'     => 2000,
    //'max_height'    => 2000,



    /**
     * Set default background color for all images. Override it using
     * option bgColor.
     * Colorvalue is 6 digit hex string between 000000-FFFFFF
     * or 8 digit hex string if using the alpha channel where
     * the alpha value is between 00 (opaqe) and 7F (transparent),
     * that is between 00000000-FFFFFF7F.
     *
     * Default values.
     *  background_color: As specified by CImage
     */
    //'background_color' => "FFFFFF",
    //'background_color' => "FFFFFF7F",



    /**
     * Post processing of images using external tools, set to true or false
     * and set command to be executed.
     *
     * The png_lossy can alos have a value of null which means that its
     * enabled but not used as default. Each image having the option
     * &lossy will be processed. This means one can individually choose
     * when to use the lossy processing.
     *
     * Default values.
     *
     *  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'
     */
    /*
    'postprocessing' => array(
        'png_lossy'       => null,
        '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',
    ),
    */



    /**
     * Create custom convolution expressions, matrix 3x3, divisor and
     * offset.
     *
     * Default values.
     *  convolution_constant: array()
     */
    /*
    'convolution_constant' => array(
        //'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',
    ),
    */



    /**
     * Prevent leeching of images by controlling the hostname of those who
     * can access the images. Default is to allow hotlinking.
     *
     * Password apply when hotlinking is disallowed, use password to allow
     * hotlinking.
     *
     * The whitelist is an array of regexpes for allowed hostnames that can
     * hotlink images.
     *
     * Default values.
     *  allow_hotlinking:     true
     *  hotlinking_whitelist: array()
     */
     /*
    'allow_hotlinking' => false,
    'hotlinking_whitelist' => array(
        '^dbwebb\.se$',
    ),
    */


    /**
     * Create custom shortcuts for more advanced expressions.
     *
     * Default values.
     *  shortcut: array(
     *      'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
     *  )
     */
     /*
    'shortcut' => array(
        'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
    ),*/



    /**
     * Predefined size constants.
     *
     * These can be used together with &width or &height to create a constant value
     * for a width or height where can be changed in one place.
     * Useful when your site changes its layout or if you have a grid to fit images into.
     *
     * Example:
     *  &width=w1  // results in width=613
     *  &width=c2  // results in spanning two columns with a gutter, 30*2+10=70
     *  &width=c24 // results in spanning whole grid 24*30+((24-1)*10)=950
     *
     * Default values.
     *  size_constant: As specified by the function below.
     */
    /*
    '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;
    },*/



    /**
     * Predefined aspect ratios.
     *
     * Default values.
     *  aspect_ratio_constant: As the function below.
     */
    /*'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,
        );
    },*/



    /**
     * Default options for ascii image.
     *
     * Default values as specified below in the array.
     *  ascii-options:
     *   characterSet:       Choose any character set available in CAsciiArt.
     *   scale:              How many pixels should each character
     *                       translate to.
     *   luminanceStrategy:  Choose any strategy available in CAsciiArt.
     *   customCharacterSet: Define your own character set.
     */
    /*'ascii-options' => array(
            "characterSet" => 'two',
            "scale" => 14,
            "luminanceStrategy" => 3,
            "customCharacterSet" => null,
        ), */



    /**
     * Default options  for creating interlaced progressive JPEG images. Set
     * to true to always render jpeg images as interlaced. This setting can
     * be overridden by using `?interlace`, `?interlace=true` or
     * `?interlace=false`.
     *
     * Default values are:
     *  interlace:  false
     */
     /*'interlace' => false,*/
);


================================================
FILE: webroot/img_header.php
================================================
 'production',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);


================================================
FILE: webroot/imgd.php
================================================
 'development',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);



// Version of cimage and img.php
define("CIMAGE_VERSION", "v0.8.6 (2023-10-27)");

// For CRemoteImage
define("CIMAGE_USER_AGENT", "CImage/" . CIMAGE_VERSION);

// Image type IMG_WEBP is only defined from 5.6.25
if (!defined("IMG_WEBP")) {
    define("IMG_WEBP", -1);
}



/**
 * General functions to use in img.php.
 */



/**
 * Trace and log execution to logfile, useful for debugging and development.
 *
 * @param string $msg message to log to file.
 *
 * @return void
 */
function trace($msg)
{
    $file = CIMAGE_DEBUG_FILE;
    if (!is_writable($file)) {
        return;
    }

    $timer = number_format((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 6);
    $details  = "{$timer}ms";
    $details .= ":" . round(memory_get_peak_usage()/1024/1024, 3) . "MB";
    $details .= ":" . count(get_included_files());
    file_put_contents($file, "$details:$msg\n", FILE_APPEND);
}



/**
 * Display error message.
 *
 * @param string $msg to display.
 * @param int $type of HTTP error to display.
 *
 * @return void
 */
function errorPage($msg, $type = 500)
{
    global $mode;

    switch ($type) {
        case 403:
            $header = "403 Forbidden";
            break;
        case 404:
            $header = "404 Not Found";
            break;
        default:
            $header = "500 Internal Server Error";
    }

    if ($mode == "strict") {
        $header = "404 Not Found";
    }

    header("HTTP/1.0 $header");

    if ($mode == "development") {
        die("[img.php] $msg");
    }

    error_log("[img.php] $msg");
    die("HTTP/1.0 $header");
}



/**
 * 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 of input from query string or else $undefined.
 *
 * @param mixed $key       as string or array of string values to look for in $_GET.
 * @param mixed $undefined value to return when $key has no, or empty value in $_GET.
 *
 * @return mixed value as or $undefined.
 */
function getValue($key, $undefined)
{
    $val = get($key);
    if (is_null($val) || $val === "") {
        return $undefined;
    }
    return $val;
}



/**
 * 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, $arg = "")
{
    global $verbose, $verboseFile;
    static $log = array();

    if (!($verbose || $verboseFile)) {
        return;
    }

    if (is_null($msg)) {
        return $log;
    }

    if (is_null($arg)) {
        $arg = "null";
    } elseif ($arg === false) {
        $arg = "false";
    } elseif ($arg === true) {
        $arg = "true";
    }

    $log[] = $msg . $arg;
}



/**
 * Log when verbose mode, when used without argument it returns the result.
 *
 * @param string $msg to log.
 *
 * @return void or array.
 */
function checkExternalCommand($what, $enabled, $commandString)
{
    $no = $enabled ? null : 'NOT';
    $text = "Post processing $what is $no enabled.
"; list($command) = explode(" ", $commandString); $no = is_executable($command) ? null : 'NOT'; $text .= "The command for $what is $no an executable.
"; return $text; } /** * Get a image from a remote server using HTTP GET and If-Modified-Since. * */ class CHttpGet { private $request = array(); private $response = array(); /** * Constructor * */ public function __construct() { $this->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']; } } /** * 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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } if (!is_null($this->verboseFileName)) { file_put_contents( $this->verboseFileName, str_replace("
", "\n", $log) ); } else { echo <<CImage Verbose Output
{$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.
"; $no = extension_loaded('curl') ? null : 'NOT'; $text .= "Extension curl is $no loaded.
"; $no = extension_loaded('imagick') ? null : 'NOT'; $text .= "Extension imagick is $no loaded.
"; $no = extension_loaded('gd') ? null : 'NOT'; $text .= "Extension gd is $no loaded.
"; $text .= checkExternalCommand("PNG LOSSY", $postProcessing["png_lossy"], $postProcessing["png_lossy_cmd"]); $text .= checkExternalCommand("PNG FILTER", $postProcessing["png_filter"], $postProcessing["png_filter_cmd"]); $text .= checkExternalCommand("PNG DEFLATE", $postProcessing["png_deflate"], $postProcessing["png_deflate_cmd"]); $text .= checkExternalCommand("JPEG OPTIMIZE", $postProcessing["jpeg_optimize"], $postProcessing["jpeg_optimize_cmd"]); if (!$no) { $text .= print_r(gd_info(), 1); } echo << CImage status
$text
EOD; 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 << CImage verbose output $url1




EOD;
}



/**
 * Load, process and output the image
 */
$img->log("PHP version: " . phpversion())
    ->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,

            // Postprocessing using external tools
            'lossy' => $lossy,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();





================================================
FILE: webroot/imgf.php
================================================
  true,
        "autoloader" =>  __DIR__ . "/../autoload.php",
        "cache_path" =>  __DIR__ . "/../cache/",
    );
}

// Make CIMAGE_DEBUG false by default, if not already defined
if (!defined("CIMAGE_DEBUG")) {
    define("CIMAGE_DEBUG", false);
}

// Debug mode needs additional functions
if (CIMAGE_DEBUG) {
    require $config["autoloader"];
}

// Cache path must be valid
$cacheIsReadable = is_dir($config["cache_path"]) && is_readable($config["cache_path"]);
if (!$cacheIsReadable) {
    die("imgf.php: Cache is not readable, check path in configfile.");
}

// Prepare to check if fast cache should be used
$cachePath = $config["cache_path"] . "/fasttrack";
$query = $_GET;

// Do not use cache when no-cache is active
$useCache = !(array_key_exists("no-cache", $query) || array_key_exists("nc", $query));

// Only use cache if enabled by configuration
$useCache = $useCache && isset($config["fast_track_allow"]) && $config["fast_track_allow"] === true;

// Remove parts from querystring that should not be part of filename
$clear = array("nc", "no-cache");
foreach ($clear as $value) {
    unset($query[$value]);
}

// Create the cache filename
arsort($query);
$queryAsString = http_build_query($query);
$filename = md5($queryAsString);
$filename = "$cachePath/$filename";

// Check cached item, if any
if ($useCache && is_readable($filename)) {
    $item = json_decode(file_get_contents($filename), true);

    if (is_readable($item["source"])) {
        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("imgf 304");
            }
            exit;
        }

        foreach ($item["header-output"] as $value) {
            header($value);
        }

        if (CIMAGE_DEBUG) {
            trace("imgf 200");
        }
        readfile($item["source"]);
        exit;
    }
}

// No fast track cache, proceed as usual
include __DIR__ . "/img.php";


================================================
FILE: webroot/imgp.php
================================================
 'production',               // 'production', 'development', 'strict'
    //'image_path'   =>  __DIR__ . '/img/',
    //'cache_path'   =>  __DIR__ . '/../cache/',
    //'alias_path'   =>  __DIR__ . '/img/alias/',
    //'remote_allow' => true,
    //'password'     => false,                      // "secret-password",

);



// Version of cimage and img.php
define("CIMAGE_VERSION", "v0.8.6 (2023-10-27)");

// For CRemoteImage
define("CIMAGE_USER_AGENT", "CImage/" . CIMAGE_VERSION);

// Image type IMG_WEBP is only defined from 5.6.25
if (!defined("IMG_WEBP")) {
    define("IMG_WEBP", -1);
}



/**
 * General functions to use in img.php.
 */



/**
 * Trace and log execution to logfile, useful for debugging and development.
 *
 * @param string $msg message to log to file.
 *
 * @return void
 */
function trace($msg)
{
    $file = CIMAGE_DEBUG_FILE;
    if (!is_writable($file)) {
        return;
    }

    $timer = number_format((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 6);
    $details  = "{$timer}ms";
    $details .= ":" . round(memory_get_peak_usage()/1024/1024, 3) . "MB";
    $details .= ":" . count(get_included_files());
    file_put_contents($file, "$details:$msg\n", FILE_APPEND);
}



/**
 * Display error message.
 *
 * @param string $msg to display.
 * @param int $type of HTTP error to display.
 *
 * @return void
 */
function errorPage($msg, $type = 500)
{
    global $mode;

    switch ($type) {
        case 403:
            $header = "403 Forbidden";
            break;
        case 404:
            $header = "404 Not Found";
            break;
        default:
            $header = "500 Internal Server Error";
    }

    if ($mode == "strict") {
        $header = "404 Not Found";
    }

    header("HTTP/1.0 $header");

    if ($mode == "development") {
        die("[img.php] $msg");
    }

    error_log("[img.php] $msg");
    die("HTTP/1.0 $header");
}



/**
 * 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 of input from query string or else $undefined.
 *
 * @param mixed $key       as string or array of string values to look for in $_GET.
 * @param mixed $undefined value to return when $key has no, or empty value in $_GET.
 *
 * @return mixed value as or $undefined.
 */
function getValue($key, $undefined)
{
    $val = get($key);
    if (is_null($val) || $val === "") {
        return $undefined;
    }
    return $val;
}



/**
 * 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, $arg = "")
{
    global $verbose, $verboseFile;
    static $log = array();

    if (!($verbose || $verboseFile)) {
        return;
    }

    if (is_null($msg)) {
        return $log;
    }

    if (is_null($arg)) {
        $arg = "null";
    } elseif ($arg === false) {
        $arg = "false";
    } elseif ($arg === true) {
        $arg = "true";
    }

    $log[] = $msg . $arg;
}



/**
 * Log when verbose mode, when used without argument it returns the result.
 *
 * @param string $msg to log.
 *
 * @return void or array.
 */
function checkExternalCommand($what, $enabled, $commandString)
{
    $no = $enabled ? null : 'NOT';
    $text = "Post processing $what is $no enabled.
"; list($command) = explode(" ", $commandString); $no = is_executable($command) ? null : 'NOT'; $text .= "The command for $what is $no an executable.
"; return $text; } /** * Get a image from a remote server using HTTP GET and If-Modified-Since. * */ class CHttpGet { private $request = array(); private $response = array(); /** * Constructor * */ public function __construct() { $this->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']; } } /** * 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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } if (!is_null($this->verboseFileName)) { file_put_contents( $this->verboseFileName, str_replace("
", "\n", $log) ); } else { echo <<CImage Verbose Output
{$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.
"; $no = extension_loaded('curl') ? null : 'NOT'; $text .= "Extension curl is $no loaded.
"; $no = extension_loaded('imagick') ? null : 'NOT'; $text .= "Extension imagick is $no loaded.
"; $no = extension_loaded('gd') ? null : 'NOT'; $text .= "Extension gd is $no loaded.
"; $text .= checkExternalCommand("PNG LOSSY", $postProcessing["png_lossy"], $postProcessing["png_lossy_cmd"]); $text .= checkExternalCommand("PNG FILTER", $postProcessing["png_filter"], $postProcessing["png_filter_cmd"]); $text .= checkExternalCommand("PNG DEFLATE", $postProcessing["png_deflate"], $postProcessing["png_deflate_cmd"]); $text .= checkExternalCommand("JPEG OPTIMIZE", $postProcessing["jpeg_optimize"], $postProcessing["jpeg_optimize_cmd"]); if (!$no) { $text .= print_r(gd_info(), 1); } echo << CImage status
$text
EOD; 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 << CImage verbose output $url1




EOD;
}



/**
 * Load, process and output the image
 */
$img->log("PHP version: " . phpversion())
    ->log("Incoming arguments: " . print_r(verbose(), 1))
    ->setSaveFolder($cachePath)
    ->useCache($useCache)
    ->setSource($srcImage, $imagePath)
    ->setOptions(
        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,

            // Postprocessing using external tools
            'lossy' => $lossy,
        )
    )
    ->loadImageDetails()
    ->initDimensions()
    ->calculateNewWidthAndHeight()
    ->setSaveAsExtension($saveAs)
    ->setJpegQuality($quality)
    ->setPngCompression($compress)
    ->useOriginalIfPossible($useOriginal)
    ->generateFilename($cachePath)
    ->useCacheIfPossible($useCache)
    ->load()
    ->preResize()
    ->resize()
    ->postResize()
    ->setPostProcessingOptions($postProcessing)
    ->save()
    ->linkToCacheFile($aliasTarget)
    ->output();





================================================
FILE: webroot/imgs.php
================================================
 'strict', ); define("CIMAGE_VERSION", "v0.8.6 (2023-10-27)"); define("CIMAGE_USER_AGENT", "CImage/" . CIMAGE_VERSION); if (!defined("IMG_WEBP")) { define("IMG_WEBP", -1); } function trace($msg) { $file = CIMAGE_DEBUG_FILE; if (!is_writable($file)) { return; } $timer = number_format((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 6); $details = "{$timer}ms"; $details .= ":" . round(memory_get_peak_usage()/1024/1024, 3) . "MB"; $details .= ":" . count(get_included_files()); file_put_contents($file, "$details:$msg\n", FILE_APPEND); } function errorPage($msg, $type = 500) { global $mode; switch ($type) { case 403: $header = "403 Forbidden"; break; case 404: $header = "404 Not Found"; break; default: $header = "500 Internal Server Error"; } if ($mode == "strict") { $header = "404 Not Found"; } header("HTTP/1.0 $header"); if ($mode == "development") { die("[img.php] $msg"); } error_log("[img.php] $msg"); die("HTTP/1.0 $header"); } 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; } function getDefined($key, $defined, $undefined) { return get($key) === null ? $undefined : $defined; } function getValue($key, $undefined) { $val = get($key); if (is_null($val) || $val === "") { return $undefined; } return $val; } function getConfig($key, $default) { global $config; return isset($config[$key]) ? $config[$key] : $default; } function verbose($msg = null, $arg = "") { global $verbose, $verboseFile; static $log = array(); if (!($verbose || $verboseFile)) { return; } if (is_null($msg)) { return $log; } if (is_null($arg)) { $arg = "null"; } elseif ($arg === false) { $arg = "false"; } elseif ($arg === true) { $arg = "true"; } $log[] = $msg . $arg; } function checkExternalCommand($what, $enabled, $commandString) { $no = $enabled ? null : 'NOT'; $text = "Post processing $what is $no enabled.
"; list($command) = explode(" ", $commandString); $no = is_executable($command) ? null : 'NOT'; $text .= "The command for $what is $no an executable.
"; return $text; } class CHttpGet { private $request = array(); private $response = array(); public function __construct() { $this->request['header'] = array(); } 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; } 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; } public function setHeader($field, $value) { $this->request['header'][] = "$field: $value"; return $this; } public function parseHeader() { $rawHeaders = rtrim($this->response['headerRaw'], "\r\n"); $headerGroups = explode("\r\n\r\n", $rawHeaders); $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; } 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; } 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) . '
'; } } else { $log .= htmlentities($val) . '
'; } } if (!is_null($this->verboseFileName)) { file_put_contents( $this->verboseFileName, str_replace("
", "\n", $log) ); } else { echo <<CImage Verbose Output
{$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.
"; $no = extension_loaded('curl') ? null : 'NOT'; $text .= "Extension curl is $no loaded.
"; $no = extension_loaded('imagick') ? null : 'NOT'; $text .= "Extension imagick is $no loaded.
"; $no = extension_loaded('gd') ? null : 'NOT'; $text .= "Extension gd is $no loaded.
"; $text .= checkExternalCommand("PNG LOSSY", $postProcessing["png_lossy"], $postProcessing["png_lossy_cmd"]); $text .= checkExternalCommand("PNG FILTER", $postProcessing["png_filter"], $postProcessing["png_filter_cmd"]); $text .= checkExternalCommand("PNG DEFLATE", $postProcessing["png_deflate"], $postProcessing["png_deflate_cmd"]); $text .= checkExternalCommand("JPEG OPTIMIZE", $postProcessing["jpeg_optimize"], $postProcessing["jpeg_optimize_cmd"]); if (!$no) { $text .= print_r(gd_info(), 1); } echo << CImage status
$text
EOD; 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 << CImage verbose output $url1




EOD;
} $img->log("PHP version: " . phpversion()) ->log("Incoming arguments: " . print_r(verbose(), 1)) ->setSaveFolder($cachePath) ->useCache($useCache) ->setSource($srcImage, $imagePath) ->setOptions( 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, 'lossy' => $lossy, ) ) ->loadImageDetails() ->initDimensions() ->calculateNewWidthAndHeight() ->setSaveAsExtension($saveAs) ->setJpegQuality($quality) ->setPngCompression($compress) ->useOriginalIfPossible($useOriginal) ->generateFilename($cachePath) ->useCacheIfPossible($useCache) ->load() ->preResize() ->resize() ->postResize() ->setPostProcessingOptions($postProcessing) ->save() ->linkToCacheFile($aliasTarget) ->output(); 

================================================
FILE: webroot/js/cimage.js
================================================
/**
 * JavaScript utilities for CImage and img.php.
 */
window.CImage = (function() {
    "use strict";


    /**
     * Waiting for ECMA 6...
     */
     var forEach = function(array, callback, scope) {
      for (var i = 0; i < array.length; i++) {
        callback.call(scope, i, array[i]);
      }
    };



    /**
     * Update the permalink.
     */
    function updatePermaLink() {
        var link,
            input1 = document.getElementById("input1"),
            input2 = document.getElementById("input2"),
            input3 = document.getElementById("input3"),
            input4 = document.getElementById("input4"),
            input5 = document.getElementById("input5"),
            input6 = document.getElementById("input6"),
            details = document.getElementById("viewDetails"),
            stack = document.getElementById("stack"),
            bg = document.getElementById("bg"),
            permalink = document.getElementById("permalink");
            
        link  = "?";
        link += "input1=" + encodeURIComponent(input1.value) + "&";
        link += "input2=" + encodeURIComponent(input2.value) + "&";
        link += "input3=" + encodeURIComponent(input3.value) + "&";
        link += "input4=" + encodeURIComponent(input4.value) + "&";
        link += "input5=" + encodeURIComponent(input5.value) + "&";
        link += "input6=" + encodeURIComponent(input6.value) + "&";
        link += "json=" + encodeURIComponent(details.checked) + "&";
        link += "stack=" + encodeURIComponent(stack.checked) + "&";
        link += "bg=" + encodeURIComponent(bg.checked);
        permalink.href = link;
    }



    /**
     * Init the compare page with details.
     */
    function compareLoadImage(event) {
        var img, json, button, area, deck, id, permalink;

        id = this.dataset.id;
        img = document.getElementById("img" + id);
        json = document.getElementById("json" + id);
        button = document.getElementById("button" + id);
        area = document.getElementById("area" + id);
        deck = document.getElementById("deck" + id);
        
        updatePermaLink();
        
        if (this.value == "") {
            // Clear image if input is cleared
            button.setAttribute("disabled", "disabled");
            area.classList.add("hidden");
            button.classList.remove("selected");
            return;
        }

        // Display image in its area
        img.src = this.value;
        area.classList.remove("hidden");

        $.getJSON(this.value + "&json", function(data) {
            json.innerHTML = "filename: " + data.filename + "\ncolors: " + data.colors + "\nsize: " + data.size + "\nwidth: " + data.width + "\nheigh: " + data.height + "\naspect-ratio: " + data.aspectRatio + "\npng-type: " + data.pngType;
        })
            .fail(function() {
                json.innerHTML = "Details not available."
                console.log( "JSON error" );
            });

        // Display image in overlay
        button.removeAttribute("disabled");
        button.classList.add("selected");


    };



    /**
     * Init the compare page with details.
     */
    function compareInit(options) 
    {
        var elements, id, onTop, myEvent,
            input1 = document.getElementById("input1"),
            input2 = document.getElementById("input2"),
            input3 = document.getElementById("input3"),
            input4 = document.getElementById("input4"),
            input5 = document.getElementById("input5"),
            input6 = document.getElementById("input6"),
            details = document.getElementById("viewDetails"),
            stack = document.getElementById("stack"),
            bg = document.getElementById("bg"),
            buttons = document.getElementById("buttonWrap");

        input1.addEventListener("change", compareLoadImage);
        input2.addEventListener("change", compareLoadImage);
        input3.addEventListener("change", compareLoadImage);
        input4.addEventListener("change", compareLoadImage);
        input5.addEventListener("change", compareLoadImage);
        input6.addEventListener("change", compareLoadImage);

        // Toggle json
        details.addEventListener("change", function() {
            var elements = document.querySelectorAll(".json");
            
            forEach(elements, function (index, element) {
                element.classList.toggle("hidden");
            });

            /* ECMA 6
            for (var element of elements) {
                element.classList.toggle("hidden");
            }
            */
            
            updatePermaLink();
            console.log("View JSON");
        });

        // Show json as default
        if (options.json === true) {
            details.setAttribute("checked", "checked");
            myEvent = new CustomEvent("change");
            details.dispatchEvent(myEvent);
        }

        // Toggle background color
        bg.addEventListener("change", function() {
            var elements = document.querySelectorAll(".area");

            forEach(elements, function (index, element) {
                element.classList.toggle("invert");
            });
        });

        // Check background
        if (options.bg === true) {
            bg.setAttribute("checked", "checked");
            bg.classList.toggle("invert");
            myEvent = new CustomEvent("change");
            bg.dispatchEvent(myEvent);
        }

        // Toggle stack
        stack.addEventListener("change", function() {
            var element,
                elements = document.querySelectorAll(".area");

            buttons.classList.toggle("hidden");

            forEach(elements, function (index, element) {
                element.classList.toggle("stack");

                if (!element.classList.contains('hidden')) {
                    onTop = element;
                }
            });

            /* ECMA 6
            for (element of elements) {
                element.classList.toggle("stack");

                if (!element.classList.contains('hidden')) {
                    onTop = element;
                }
            }
            */

            onTop.classList.toggle("top");
            updatePermaLink();

            console.log("Stacking");
        });

        // Stack as default
        if (options.stack === true) {
            stack.setAttribute("checked", "checked");
            myEvent = new CustomEvent("change");
            stack.dispatchEvent(myEvent);
        }

        // Button clicks
        elements = document.querySelectorAll(".button");

        forEach(elements, function (index, element) {
            element.addEventListener("click", function() {
                var id = this.dataset.id,
                    area = document.getElementById("area" + id);

                area.classList.toggle("top");
                onTop.classList.toggle("top");
                onTop = area;
                console.log("button" + id);
            });
        });

        /* ECMA 6
        for (var element of elements) {
            element.addEventListener("click", function() {
                var id = this.dataset.id,
                    area = document.getElementById("area" + id);
                
                area.classList.toggle("top");
                onTop.classList.toggle("top");
                onTop = area;
                console.log("button" + id);
            });
        }
        */

        input1.value = options.input1 || null;
        input2.value = options.input2 || null;
        input3.value = options.input3 || null;
        input4.value = options.input4 || null;
        input5.value = options.input5 || null;
        input6.value = options.input6 || null;

        compareLoadImage.call(input1);
        compareLoadImage.call(input2);
        compareLoadImage.call(input3);
        compareLoadImage.call(input4);
        compareLoadImage.call(input5);
        compareLoadImage.call(input6);

        console.log(options);
    } 


    return {
        "compare": compareInit
    };

}());


================================================
FILE: webroot/test/config.php
================================================


  
  <?=$title?>
  
  





Images used in test

The following images are used for this test.

(json) (verbose)








Testcases used for each image

The following testcases are used for each image.


For each image, apply all testcases

. Using source image

(json) (verbose)





Testcase :

(json) (verbose)








================================================
FILE: webroot/test/test.php
================================================


  
  Testing img resizing using CImage.php


Testing CImage.php through img.php

Testcases

'Original image', 'query'=>''), array('text'=>'Crop out a rectangle of 100x100, start by position 200x200.', 'query'=>'&crop=100,100,200,200'), array('text'=>'Crop out a full width rectangle with height of 200, start by position 0x100.', 'query'=>'&crop=0,200,0,100'), array('text'=>'Max width 200.', 'query'=>'&w=200'), array('text'=>'Max height 200.', 'query'=>'&h=200'), array('text'=>'Max width 200 and max height 200.', 'query'=>'&w=200&h=200'), array('text'=>'No-ratio makes image fit in area of width 200 and height 200.', 'query'=>'&w=200&h=200&no-ratio'), array('text'=>'Crop to fit in width 200 and height 200.', 'query'=>'&w=200&h=200&crop-to-fit'), array('text'=>'Crop to fit in width 200 and height 100.', 'query'=>'&w=200&h=100&crop-to-fit'), array('text'=>'Crop to fit in width 100 and height 200.', 'query'=>'&w=100&h=200&crop-to-fit'), array('text'=>'Quality 70', 'query'=>'&w=200&h=200&quality=70'), array('text'=>'Quality 40', 'query'=>'&w=200&h=200&quality=40'), array('text'=>'Quality 10', 'query'=>'&w=200&h=200&quality=10'), array('text'=>'Filter: Negate', 'query'=>'&w=200&h=200&f=negate'), array('text'=>'Filter: Grayscale', 'query'=>'&w=200&h=200&f=grayscale'), array('text'=>'Filter: Brightness 90', 'query'=>'&w=200&h=200&f=brightness,90'), array('text'=>'Filter: Contrast 50', 'query'=>'&w=200&h=200&f=contrast,50'), array('text'=>'Filter: Colorize 0,255,0,0', 'query'=>'&w=200&h=200&f=colorize,0,255,0,0'), array('text'=>'Filter: Edge detect', 'query'=>'&w=200&h=200&f=edgedetect'), array('text'=>'Filter: Emboss', 'query'=>'&w=200&h=200&f=emboss'), array('text'=>'Filter: Gaussian blur', 'query'=>'&w=200&h=200&f=gaussian_blur'), array('text'=>'Filter: Selective blur', 'query'=>'&w=200&h=200&f=selective_blur'), array('text'=>'Filter: Mean removal', 'query'=>'&w=200&h=200&f=mean_removal'), array('text'=>'Filter: Smooth 2', 'query'=>'&w=200&h=200&f=smooth,2'), array('text'=>'Filter: Pixelate 10,10', 'query'=>'&w=200&h=200&f=pixelate,10,10'), array('text'=>'Multiple filter: Negate, Grayscale and Pixelate 10,10', 'query'=>'&w=200&h=200&&f=negate&f0=grayscale&f1=pixelate,10,10'), array('text'=>'Crop with width & height and crop-to-fit with quality and filter', 'query'=>'&crop=100,100,100,100&w=200&h=200&crop-to-fit&q=70&f0=grayscale'), ); ?>

Test case with image wider.jpg

$val) { $url = "../img.php?src=wider.jpg{$val['query']}"; echo ""; } ?>
Test case with image wider.jpg
Testcase:Result:
$key
{$val['text']}
".htmlentities($url)."

Test case with image higher.jpg

$val) { $url = "../img.php?src=higher.jpg{$val['query']}"; echo ""; } ?>
Test case with image higher.jpg
Testcase:Result:
$key
{$val['text']}
".htmlentities($url)."
================================================ FILE: webroot/test/test_issue101-dummy.php ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Apply testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: webroot/test/test_issue36_rb-ra-270.php ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Apply testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: webroot/test/test_issue36_rb-ra-45.php ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Apply testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: webroot/test/test_issue36_rb-ra-90.php ================================================ "; // Provide a short description of the testcase. $description = ""; // Use these images in the test $images = array( 'kodim08.png', 'kodim04.png', ); // For each image, apply these testcases $testcase = array( "&rb=$angle&nc", "&rb=$angle&nc&w=200", "&rb=$angle&nc&h=200", "&rb=$angle&nc&w=200&h=200&cf", "&ra=$angle&nc", "&ra=$angle&nc&w=200", "&ra=$angle&nc&h=200", "&ra=$angle&nc&w=200&h=200&cf", ); // Applu testcases and present results include __DIR__ . "/template.php"; ================================================ FILE: webroot/test/test_issue38.php ================================================ Links to use for testing

Links useful for testing

A collection of linkt to use to test various aspects of the cimage process.