namespace Ice\Image; use Ice\Image; use Ice\Exception; /** * Gd image driver. * * @package Ice/Image * @category Driver * @author Ice Team * @copyright (c) 2014-2023 Ice Team * @license http://iceframework.org/license */ class Gd extends Image { protected static bundled; protected image; protected createFunction; /** * Run install check and load the image. * * @param string file Image file path * * @return void * @throws Exception */ public function __construct(string file) { var create; if !self::checked { // Run the install check self::check(); } parent::__construct(file); // Set the image creation function name switch this->type { case IMAGETYPE_JPEG: let create = "imagecreatefromjpeg"; break; case IMAGETYPE_GIF: let create = "imagecreatefromgif"; break; case IMAGETYPE_PNG: let create = "imagecreatefrompng"; break; } if !create || !function_exists(create) { throw new Exception(["Installed GD does not support %s images", image_type_to_extension(this->type, false)]); } // Save function for future use let this->createFunction = create; // Save filename for lazy loading let this->image = this->file; } /** * Checks if GD is enabled and bundled. Bundled GD is required for some methods to work. * Exceptions will be thrown from those methods when GD is not bundled. * * @return boolean */ public static function check() -> boolean { var version; if !function_exists("gd_info") { throw new Exception("GD is either not installed or not enabled, check your configuration"); } if defined("GD_BUNDLED") { // Get the version via a constant let self::bundled = GD_BUNDLED; } if defined("GD_VERSION") { // Get the version via a constant let version = GD_VERSION; } if !version_compare(version, "2.0.1", ">=") { throw new Exception(["GD requires GD version %s or greater, you have %s", "2.0.1", version]); } let self::checked = true; return self::checked; } /** * Destroys the loaded image to free up resources. * * @return void */ public function __destruct() { if is_resource(this->image) { // Free all resources imagedestroy(this->image); } } /** * Loads an image into GD. * * @return void */ protected function loadImage() -> void { var create; if !is_resource(this->image) { // Gets create function let create = this->createFunction; // Open the temporary image let this->image = {create}(this->file); // Preserve transparency when saving imagesavealpha(this->image, true); } } /** * Execute a resize. * * @param integer width New width * @param integer height New height * * @return void */ protected function doResize(int width, int height) -> void { var preWidth, preHeight, reductionWidth, reductionHeight, image; // Presize width and height let preWidth = this->width, preHeight = this->height; // Loads image if not yet loaded this->loadImage(); // Test if we can do a resize without resampling to speed up the final resize if width > (this->width / 2) && height > (this->height / 2) { // The maximum reduction is 10% greater than the final size let reductionWidth = round(width * 1.1), reductionHeight = round(height * 1.1); while preWidth / 2 > reductionWidth && preHeight / 2 > reductionHeight { // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction let preWidth /= 2, preHeight /= 2; } // Create the temporary image to copy to let image = this->create(preWidth, preHeight); if imagecopyresized(image, this->image, 0, 0, 0, 0, preWidth, preHeight, this->width, this->height) { // Swap the new image for the old one imagedestroy(this->image); let this->image = image; } } // Create the temporary image to copy to let image = this->create(width, height); // Execute the resize if imagecopyresampled(image, this->image, 0, 0, 0, 0, width, height, preWidth, preHeight) { // Swap the new image for the old one imagedestroy(this->image); let this->image = image; // Reset the width and height let this->width = imagesx(image), this->height = imagesy(image); } } /** * Execute a crop. * * @param integer width New width * @param integer height New height * @param integer offsetX Offset from the left * @param integer offsetY Offset from the top * * @return void */ protected function doCrop(int width, int height, int offsetX, int offsetY) -> void { var image; // Create the temporary image to copy to let image = this->create(width, height); // Loads image if not yet loaded this->loadImage(); // Execute the crop if imagecopyresampled(image, this->image, 0, 0, offsetX, offsetY, width, height, width, height) { // Swap the new image for the old one imagedestroy(this->image); let this->image = image; // Reset the width and height let this->width = imagesx(image), this->height = imagesy(image); } } /** * Execute a rotation. * * @param integer degrees Degrees to rotate * * @return void */ protected function doRotate(int degrees) -> void { var transparent, image, width, height; // Loads image if not yet loaded this->loadImage(); // Transparent black will be used as the background for the uncovered region let transparent = imagecolorallocatealpha(this->image, 0, 0, 0, 127); // Rotate, setting the transparent color let image = imagerotate(this->image, 360 - degrees, transparent, 1); // Save the alpha of the rotated image imagesavealpha(image, true); // Get the width and height of the rotated image let width = imagesx(image), height = imagesy(image); if imagecopymerge(this->image, image, 0, 0, 0, 0, width, height, 100) { // Swap the new image for the old one imagedestroy(this->image); let this->image = image; // Reset the width and height let this->width = width, this->height = height; } } /** * Execute a flip. * * @param integer direction Direction to flip * * @return void */ protected function doFlip(int direction) -> void { var flipped; // Create the flipped image let flipped = this->create(this->width, this->height); // Loads image if not yet loaded this->loadImage(); if direction === Image::HORIZONTAL { int x = 0; while x < this->width { // Flip each row from top to bottom imagecopy(flipped, this->image, x, 0, this->width - x - 1, 0, 1, this->height); let x++; } } else { int y = 0; while y < this->height { // Flip each column from left to right imagecopy(flipped, this->image, 0, y, 0, this->height - y - 1, this->width, 1); let y++; } } // Swap the new image for the old one imagedestroy(this->image); let this->image = flipped; // Reset the width and height let this->width = imagesx(flipped), this->height = imagesy(flipped); } /** * Execute a sharpen. * * @param integer amount Amount to sharpen * * @return void */ protected function doSharpen(int amount) -> void { var tmp, matrix; // Loads image if not yet loaded this->loadImage(); // Amount should be in the range of 18-10 let tmp = round(abs(-18 + (amount * 0.08)), 2), amount = (int) tmp; // Gaussian blur matrix let matrix = [ [-1, -1, -1], [-1, amount, -1], [-1, -1, -1] ]; // Perform the sharpen if imageconvolution(this->image, matrix, amount - 8, 0) { // Reset the width and height let this->width = imagesx(this->image), this->height = imagesy(this->image); } } /** * Execute a reflection. * * @param integer height Reflection height * @param integer opacity Reflection opacity * @param boolean fadeIn True to fade out, false to fade in * * @return void */ protected function doReflection(int height, int opacity, boolean fadeIn) -> void { var tmp, srcY, dstY, dstOpacity, stepping, reflection, line; // Loads image if not yet loaded this->loadImage(); // Convert an opacity range of 0-100 to 127-0 let tmp = round(abs((opacity * 127 / 100) - 127)), opacity = (int) tmp; if opacity < 127 { // Calculate the opacity stepping let stepping = (127 - opacity) / height; } else { // Avoid a "divide by zero" error let stepping = 127 / height; } // Create the reflection image let reflection = this->create(this->width, this->height + height); // Copy the image to the reflection imagecopy(reflection, this->image, 0, 0, 0, 0, this->width, this->height); int offset = 0; while height >= offset { // Read the next line down let srcY = this->height - offset - 1; // Place the line at the bottom of the reflection let dstY = this->height + offset; if fadeIn === true { // Start with the most transparent line first let dstOpacity = round(opacity + (stepping * (height - offset))); } else { // Start with the most opaque line first let dstOpacity = round(opacity + (stepping * offset)); } // Create a single line of the image let line = this->create(this->width, 1); // Copy a single line from the current image into the line imagecopy(line, this->image, 0, 0, 0, srcY, this->width, 1); // Colorize the line to add the correct alpha level imagefilter(line, IMG_FILTER_COLORIZE, 0, 0, 0, dstOpacity); // Copy a the line into the reflection imagecopy(reflection, line, 0, dstY, 0, 0, this->width, 1); let offset++; } // Swap the new image for the old one imagedestroy(this->image); let this->image = reflection; // Reset the width and height let this->width = imagesx(reflection), this->height = imagesy(reflection); } /** * Execute a watermarking. * * @param Image image Watermarking Image * @param integer offsetX Offset from the left * @param integer offsetY Offset from the top * @param integer opacity Opacity of watermark * * @return void */ protected function doWatermark(watermark, int offsetX, int offsetY, int opacity) -> void { var tmp, overlay, width, height, color; // Loads image if not yet loaded this->loadImage(); // Create the watermark image resource let overlay = imagecreatefromstring(watermark->render()); imagesavealpha(overlay, true); // Get the width and height of the watermark let width = imagesx(overlay), height = imagesy(overlay); if opacity < 100 { // Convert an opacity range of 0-100 to 127-0 let tmp = round(abs((opacity * 127 / 100) - 127)), opacity = (int) tmp; // Allocate transparent gray let color = imagecolorallocatealpha(overlay, 127, 127, 127, opacity); // The transparent image will overlay the watermark imagelayereffect(overlay, IMG_EFFECT_OVERLAY); // Fill the background with the transparent color imagefilledrectangle(overlay, 0, 0, width, height, color); } // Alpha blending must be enabled on the background! imagealphablending(this->image, true); if imagecopy(this->image, overlay, offsetX, offsetY, 0, 0, width, height) { // Destroy the overlay image imagedestroy(overlay); } } /** * Execute a background. * * @param integer r Red * @param integer g Green * @param integer b Blue * @param integer opacity Opacity * * @return void */ protected function doBackground(int r, int g, int b, int opacity) -> void { var tmp, background, color; // Loads image if not yet loaded this->loadImage(); // Convert an opacity range of 0-100 to 127-0 let tmp = round(abs((opacity * 127 / 100) - 127)), opacity = (int) tmp; // Create a new background let background = this->create(this->width, this->height); // Allocate the color let color = imagecolorallocatealpha(background, r, g, b, opacity); // Fill the image with white imagefilledrectangle(background, 0, 0, this->width, this->height, color); // Alpha blending must be enabled on the background! imagealphablending(background, true); // Copy the image onto a white background to remove all transparency if imagecopy(background, this->image, 0, 0, 0, 0, this->width, this->height) { // Swap the new image for the old one imagedestroy(this->image); let this->image = background; } } /** * Execute a save. * * @param string file New image filename * @param integer quality Quality * * @return boolean */ protected function doSave(string file, var quality = null) -> boolean { var tmp, extension, save, type, status; // Loads image if not yet loaded this->loadImage(); // Get the extension of the file let extension = pathinfo(file, PATHINFO_EXTENSION); // Get the save function and IMAGETYPE let tmp = this->saveFunction(extension, quality), save = tmp[0], type = tmp[1], quality = tmp[2]; // Save the image to a file let status = quality ? {save}(this->image, file, quality) : {save}(this->image, file); if status === true && type !== this->type { // Reset the image type and mime type let this->type = type, this->mime = image_type_to_mime_type(type); } return true; } /** * Execute a render. * * @param string type Image type: png, jpg, gif, etc * @param integer quality Quality * * @return string */ protected function doRender(string type, quality) -> string { var tmp, save, status; // Loads image if not yet loaded this->loadImage(); // Get the save function and IMAGETYPE let tmp = this->saveFunction(type, quality), save = tmp[0], type = (string) tmp[1], quality = tmp[2]; // Capture the output ob_start(); // Render the image let status = quality ? {save}(this->image, null, quality) : {save}(this->image, null); if status === true && type !== this->type { // Reset the image type and mime type let this->type = type, this->mime = image_type_to_mime_type(type); } return ob_get_clean(); } /** * Get the GD saving function and image type for this extension. * Also normalizes the quality setting * * @param string extension Image type: png, jpg, etc * @param integer quality Image quality * @return array Save function, IMAGETYPE_* constant * @throws Exception */ protected function saveFunction(string extension, int quality) -> array { var tmp, save, type; if !extension { // Use the current image type let tmp = image_type_to_extension(this->type, false), extension = (string) tmp; } switch strtolower(extension) { case "jpg": case "jpeg": // Save a JPG file let save = "imagejpeg", type = IMAGETYPE_JPEG; break; case "gif": // Save a GIF file let save = "imagegif", type = IMAGETYPE_GIF; // GIFs do not a quality setting let quality = null; break; case "png": // Save a PNG file let save = "imagepng", type = IMAGETYPE_PNG; // Use a compression level of 9 (does not affect quality!) let quality = 9; break; default: throw new Exception(["Installed GD does not support %s images", extension]); } return [save, type, quality]; } /** * Create an empty image with the given width and height. * * @param integer width Image width * @param integer height Image height * * @return resource */ protected function create(int width, int height) -> resource { var image; // Create an empty image let image = imagecreatetruecolor(width, height); // Do not apply alpha blending imagealphablending(image, false); // Save alpha levels imagesavealpha(image, true); return image; } }