Typesetter/include/tool/Image.php

578 lines
15 KiB
PHP
Raw Permalink Normal View History

2021-09-08 19:52:21 +02:00
<?php
namespace gp\tool{
defined('is_running') or die('Not an entry point...');
includeFile('thirdparty/jpeg-icc/autoload.php');
includeFile('tool/ImageMeta.php');
class Image{
/**
* Reduce the size of an image according to $max_area
*
* @since 1.7
*
* @param string $img_path location of the image
* @param int $max_area Maximum area of the image: 600x800 pixels = 480,000 sq/pixels
* @return bool true if the image doesn't need to be reduced or if a reduction is made
*/
static function CheckArea($img_path,$max_area=1024000){
global $config, $dataDir;
$src_img = self::getSrcImg($img_path); //memory usage before and after this call are small 1093804 vs 1094616
if( $src_img === false ){
return false;
}
//Original Size
$old_x = imagesx($src_img);
$old_y = imagesy($src_img);
$old_area = ($old_x * $old_y);
//don't enlarge check 1
if( $old_area < $max_area ){
return true;
}
//Calculate the new size
$inv_ratio = $old_y / $old_x;
$new_y = sqrt($max_area * $inv_ratio);
$new_y = round($new_y);
$new_x = round($max_area / $new_y);
//don't enlarge check 2
$new_area = ($new_y * $new_x);
if( $new_area > $old_area ){
return true;
}
$src_path = $img_path;
$result = self::createImg($src_img, $img_path, 0, 0, 0, 0, $new_x, $new_y, $old_x, $old_y, false, false, $src_path);
@chmod($img_path, gp_chmod_file);
return $result;
}
/**
* Create a square image at $dest_path from an image at $source_path
*
* @param string $source_path location of the source image
* @param string $dest_path location of the image to be created
* @param int $size the dimension of the image
* @param string $type_file a string representing the type of the source file (png, jpg)
* @return bool
*/
static function createSquare($source_path,$dest_path,$size=50,$type_file=false){
global $config;
$img_type = self::getType($source_path);
if( strpos('svgz', $img_type) === 0 ){
// image is SVG
2021-10-19 23:06:37 +02:00
return self::CreateRectSVG($source_path,$dest_path,false,$size,$size);
2021-09-08 19:52:21 +02:00
}
$new_w = $new_h = $size;
$src_img = self::getSrcImg($source_path,$type_file);
if( $src_img === false ){
return false;
}
//Size
$old_x = imagesx($src_img);
$old_y = imagesy($src_img);
if( $old_x > $old_y ){
$off_w = ($old_x - $old_y) / 2;
$off_h = 0;
$old_x = $old_y;
}elseif( $old_y > $old_x ){
$off_w = 0;
$off_h = ($old_y - $old_x) / 2;
$old_y = $old_x;
}else{
$off_w = 0;
$off_h = 0;
}
//don't make the thumbnail larger
if( ($old_x < $size) && ($old_y < $size ) ){
$new_w = $new_h = max($old_x,$old_y);
}
$result = self::createImg($src_img, $dest_path, 0, 0, $off_w, $off_h, $new_w, $new_h, $old_x, $old_y, false, false, $source_path);
@chmod($dest_path, gp_chmod_file);
return $result;
}
/**
* Create a rectangular image at $dest_path from an image at $source_path
*
* @param string $source_path location of the source image
* @param string $dest_path location of the image to be created
* @param int $new_w The width of the new image
* @param int $new_h The height of the new image
* @param boolean $keep_aspect_ratio Scale to fit / Crop to cover new size
* @return bool
*/
static function CreateRect($source_path,$dest_path,$new_w=50,$new_h=50,$keep_aspect_ratio=false){
global $config;
$img_type = self::getType($source_path);
if( strpos('svgz', $img_type) === 0 ){
// image is SVG
2021-10-19 23:06:37 +02:00
return self::CreateRectSVG($source_path,$dest_path,$keep_aspect_ratio,$new_w,$new_h);
2021-09-08 19:52:21 +02:00
}
$src_img = self::getSrcImg($source_path,$img_type);
if( $src_img === false ){
return false;
}
// Size
$old_w = imagesx($src_img);
$old_h = imagesy($src_img);
// Ratios
$old_aspect_ratio = $old_w / $old_h;
$new_aspect_ratio = $new_w / $new_h;
$off_w = 0;
$off_h = 0;
if( $keep_aspect_ratio ){
// scale to fit into new width/height
if( $old_aspect_ratio > $new_aspect_ratio ){
// old img is wider than new one
2021-10-17 11:28:43 +02:00
$new_h = (int)(round($new_h / $old_aspect_ratio * $new_aspect_ratio));
2021-09-08 19:52:21 +02:00
}else{
// old img is narrower than new one
2021-10-17 11:28:43 +02:00
$new_w = (int)(round($new_w / $new_aspect_ratio * $old_aspect_ratio));
2021-09-08 19:52:21 +02:00
}
}else{
// crop to cover new width/height
if( $old_aspect_ratio > $new_aspect_ratio ){
// old img is wider than new one
$old_w_tmp = round($old_h * $new_aspect_ratio);
$off_w = round(($old_w -$old_w_tmp) / 2);
$old_w = $old_w_tmp;
}else{
// old img is narrower than new one
$old_h_tmp = round($old_w / $new_aspect_ratio);
$off_h = round(($old_h - $old_h_tmp) / 2);
$old_h = $old_h_tmp;
}
}
/* DEBUG */
/*
msg(
basename($source_path) . ": <br/>"
. "old_w=" . $old_w . " | old_h=" . $old_h . "<br/>"
. "off_w=" . $off_w . " | off_h=" . $off_h . "<br/>"
. "old_aspect_ratio=" . $old_aspect_ratio . "<br/>"
. "new_aspect_ratio=" . $new_aspect_ratio . "<br/>"
. "new_w=" . $new_w . " | new_h=" . $new_h . "<br/>"
// . "<br/><br/>"
);
*/
$result = self::createImg($src_img, $dest_path, 0, 0, $off_w, $off_h, $new_w, $new_h, $old_w, $old_h, false, false, $source_path);
@chmod($dest_path, gp_chmod_file);
return $result;
}
/**
* SVG -- Create a rectangular SVG image at $dest_path from an SVG image at $source_path
*
* @param string $source_path location of the source SVG image
* @param string $dest_path location of the SVG image to be created
* @param int $size the dimension of the SVG image
* @param string $type_file a string representing the type of the source file (svg, svgz)
* @return bool
*/
2021-10-19 23:06:37 +02:00
static function CreateRectSVG($source_path, $dest_path, $keep_aspect_ratio, $width=50, $height=50, ){
2021-09-08 19:52:21 +02:00
$src_svg = @file_get_contents($source_path);
if( !$src_svg ){
// msg($src_svg . "does not exist!");
return false;
}
/*
if( substr($src_svg,0,2) === hex2bin('1F8B') ){
// gzip encoded svgz, decode it
$src_svg = gzdecode($src_svg);
}
*/
$internalErrors = libxml_use_internal_errors(true);
/* $disableEntities = libxml_disable_entity_loader(true); php8 deprecated */
2021-09-08 19:52:21 +02:00
libxml_clear_errors();
/* $doc->loadXML(file_get_contents($filename),$options); */
2021-09-08 19:52:21 +02:00
$doc = new \DOMDocument();
$doc->loadXML($src_svg, LIBXML_NONET);
libxml_use_internal_errors($internalErrors);
2021-09-08 19:52:21 +02:00
if( $error = libxml_get_last_error() ){
libxml_clear_errors();
// msg("SVG processing - LibXML Error: " . $error->message );
return false;
}
if( strtolower($doc->documentElement->tagName) !== 'svg'){
// msg("SVG processing - Error: data stream is not an svg image");
return false;
}
$svg = $doc->documentElement;
// get size
$svg_width = self::getPxVal($svg->getAttribute('width'));
$svg_height = self::getPxVal($svg->getAttribute('height'));
if( $svg->hasAttribute('viewBox') ){
$viewBox = preg_split('/[\s,]+/', $svg->getAttribute('viewBox'));
$vb_x = $viewBox[0];
$vb_y = $viewBox[1];
$vb_w = $viewBox[2];
$vb_h = $viewBox[3];
}
if( (!$svg_width && !$svg_height) && (!$vb_w && !$vb_h) ){
// msg("SVG processing - Error: width and height could not be determined");
// return false;
// no width/height provided -> we assume a 800 x 800px as default size
$svg_width = 800;
$svg_height = 800;
}
// msg("SVG file:" . basename($source_path) . " -- svg_width=".$svg_width." | svg_height=".$svg_height);
if( !$svg->hasAttribute('viewBox') ){
$svg->setAttribute('viewBox', '0 0 ' . $svg_width . ' ' . $svg_height);
$vb_x = 0;
$vb_y = 0;
$vb_w = $svg_width;
$vb_h = $svg_height;
}
$w = $vb_w - $vb_x;
$h = $vb_h - $vb_y;
// left/top offsets ### needs to be changed ###
if( $w > $h ){
$vb_x += ($w - $h) / 2;
$vb_w -= ($w - $h);
}else{
$vb_y += ($h - $w) / 2;
$vb_h -= ($h - $w);
}
$svg->setAttribute('viewBox', $vb_x . ' ' . $vb_y . ' ' . $vb_w . ' ' . $vb_h);
$svg->setAttribute('width', $width);
$svg->setAttribute('height', $height);
return self::saveSVG($doc, $dest_path);
}
/**
* Save SVG document to file
*/
static function saveSVG($doc, $path){
global $langmessage;
$svg_xml = $doc->saveXML();
$result = file_put_contents($path, $svg_xml);
if( $result ){
@chmod($path, gp_chmod_file);
return true;
}
// msg($langmessage['OOPS'] . ': Unable to save SVG to ' . $path);
return false;
}
/**
* Return pixel value from value+units
*/
static function getPxVal($size) {
$map = array('px'=>1, 'pt'=>1.3333333, 'mm'=>3.7795276, 'ex'=>8, 'em'=>16, 'pc'=>16, 'cm'=>37.795276, 'in'=>96);
$size = trim($size);
$value = substr($size, 0, -2);
$unit = substr($size, -2);
if( isset($map[$unit]) && is_numeric($value) ){
$size = $value * $map[$unit];
}
if( is_numeric($size) ){
return (int) round($size);
}
return 0;
}
/**
* Return a type file type of a given file given by it's $path on the file system
* @static
*/
static function getType($path){
$nameParts = explode('.',$path);
$type = array_pop($nameParts);
return strtolower($type);
}
/**
* Attempt to increase php's memory limit using the current memory used and the post_max_size value
* Generally speaking, memory_limit should be larger than post_max_size http://php.net/manual/en/ini.core.php
* @static
*/
static function AdjustMemoryLimit(){
//get memory limit in bytes
$limit = @ini_get('memory_limit') or '8M';
$limit = \gp\tool::getByteValue($limit);
//get memory usage or use a default value
if( function_exists('memory_get_usage') ){
$memoryUsed = memory_get_usage();
}else{
$memoryUsed = 3*1048576; //sizable buffer 3MB
}
//since imageHeight and imageWidth aren't always available
//use post_max_size to figure maximum memory limit
$max_post = @ini_get('post_max_size') or '8M'; //defaults to 8M
$max_post = \gp\tool::getByteValue($max_post);
$needed = $max_post + $memoryUsed;
if( $limit < $needed ){
@ini_set( 'memory_limit', $needed);
}
}
/**
* @deprecated 4.3
*/
static function getByteValue($value){}
/**
* Using the given $source_path of an image, return the GD image if possible
* @param string $source_path The path of the source image
* @param string $type_file The file type of the source image
* @return mixed GD image if successful, false otherwise
*/
static function getSrcImg($source_path,$type_file=false){
if( !function_exists('imagetypes') ){
return false;
}
self::AdjustMemoryLimit();
if( $type_file !== false ){
$img_type = self::getType($type_file);
}else{
$img_type = self::getType($source_path);
}
$supported_types = imagetypes();
//start
switch($img_type){
case 'jpg':
case 'jpeg':
if( $supported_types & IMG_JPG ){
return imagecreatefromjpeg($source_path);
}
break;
case 'gif':
return imagecreatefromgif($source_path);
break;
case 'png':
if( $supported_types & IMG_PNG) {
return imagecreatefrompng($source_path);
}
break;
case 'bmp';
if( $supported_types & IMG_WBMP) {
return imagecreatefromwbmp($source_path);
}
break;
}
//msg('not supported for thumbnail: '.$img_type);
return false;
}
/**
* Save the GD image ($src_img) to the desired location ($dest_path) with the sizing arguments
*
* $src_path is required for ICC/meta data
* The method will still work w/o it but will not retain ICC/meta data (for possible plugin calls)
*
*/
static function createImg($src_img, $dest_path, $dst_x, $dst_y, $off_w, $off_h, $dst_w, $dst_h, $old_x, $old_y, $new_w = false, $new_h = false, $src_path = false){
global $config;
$preserve_icc_profiles = false;
$preserve_image_metadata = false;
if( $src_path ){
$img_type = self::getType($src_path);
$dest_type = self::getType($dest_path);
if( ($img_type == 'jpg' || $img_type == 'jpeg') && ($dest_type == 'jpg' || $dest_type == 'jpeg') ){
$preserve_icc_profiles = !empty($config['preserve_icc_profiles']);
$preserve_image_metadata = !empty($config['preserve_image_metadata']);
if( $preserve_icc_profiles ){
$jpeg_icc = new \JPEG_ICC();
$has_icc = $jpeg_icc->LoadFromJPEG($src_path);
if( !$has_icc ){
$preserve_icc_profiles = false;
unset($jpeg_icc);
}
}
if( $preserve_image_metadata ){
$image_meta = \gp\tool\ImageMeta::getMeta($src_path);
/* FOR DEBUGGING - write a text file with acquired metadata and/or possible errors to the source directory */
/*
$pi = pathinfo($src_path);
$debug_metadata_file = $pi['dirname'] . '/' . $pi['filename'] . '_meta.txt';
$debug_metadata = 'Metadata for image file "' . basename($src_path) . '"' . "\n\n";
foreach( $image_meta as $mk => $mv ){
if( is_array($mv) ){
$debug_metadata .= $mk . ' : ' . "\n";
foreach( $md as $errk => $errd ){
$debug_metadata .= ' ' . $errk . ' : ' . $errv . "\n";
}
}else{
$debug_metadata .= $mk . ' : ' . $mv . "\n";
}
}
\gp\tool\Files::Save($debug_metadata_file, $debug_metadata);
*/
}
}
}
if( !$new_w ){
$new_w = $dst_w;
}
if( !$new_h ){
$new_h = $dst_h;
}
$dst_img = imagecreatetruecolor($new_w, $new_h);
if( !$dst_img ){
trigger_error('dst_img not created');
return false;
}
$img_type = self::getType($dest_path);
// allow gif & png to have transparent background
switch($img_type){
case 'gif':
case 'png':
$dst_img = self::Transparency($dst_img);
break;
}
if( !imagecopyresampled($dst_img, $src_img, $dst_x, $dst_y, $off_w, $off_h, $dst_w, $dst_h, $old_x, $old_y) ){
trigger_error('copyresample failed');
imagedestroy($dst_img);
imagedestroy($src_img);
if( $preserve_icc_profiles ){
unset($jpeg_icc);
}
return false;
}
imagedestroy($src_img);
$saved = self::SrcToImage($dst_img,$dest_path,$img_type);
if( $preserve_image_metadata ){
$iptc_embedded = \gp\tool\ImageMeta::saveMeta($dest_path, $image_meta);
}
if( $preserve_icc_profiles ){
$jpeg_icc->SaveToJPEG($dest_path);
unset($jpeg_icc);
}
return $saved;
}
static function Transparency($image){
if( function_exists('imagesavealpha') ){
imagesavealpha($image,true);
$bgcolor = imagecolorallocatealpha($image, 133, 134, 135, 127);
imagefill($image, 0, 0, $bgcolor);
}
return $image;
}
/**
* Output image to path based on type
* will already have checked for support via the getSrcImg function
*
*/
static function SrcToImage($src,$path,$type){
$result = false;
switch($type){
case 'jpeg':
case 'jpg':
$result = imagejpeg($src,$path,90);
break;
case 'gif':
$result = imagegif($src,$path);
break;
case 'png':
$result = imagepng($src,$path,9,PNG_ALL_FILTERS);
break;
case 'bmp':
$result = imagewbmp($src,$path);
break;
}
if( $result === false ){
@imagedestroy($src);
return false;
}
@chmod($path, gp_chmod_file);
imagedestroy($src);
return true;
}
}
}
namespace{
class thumbnail extends \gp\tool\Image{}
}