From f2b12698484a4dd39d6a9d55a0ba60ae7c7f1e82 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 10 Jan 2017 13:10:54 -0500 Subject: [PATCH] SimpleImage 3.0 --- readme.md | 606 +++++++---- src/abeautifulsite/SimpleImage.php | 1484 -------------------------- src/claviska/SimpleImage.php | 1563 ++++++++++++++++++++++++++++ 3 files changed, 1959 insertions(+), 1694 deletions(-) delete mode 100644 src/abeautifulsite/SimpleImage.php create mode 100644 src/claviska/SimpleImage.php diff --git a/readme.md b/readme.md index bd2c544..8a4cba6 100644 --- a/readme.md +++ b/readme.md @@ -1,267 +1,453 @@ -The SimpleImage PHP class -========================= +# SimpleImage -*By Cory LaViska for A Beautiful Site, LLC -(http://www.abeautifulsite.net/)* +A PHP class to that makes working with images as simple as possible. -*Licensed under the MIT license: http://opensource.org/licenses/MIT* +Developed and maintained by [Cory LaViska](https://github.com/claviska). -Overview --------- +_If this project has you loving PHP image manipulation again, please consider making [a small donation](https://paypal.me/claviska) to support its development._ -This class makes image manipulation in PHP as simple as possible. The -examples are the best way to learn how to use it, but here it is in a -nutshell: +--- + +## Overview ```php flip('x')->rotate(90)->best_fit(320, 200)->sepia()->save('example/result.gif'); -} catch(Exception $e) { - echo 'Error: ' . $e->getMessage(); + // Create a new SimpleImage object + $image = new \claviska\SimpleImage(); + + // Magic! ✨ + $image + ->fromFile('image.jpg') // load image.png + ->autoOrient() // adjust orientation based on exif data + ->resize(320, 200) // resize to 320x200 pixels + ->flip('x') // flip horizontally + ->colorize('DarkBlue') // tint light blue + ->border('black', 10) // add a 10 pixel black border + ->overlay('watermark.png', 'bottom right') // add a watermark image + ->toFile('new-image.png', 'image/png') // convert to PNG and save a copy to new-image.png + ->toScreen(); // output to the screen + + // And much more! 💪 +} catch(Exception $err) { + // Handle errors + echo $err->getMessage(); } - -?> ``` -The two lines inside the _try_ block load **image.jpg**, flip it horizontally, rotate -it 90 degrees clockwise, shrink it to fit within a 320x200 box, apply a sepia -effect, convert it to a GIF, and save it to **result.gif**. - -With this class, you can effortlessly: -- Resize images (free resize, resize to width, resize to height, resize to fit) -- Crop images -- Flip/rotate/adjust orientation -- Adjust brightness & contrast -- Desaturate, colorize, pixelate, blur, etc. -- Overlay one image onto another (watermarking) -- Add text using a font of your choice -- Convert between GIF, JPEG, and PNG formats -- Strip EXIF data - -Requirements ------------- - -This class requires PHP 5.3 and PHP GD library. +## Requirements -Usage ------ +- PHP 5.6+ +- [GD extension](http://php.net/manual/en/book.image.php) -### Loading +## Features -You can load an image when you instantiate a new SimpleImage object: +- Supports reading, writing, and converting GIF, JPEG, PNG, WEBP formats. +- Reads and writes files and data URIs. +- Manipulation: crop, resize, overlay/watermark, adding TTF text +- Drawing: arc, border, dot, ellipse, line, polygon, rectangle, rounded rectangle +- Filters: blur, brighten, colorize, contrast, darken, desaturate, edge detect, emboss, invert, pixelate, sepia, sketch +- Utility methods: color adjustment, darken/lighten color, exif data, height/width, mime type, orientation +- Color arguments can be passed in as any CSS color (e.g. `LightBlue`), a hex color, or an RGB(A) array. +- Support for alpha-transparency (GIF, PNG, WEBP) +- Chainable methods +- Uses exceptions +- Load with Composer or manually (just one file) -```php -$img = new abeautifulsite\SimpleImage('image.jpg'); -``` +## Installation -Or you can create empty image 200x100 with black background: +Install with Composer: -```php -$img = new abeautifulsite\SimpleImage(null, 200, 100, '#000'); ``` - -### Saving - -Images must be saved after you manipulate them. To save your changes to -the original file, simply call: - -```php -$img->save(); +composer require claviska/simpleimage ``` -Alternatively, you can specify a new filename: +Or include the library manually: ```php -$img->save('new-image.jpg'); -``` - -You can specify quality as a second parameter in percents within range 0-100 - -```php -$img->save('new-image.jpg', 90); -``` - -### Converting Between Formats - -When saving, the resulting image format is determined by the file -extension. For example, you can convert a JPEG to a GIF by doing this: - -```php -$img = new abeautifulsite\SimpleImage('image.jpg'); -$img->save('image.gif'); -``` - -### Stripping EXIF data - -There is no built-in method for stripping EXIF data, partly because -there is currently no way to *prevent* EXIF data from being stripped -using the GD library. However, you can easily strip EXIF data simply by -loading and saving: - -```php -$img = new abeautifulsite\SimpleImage('image.jpg'); -$img->save(); -``` - -### Method Chaining - -SimpleImage supports method chaining, so you can make multiple changes -and save the resulting image with just one line of code: - -```php -$img = new abeautifulsite\SimpleImage('image.jpg'); -$img->flip('x')->rotate(90)->best_fit(320, 200)->desaturate()->invert()->save('result.jpg') -``` - -You can chain all of the methods below as well methods above. - -### Error Handling - -SimpleImage throws exceptions when things don't work right. You should -always load/manipulate/save images inside of a *try/catch* block to -handle them properly: - -```php -try { - $img = new abeautifulsite\SimpleImage('image.jpg'); - $img->flip('x')->save('flipped.jpg'); -} catch(Exception $e) { - echo 'Error: ' . $e->getMessage(); -} +flip('x'); +SimpleImage is developed and maintained by [Cory LaViska](https://github.com/claviska). Copyright A Beautiful Site, LLC. -// Rotate the image 90 degrees clockwise -$img->rotate(90); +Contributions are appreciated! If you enjoy using SimpleImage, especially in commercial applications, please consider [making a contribution](https://paypal.me/claviska) to support its development. -// Adjust the orientation if needed (physically rotates/flips the image based on its EXIF 'Orientation' property) -$img->auto_orient(); +Thanks! 🙌 -// Resize the image to 320x200 -$img->resize(320, 200); +## License -// Trim the image and resize to exactly 100x75 -$img->thumbnail(100, 75); +Licensed under the [MIT license](http://opensource.org/licenses/MIT). -// Trim the image and resize to exactly 100x75, keeping the top* of the image -$img->thumbnail(100, 75, 'top'); +--- -// Shrink the image to the specified width while maintaining proportion (width) -$img->fit_to_width(320); +## API -// Shrink the image to the specified height while maintaining proportion (height) -$img->fit_to_height(200); +Order of awesomeness: -// Shrink the image proportionally to fit inside a 500x500 box -$img->best_fit(500, 500); +1. Load an image +2. Manipulate the image +3. Save/output the image -// Crop a portion of the image from x1, y1 to x2, y2 -$img->crop(100, 100, 400, 400); +API tips: -// Fill image with white color -$img->fill('#fff'); +- An asterisk denotes a required argument. +- Methods that return a SimpleImage object are chainable. +- You can pass a file or data URI to the constructor to avoid calling `fromFile` or `fromDataUri`. -// Desaturate (grayscale) -$img->desaturate(); +### Loaders -// Invert -$img->invert(); +`fromDataUri($uri)` - Loads an image from a data URI. +- `$uri`* (string) - A data URI. +Returns a SimpleImage object. -// Adjust Brightness (-255 to 255) -$img->brightness(100); -// Adjust Contrast (-100 to 100) -$img->contrast(50); +`fromFile($file)` - Loads an image from a file. +- `$file`* (string) - The image file to load. +Returns a SimpleImage object. -// Colorize red at 50% opacity -$img->colorize('#FF0000', .5); +`fromNew($width, $height, $color)` - Creates a new image. +- `$width`* (int) - The width of the image. +- `$height`* (int) - The height of the image. +- `$color` (string|array) - Optional fill color for the new image (default 'transparent'). +Returns a SimpleImage object. -// Edges filter -$img->edges(); +## Savers -// Emboss filter -$img->emboss(); +`toDataUri($mimeType, $quality)` - Generates a data URI. +- `$mimeType` (string) - The image format to output as a mime type (defaults to the original mime type). +- `$quality` (int) - Image quality as a percentage (default 100). +Returns a string containing a data URI. -// Mean removal filter -$img->mean_remove(); +`toFile($file, $mimeType, $quality)` - Writes the image to a file. +- `$mimeType` (string) - The image format to output as a mime type (defaults to the original mime type). +- `$quality` (int) - Image quality as a percentage (default 100). +Returns a SimpleImage instance. -// Selective blur (one pass) -$img->blur(); +`toScreen($mimeType, $quality)` - Outputs the image to the screen. +- `$mimeType` (string) - The image format to output as a mime type (defaults to the original mime type). +- `$quality` (int) - Image quality as a percentage (default 100). +Returns a SimpleImage instance. -// Gaussian blur (two passes) -$img->blur('gaussian', 2); +### Utilities -// Sketch filter -$img->sketch(); +`getExif()` - Gets the image's exif data. +Returns an array of exif data or null if no data is available. -// Smooth filter (-10 to 10) -$img->smooth(5); +`getHeight()` - Gets the image's current height. +Returns the height as an integer. -// Pixelate using 8px blocks -$img->pixelate(8); +`getMimeType()` - Gets the mime type of the loaded image. +Returns a mime type string. -// Sepia effect (simulated) -$img->sepia(); +`getOrientation()` - Gets the image's current orientation. +Returns a string: 'landscape', 'portrait', or 'square' -// Change opacity -$img->opacity(.5); +`getWidth()` - Gets the image's current width. +Returns the width as an integer. -// Overlay watermark.png at 50% opacity at the bottom-right of the image with a 10 pixel horizontal and vertical margin -$img->overlay('watermark.png', 'bottom right', .5, -10, -10); +### Manipulation -// Add 32-point white text top-centered (plus 20px) on the image* -$img->text('Your Text', 'font.ttf', 32, '#FFFFFF', 'top', 0, 20); +`autoOrient()` - Rotates an image so the orientation will be correct based on its exif data. It is safe to call this method on images that don't have exif data (no changes will be made). +Returns a SimpleImage object. + +`bestFit($maxWidth, $maxHeight)` - Proportionally resize the image to fit a specified width and height. +- `$maxWidth`* (int) - The maximum width the image can be. +- `$maxHeight`* (int) - The maximum height the image can be. +Returns a SimpleImage object. + +`crop($x1, $y1, $x2, $y2)` - Crop the image. +- $x1 - Top left x coordinate. +- $y1 - Top left y coordinate. +- $x2 - Bottom right x coordinate. +- $y2 - Bottom right x coordinate. +Returns a SimpleImage object. + +`fitToHeight($height)` - Proportionally resize the image to a specific height. +- `$height`* (int) - The height to resize the image to. +Returns a SimpleImage object. + +`fitToWidth($width)` - Proportionally resize the image to a specific width. +- `$width`* (int) - The width to resize the image to. +Returns a SimpleImage object. + +`flip($direction)` - Flip the image horizontally or vertically. +- `$direction`* (string) - The direction to flip: x|y|both +Returns a SimpleImage object. + +`maxColors($max, $dither)` - Reduces the image to a maximum number of colors. +- `$max`* (int) - The maximum number of colors to use. +- `$dither` (bool) - Whether or not to use a dithering effect (default true). +Returns a SimpleImage object. + +`overlay($overlay, $anchor, $opacity, $xOffset, $yOffset)` - Place an image on top of the current image. +- `$overlay`* (string|SimpleImage) - The image to overlay. This can be a filename, a data URI, or a SimpleImage object. +- `$anchor` (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center') +- `$opacity` (float) - The opacity level of the overlay 0-1 (default 1). +- `$xOffset` (int) - Horizontal offset in pixels (default 0). +- `$yOffset` (int) - Vertical offset in pixels (default 0). +Returns a SimpleImage object. + +`resize($width, $height)` - Resize an image to the specified dimensions. This method WILL NOT maintain proportions. To resize an image without stretching it, use fitToWidth, fitToHeight, or bestFit. +- `$width`* (int) - The new image width. +- `$height`* (int) - The new image height. +Returns a SimpleImage object. + +`rotate($angle, $backgroundColor)` - Rotates the image. +- `$angle`* (int) - The angle of rotation (-360 - 360). +- `$backgroundColor` (string|array) - The background color to use for the uncovered zone area after rotation (default 'transparent'). +Returns a SimpleImage object. + +`text($text, $options)` - Adds text to the image. +- `$text`* (string) - The desired text. +- `$options` (array) - An array of options. + - `fontFile`* (string) - The TrueType (or compatible) font file to use. + - `size` (int) - The size of the font in pixels (default 12). + - `color` (string|array) - The text color (default black). + - `anchor` (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', + 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). + - `xOffset` (int) - The horizontal offset in pixels (default 0). + - `yOffset` (int) - The vertical offset in pixels (default 0). + - `shadow` (array) - Text shadow params. + - `x`* (int) - Horizontal offset in pixels. + - `y`* (int) - Vertical offset in pixels. + - `color`* (string|array) - The text shadow color. +Returns a SimpleImage object. + +`thumbnail($width, $height, $anchor)` - Creates a thumbnail image. This function attempts to get the image as close to the provided dimensions as possible, then crops the remaining overflow to force the desired size. Useful for generating thumbnail images. +- `$width`* (int) - The thumbnail width. +- `$height`* (int) - The thumbnail height. +- `$anchor` (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). +Returns a SimpleImage object. + +### Drawing + +`arc($x, $y, $width, $height, $start, $end, $color, $thickness)` - Draws an arc. +- `$x`* (int) - The x coordinate of the arc's center. +- `$y`* (int) - The y coordinate of the arc's center. +- `$width`* (int) - The width of the arc. +- `$height`* (int) - The height of the arc. +- `$start`* (int) - The start of the arc in degrees. +- `$end`* (int) - The end of the arc in degrees. +- `$color`* (string|array) - The arc color. +- `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). +Returns a SimpleImage object. + +`border($color, $thickness)`- Draws a border around the image. +- `$color`* (string|array) - The border color. +- `$thickness` (int) - The thickness of the border (default 1). +- Returns a SimpleImage object. + +`dot($x, $y, $color)` - Draws a single pixel dot. +- `$x`* (int) - The x coordinate of the dot. +- `$y`* (int) - The y coordinate of the dot. +- `$color`* (string|array) - The dot color. +Returns a SimpleImage object. + +`ellipse($x, $y, $width, $height, $color, $thickness)` - Draws an ellipse. +- `$x`* (int) - The x coordinate of the center. +- `$y`* (int) - The y coordinate of the center. +- `$width`* (int) - The ellipse width. +- `$height`* (int) - The ellipse height. +- `$color`* (string|array) - The ellipse color. +- `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). +Returns a SimpleImage object. + +`fill($color)` - Fills the image with a solid color. +- `$color` (string|array) - The fill color. +Returns a SimpleImage object. + +`line($x1, $y1, $x2, $y2, $color, $thickness)` - Draws a line. +- `$x1`* (int) - The x coordinate for the first point. +- `$y1`* (int) - The y coordinate for the first point. +- `$x2`* (int) - The x coordinate for the second point. +- `$y2`* (int) - The y coordinate for the second point. +- `$color` (string|array) - The line color. +- `$thickness` (int) - The line thickness (default 1). +Returns a SimpleImage object. + +`polygon($vertices, $color, $thickness)` - Draws a polygon. +- `$vertices`* (array) - The polygon's vertices in an array of x/y arrays. Example: + ``` + [ + ['x' => x1, 'y' => y1], + ['x' => x2, 'y' => y2], + ['x' => xN, 'y' => yN] + ] + ``` +- `$color`* (string|array) - The polygon color. +- `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). +Returns a SimpleImage object. + +`rectangle($x1, $y1, $x2, $y2, $color, $thickness)` - Draws a rectangle. +- `$x1`* (int) - The upper left x coordinate. +- `$y1`* (int) - The upper left y coordinate. +- `$x2`* (int) - The bottom right x coordinate. +- `$y2`* (int) - The bottom right y coordinate. +- `$color`* (string|array) - The rectangle color. +- `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). +Returns a SimpleImage object. + +`roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $thickness)` - Draws a rounded rectangle. +- `$x1`* (int) - The upper left x coordinate. +- `$y1`* (int) - The upper left y coordinate. +- `$x2`* (int) - The bottom right x coordinate. +- `$y2`* (int) - The bottom right y coordinate. +- `$radius`* (int) - The border radius in pixels. +- `$color`* (string|array) - The rectangle color. +- `$thickness` (int|string) - Line thickness in pixels or 'filled' (default 1). +Returns a SimpleImage object. + +### Filters + +`blur($type, $passes)` - Applies the blur filter. +- `$type` (string) - The blur algorithm to use: 'selective', 'gaussian' (default 'gaussian'). +- `$passes` (int) - The number of time to apply the filter, enhancing the effect (default 1). +Returns a SimpleImage object. + +`brighten($percentage)` - Applies the brightness filter to brighten the image. +- `$percentage`* (int) - Percentage to brighten the image (0 - 100). +Returns a SimpleImage object. + +`colorize($color)` - Applies the colorize filter. +- `$color`* (string|array) - The filter color. +Returns a SimpleImage object. + +`contrast($percentage)` - Applies the contrast filter. +- `$percentage`* (int) - Percentage to adjust (-100 - 100). +Returns a SimpleImage object. + +`darken($percentage)` - Applies the brightness filter to darken the image. +- `$percentage`* (int) - Percentage to darken the image (0 - 100). +Returns a SimpleImage object. + +`desaturate()` - Applies the desaturate (grayscale) filter. +Returns a SimpleImage object. + +`edgeDetect()` - Applies the edge detect filter. +Returns a SimpleImage object. + +`emboss()` - Applies the emboss filter. +Returns a SimpleImage object. + +`invert()` - Inverts the image's colors. +Returns a SimpleImage object. + +`pixelate($size)` - Applies the pixelate filter. +- `$size` (int) - The size of the blocks in pixels (default 10). +Returns a SimpleImage object. + +`sepia()` - Simulates a sepia effect by desaturating the image and applying a sepia tone. +Returns a SimpleImage object. + +`sketch()` - Applies the mean remove filter to produce a sketch effect. +Returns a SimpleImage object. + +### Color utilities + +`adjustColor($color, $red, $green, $blue, $alpha)` - Adjusts a color by increasing/decreasing red/green/blue/alpha values independently. +- `$color`* (string|array) - The color to adjust. +- `$red`* (int) - Red adjustment (-255 - 255). +- `$green`* (int) - Green adjustment (-255 - 255). +- `$blue`* (int) - Blue adjustment (-255 - 255). +- `$alpha`* (float) - Alpha adjustment (-1 - 1). +Returns an RGBA color array. + +`darkenColor($color, $amount)` - Darkens a color. +- `$color`* (string|array) - The color to darken. +- `$amount`* (int) - Amount to darken (0 - 255). +Returns an RGBA color array. + +`lightenColor($color, $amount)` - Lightens a color. +- `$color`* (string|array) - The color to lighten. +- `$amount`* (int) - Amount to darken (0 - 255). +Returns an RGBA color array. + +`normalizeColor($color)` - Normalizes a hex or array color value to a well-formatted RGBA array. +- `$color`* (string|array) - A CSS color name, hex string, or an array [red, green, blue, alpha]. +Returns an array [red, green, blue, alpha] + +### Exceptions + +SimpleImage throws standard exceptions when things go wrong. You should always use a try/catch block around your code to properly handle them. -// Add multiple colored text -$img->text('Your Text', 'font.ttf', 32, ['#FFFFFF' '#000000'], 'top', 0, 20); +```php +try { + $image = new \claviska\SimpleImage('image.jpeg') + // ... +} catch(Exception $err) { + echo $err->getMessage(); +} ``` -* Valid positions are *center, top, right, bottom, left, top left, top -right, bottom left, bottom right* - -### Utility Methods - -The following methods are not chainable, because they return information about -the image you're working with or output the image directly to the browser: +To check for specific errors, compare `$err->getCode()` to the defined error constants. ```php -// Get info about the original image (before any changes were made) -// -// Returns: -// -// array( -// width => 320, -// height => 200, -// orientation => ['portrait', 'landscape', 'square'], -// exif => array(...), -// mime => ['image/jpeg', 'image/gif', 'image/png'], -// format => ['jpeg', 'gif', 'png'] -// ) -$info = $img->get_original_info(); - -// Get the current width -$width = $img->get_width(); - -// Get the current height -$height = $img->get_height(); - -// Get the current orientation (returns 'portrait', 'landscape', or 'square') -$orientation = $img->get_orientation(); - -// Flip the image and output it directly to the browser (i.e. without saving to file) -$img->flip('x')->output(); -``` \ No newline at end of file +try { + $image = new \claviska\SimpleImage('image.jpeg') + // ... +} catch(Exception $err) { + if($err->getCode() === $image::ERR_FILE_NOT_FOUND) { + echo 'File not found!'; + } else { + echo $err->getMessage(); + } +} +``` + +As a best practice, always use the defined constants instead of their integers values. The values will likely change in future versions, and WILL NOT be considered a breaking change. + +- `ERR_FILE_NOT_FOUND` - The specified file could not be found or loaded for some reason. +- `ERR_FONT_FILE` - The specified font file could not be loaded. +- `ERR_FREETYPE_NOT_ENABLED` - Freetype support is not enabled in your version of PHP. +- `ERR_GD_NOT_ENABLED` - The GD extension is not enabled in your version of PHP. +- `ERR_INVALID_COLOR` - An invalid color value was passed as an argument. +- `ERR_INVALID_DATA_URI` - The specified data URI is not valid. +- `ERR_INVALID_IMAGE` - The specified image is not valid. +- `ERR_UNSUPPORTED_FORMAT` - The image format specified is not valid. +- `ERR_WEBP_NOT_ENABLED` - WEBP support is not enabled in your version of PHP. +- `ERR_WRITE` - Unable to write to the file system. + +### Useful Things To Know + +- Color arguments can be a CSS color name (e.g. `LightBlue`), a hex color string (e.g. `#0099dd`), or an RGB(A) array (e.g. `['red' => 255, 'green' => 0, 'blue' => 0, 'alpha' => 1]`). + +- When `$thickness` > 1, GD draws lines of the desired thickness from the center origin. For example, a rectangle drawn at [10, 10, 20, 20] with a thickness of 3 will actually be draw at [9, 9, 21, 21]. This is true for all shapes and is not a bug in the SimpleImage library. + +--- + +## Differences from SimpleImage 2.x + +- Normalized color arguments (colors can be a CSS color name, hex color, or RGB(A) array). +- Normalized alpha (opacity) arguments: 0 (transparent) - 1 (opaque) +- Added text shadow to `text` method. +- Added `arc` method for drawing arcs. +- Added `border` method for drawing borders. +- Added `dot` method for drawing individual pixels. +- Added `ellipse` method for drawing ellipses and circles. +- Added `line` method for drawing lines. +- Added `polygon` method for drawing polygons. +- Added `rectangle` method for drawing rectangles. +- Added `roundedRectangle` method for drawing rounded rectangles. +- Added `adjustColor` method for modifying RGBA color channels to create relative color variations. +- Added `darkenColor` method to darken a color. +- Added `lightenColor` method to lighten a color. +- Changed namespace from `abeautifulsite` to `claviska`. +- Changed `create` method to `fromNew`. +- Changed `load` method to `fromFile`. +- Changed `load_base64` method to `fromDataUri`. +- Changed `output` method to `toScreen`.x +- Changed `output_base64` method to `toDataUri`. +- Changed `save` method to `toFile`. +- Changed `text` method to accept an array of options instead of tons of arguments. +- Removed text stroke from `text` method because it produced dirty results and didn't support transparency. +- Removed `smooth` method because its arguments in the PHP manual aren't documented well. +- Removed deprecated method `adaptive_resize` (use `thumbnail` instead). +- Removed `get_meta_data` (use `getExif`, `getHeight`, `getMime`, `getOrientation`, and `getWidth` instead). +- Added [.editorconfig](http://editorconfig.org/) file. Please make sure your editor supports these settings before submitting contributions. +- Switched from four spaces to two for indentations (sorry PHP-FIG!). +- Switched from underscore_methods to camelCaseMethods. +- Organized methods into groups based on function +- Removed PHPDoc comments. At this time, I don't wish to incorporate them into the library. diff --git a/src/abeautifulsite/SimpleImage.php b/src/abeautifulsite/SimpleImage.php deleted file mode 100644 index 0ab07d3..0000000 --- a/src/abeautifulsite/SimpleImage.php +++ /dev/null @@ -1,1484 +0,0 @@ - - merging of forks, namespace support, PhpDoc editing, adaptive_resize() method, other fixes - * @license This software is licensed under the MIT license: http://opensource.org/licenses/MIT - * @copyright A Beautiful Site, LLC - * - */ - -namespace abeautifulsite; -use Exception; - -/** - * Class SimpleImage - * This class makes image manipulation in PHP as simple as possible. - * @package SimpleImage - * - */ -class SimpleImage { - - /** - * @var int Default output image quality - * - */ - public $quality = 80; - - protected $image, $filename, $original_info, $width, $height, $imagestring, $mimetype; - - /** - * Create instance and load an image, or create an image from scratch - * - * @param null|string $filename Path to image file (may be omitted to create image from scratch) - * @param int $width Image width (is used for creating image from scratch) - * @param int|null $height If omitted - assumed equal to $width (is used for creating image from scratch) - * @param null|string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). - * Where red, green, blue - integers 0-255, alpha - integer 0-127
- * (is used for creating image from scratch) - * - * @return SimpleImage - * @throws Exception - * - */ - function __construct($filename = null, $width = null, $height = null, $color = null) { - // Ignore JPEG warnings that cause imagecreatefromjpeg() to fail - ini_set('gd.jpeg_ignore_warning', 1); - - if ($filename) { - $this->load($filename); - } elseif ($width) { - $this->create($width, $height, $color); - } - return $this; - } - - /** - * Destroy image resource - * - */ - function __destruct() { - if( $this->image !== null && get_resource_type($this->image) === 'gd' ) { - imagedestroy($this->image); - } - } - - /** - * Adaptive resize - * - * This function has been deprecated and will be removed in an upcoming release. Please - * update your code to use the `thumbnail()` method instead. The arguments for both - * methods are exactly the same. - * - * @param int $width - * @param int|null $height If omitted - assumed equal to $width - * - * @return SimpleImage - * - */ - function adaptive_resize($width, $height = null) { - - return $this->thumbnail($width, $height); - - } - - /** - * Rotates and/or flips an image automatically so the orientation will be correct (based on exif 'Orientation') - * - * @return SimpleImage - * - */ - function auto_orient() { - - if(isset($this->original_info['exif']['Orientation'])) { - switch ($this->original_info['exif']['Orientation']) { - case 1: - // Do nothing - break; - case 2: - // Flip horizontal - $this->flip('x'); - break; - case 3: - // Rotate 180 counterclockwise - $this->rotate(-180); - break; - case 4: - // vertical flip - $this->flip('y'); - break; - case 5: - // Rotate 90 clockwise and flip vertically - $this->flip('y'); - $this->rotate(90); - break; - case 6: - // Rotate 90 clockwise - $this->rotate(90); - break; - case 7: - // Rotate 90 clockwise and flip horizontally - $this->flip('x'); - $this->rotate(90); - break; - case 8: - // Rotate 90 counterclockwise - $this->rotate(-90); - break; - } - } - - return $this; - - } - - /** - * Best fit (proportionally resize to fit in specified width/height) - * - * Shrink the image proportionally to fit inside a $width x $height box - * - * @param int $max_width - * @param int $max_height - * - * @return SimpleImage - * - */ - function best_fit($max_width, $max_height) { - - // If it already fits, there's nothing to do - if ($this->width <= $max_width && $this->height <= $max_height) { - return $this; - } - - // Determine aspect ratio - $aspect_ratio = $this->height / $this->width; - - // Make width fit into new dimensions - if ($this->width > $max_width) { - $width = $max_width; - $height = $width * $aspect_ratio; - } else { - $width = $this->width; - $height = $this->height; - } - - // Make height fit into new dimensions - if ($height > $max_height) { - $height = $max_height; - $width = $height / $aspect_ratio; - } - - return $this->resize($width, $height); - - } - - /** - * Blur - * - * @param string $type selective|gaussian - * @param int $passes Number of times to apply the filter - * - * @return SimpleImage - * - */ - function blur($type = 'selective', $passes = 1) { - switch (strtolower($type)) { - case 'gaussian': - $type = IMG_FILTER_GAUSSIAN_BLUR; - break; - default: - $type = IMG_FILTER_SELECTIVE_BLUR; - break; - } - for ($i = 0; $i < $passes; $i++) { - imagefilter($this->image, $type); - } - return $this; - } - - /** - * Border - * - * @param float|int $width Border width in pixels. Default 1 - * - * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). - * Where red, green, blue - integers 0-255, alpha - integer 0-127. Default #000 - * - * @return SimpleImage - * - */ - function border($width = 1, $color = '#000') { - $x1 = 0; - $y1 = 0; - $x2 = $this->width - 1; - $y2 = $this->height - 1; - $color = $this->normalize_color($color); - $color = imagecolorallocatealpha($this->image, $color['r'], $color['g'], $color['b'], 0); - - for($i = 0; $i < $width; $i++) { - imagerectangle($this->image, $x1++, $y1++, $x2--, $y2--, $color); - } - - return $this; - } - - /** - * Brightness - * - * @param int $level Darkest = -255, lightest = 255 - * - * @return SimpleImage - * - */ - function brightness($level) { - imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $this->keep_within($level, -255, 255)); - return $this; - } - - /** - * Contrast - * - * @param int $level Min = -100, max = 100 - * - * @return SimpleImage - * - * - */ - function contrast($level) { - imagefilter($this->image, IMG_FILTER_CONTRAST, $this->keep_within($level, -100, 100)); - return $this; - } - - /** - * Colorize - * - * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). - * Where red, green, blue - integers 0-255, alpha - integer 0-127 - * @param float|int $opacity 0-1 - * - * @return SimpleImage - * - */ - function colorize($color, $opacity) { - $rgba = $this->normalize_color($color); - $alpha = $this->keep_within(127 - (127 * $opacity), 0, 127); - imagefilter($this->image, IMG_FILTER_COLORIZE, $this->keep_within($rgba['r'], 0, 255), $this->keep_within($rgba['g'], 0, 255), $this->keep_within($rgba['b'], 0, 255), $alpha); - return $this; - } - - /** - * Create an image from scratch - * - * @param int $width Image width - * @param int|null $height If omitted - assumed equal to $width - * @param null|string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). - * Where red, green, blue - integers 0-255, alpha - integer 0-127 - * - * @return SimpleImage - * - */ - function create($width, $height = null, $color = null) { - - $height = $height ?: $width; - $this->width = $width; - $this->height = $height; - $this->image = imagecreatetruecolor($width, $height); - $this->original_info = array( - 'width' => $width, - 'height' => $height, - 'orientation' => $this->get_orientation(), - 'exif' => null, - 'format' => 'png', - 'mime' => 'image/png' - ); - - if ($color) { - $this->fill($color); - } - - return $this; - - } - - /** - * Crop an image - * - * @param int $x1 Left - * @param int $y1 Top - * @param int $x2 Right - * @param int $y2 Bottom - * - * @return SimpleImage - * - */ - function crop($x1, $y1, $x2, $y2) { - - // Determine crop size - if ($x2 < $x1) { - list($x1, $x2) = array($x2, $x1); - } - if ($y2 < $y1) { - list($y1, $y2) = array($y2, $y1); - } - $crop_width = $x2 - $x1; - $crop_height = $y2 - $y1; - - // Perform crop - $new = imagecreatetruecolor($crop_width, $crop_height); - imagealphablending($new, false); - imagesavealpha($new, true); - imagecopyresampled($new, $this->image, 0, 0, $x1, $y1, $crop_width, $crop_height, $crop_width, $crop_height); - - // Update meta data - $this->width = $crop_width; - $this->height = $crop_height; - $this->image = $new; - - return $this; - - } - - /** - * Desaturate - * - * @param int $percentage Level of desaturization. - * - * @return SimpleImage - * - */ - function desaturate($percentage = 100) { - - // Determine percentage - $percentage = $this->keep_within($percentage, 0, 100); - - if( $percentage === 100 ) { - imagefilter($this->image, IMG_FILTER_GRAYSCALE); - } else { - // Make a desaturated copy of the image - $new = imagecreatetruecolor($this->width, $this->height); - imagealphablending($new, false); - imagesavealpha($new, true); - imagecopy($new, $this->image, 0, 0, 0, 0, $this->width, $this->height); - imagefilter($new, IMG_FILTER_GRAYSCALE); - - // Merge with specified percentage - $this->imagecopymerge_alpha($this->image, $new, 0, 0, 0, 0, $this->width, $this->height, $percentage); - imagedestroy($new); - - } - - return $this; - } - - /** - * Edge Detect - * - * @return SimpleImage - * - */ - function edges() { - imagefilter($this->image, IMG_FILTER_EDGEDETECT); - return $this; - } - - /** - * Emboss - * - * @return SimpleImage - * - */ - function emboss() { - imagefilter($this->image, IMG_FILTER_EMBOSS); - return $this; - } - - /** - * Fill image with color - * - * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). - * Where red, green, blue - integers 0-255, alpha - integer 0-127 - * - * @return SimpleImage - * - */ - function fill($color = '#000000') { - - $rgba = $this->normalize_color($color); - $fill_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); - imagealphablending($this->image, false); - imagesavealpha($this->image, true); - imagefilledrectangle($this->image, 0, 0, $this->width, $this->height, $fill_color); - - return $this; - - } - - /** - * Fit to height (proportionally resize to specified height) - * - * @param int $height - * - * @return SimpleImage - * - */ - function fit_to_height($height) { - - $aspect_ratio = $this->height / $this->width; - $width = $height / $aspect_ratio; - - return $this->resize($width, $height); - - } - - /** - * Fit to width (proportionally resize to specified width) - * - * @param int $width - * - * @return SimpleImage - * - */ - function fit_to_width($width) { - - $aspect_ratio = $this->height / $this->width; - $height = $width * $aspect_ratio; - - return $this->resize($width, $height); - - } - - /** - * Flip an image horizontally or vertically - * - * @param string $direction x|y - * - * @return SimpleImage - * - */ - function flip($direction) { - - $new = imagecreatetruecolor($this->width, $this->height); - imagealphablending($new, false); - imagesavealpha($new, true); - - switch (strtolower($direction)) { - case 'y': - for ($y = 0; $y < $this->height; $y++) { - imagecopy($new, $this->image, 0, $y, 0, $this->height - $y - 1, $this->width, 1); - } - break; - default: - for ($x = 0; $x < $this->width; $x++) { - imagecopy($new, $this->image, $x, 0, $this->width - $x - 1, 0, 1, $this->height); - } - break; - } - - $this->image = $new; - - return $this; - - } - - /** - * Generates an image - * - * @return int - * - */ - - /** - * Get the current height - * - * @return int - * - */ - function get_height() { - return $this->height; - } - - /** - * Get the current orientation - * - * @return string portrait|landscape|square - * - */ - function get_orientation() { - - if (imagesx($this->image) > imagesy($this->image)) { - return 'landscape'; - } - - if (imagesx($this->image) < imagesy($this->image)) { - return 'portrait'; - } - - return 'square'; - - } - - /** - * Get info about the original image - * - * @return array
 array(
-     *  width        => 320,
-     *  height       => 200,
-     *  orientation  => ['portrait', 'landscape', 'square'],
-     *  exif         => array(...),
-     *  mime         => ['image/jpeg', 'image/gif', 'image/png'],
-     *  format       => ['jpeg', 'gif', 'png']
-     * )
- * - */ - function get_original_info() { - return $this->original_info; - } - - /** - * Get the current width - * - * @return int - * - */ - function get_width() { - return $this->width; - } - - /** - * Invert - * - * @return SimpleImage - * - */ - function invert() { - imagefilter($this->image, IMG_FILTER_NEGATE); - return $this; - } - - /** - * Load an image - * - * @param string $filename Path to image file - * - * @return SimpleImage - * @throws Exception - * - */ - function load($filename) { - - // Require GD library - if (!extension_loaded('gd')) { - throw new Exception('Required extension GD is not loaded.'); - } - $this->filename = $filename; - return $this->get_meta_data(); - } - - /** - * Load a base64 string as image - * - * @param string $filename base64 string - * - * @return SimpleImage - * - */ - function load_base64($base64string) { - if (!extension_loaded('gd')) { - throw new Exception('Required extension GD is not loaded.'); - } - //remove data URI scheme and spaces from base64 string then decode it - $this->imagestring = base64_decode(str_replace(' ', '+',preg_replace('#^data:image/[^;]+;base64,#', '', $base64string))); - $this->image = imagecreatefromstring($this->imagestring); - return $this->get_meta_data(); - } - - /** - * Mean Remove - * - * @return SimpleImage - * - */ - function mean_remove() { - imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); - return $this; - } - - /** - * Changes the opacity level of the image - * - * @param float|int $opacity 0-1 - * - * @throws Exception - * - */ - function opacity($opacity) { - - // Determine opacity - $opacity = $this->keep_within($opacity, 0, 1) * 100; - - // Make a copy of the image - $copy = imagecreatetruecolor($this->width, $this->height); - imagealphablending($copy, false); - imagesavealpha($copy, true); - imagecopy($copy, $this->image, 0, 0, 0, 0, $this->width, $this->height); - - // Create transparent layer - $this->create($this->width, $this->height, array(0, 0, 0, 127)); - - // Merge with specified opacity - $this->imagecopymerge_alpha($this->image, $copy, 0, 0, 0, 0, $this->width, $this->height, $opacity); - imagedestroy($copy); - - return $this; - - } - - /** - * Generates the image as a string it and sets mime type - * - * @param null|string $format If omitted or null - format of original file will be used, may be gif|jpg|png - * @param int|null $quality Output image quality in percents 0-100 - * - * @throws Exception - * - */ - protected function generate($format = null, $quality = null) { - - // Determine quality - $quality = $quality ?: $this->quality; - - // Determine mimetype - switch (strtolower($format)) { - case 'gif': - $mimetype = 'image/gif'; - break; - case 'jpeg': - case 'jpg': - imageinterlace($this->image, true); - $mimetype = 'image/jpeg'; - break; - case 'png': - $mimetype = 'image/png'; - break; - default: - $info = (empty($this->imagestring)) ? getimagesize($this->filename) : getimagesizefromstring($this->imagestring); - $mimetype = $info['mime']; - unset($info); - break; - } - - // Sets the image data - ob_start(); - switch ($mimetype) { - case 'image/gif': - imagegif($this->image); - break; - case 'image/jpeg': - imagejpeg($this->image, null, round($quality)); - break; - case 'image/png': - imagepng($this->image, null, round(9 * $quality / 100)); - break; - default: - throw new Exception('Unsupported image format: '.$this->filename); - break; - } - $imagestring = ob_get_contents(); - ob_end_clean(); - - return array($mimetype, $imagestring); - } - - /** - * Outputs image without saving - * - * @param null|string $format If omitted or null - format of original file will be used, may be gif|jpg|png - * @param int|null $quality Output image quality in percents 0-100 - * - * @throws Exception - * - */ - function output($format = null, $quality = null) { - - list( $mimetype, $imagestring ) = $this->generate( $format, $quality ); - - // Output the image - header('Content-Type: '.$mimetype); - echo $imagestring; - } - - /** - * Outputs image as data base64 to use as img src - * - * @param null|string $format If omitted or null - format of original file will be used, may be gif|jpg|png - * @param int|null $quality Output image quality in percents 0-100 - * - * @return string - * @throws Exception - * - */ - function output_base64($format = null, $quality = null) { - - list( $mimetype, $imagestring ) = $this->generate( $format, $quality ); - - // Returns formatted string for img src - return "data:{$mimetype};base64,".base64_encode($imagestring); - - } - - /** - * Overlay - * - * Overlay an image on top of another, works with 24-bit PNG alpha-transparency - * - * @param string $overlay An image filename or a SimpleImage object - * @param string $position center|top|left|bottom|right|top left|top right|bottom left|bottom right - * @param float|int $opacity Overlay opacity 0-1 - * @param int $x_offset Horizontal offset in pixels - * @param int $y_offset Vertical offset in pixels - * - * @return SimpleImage - * - */ - function overlay($overlay, $position = 'center', $opacity = 1, $x_offset = 0, $y_offset = 0) { - - // Load overlay image - if( !($overlay instanceof SimpleImage) ) { - $overlay = new SimpleImage($overlay); - } - - // Convert opacity - $opacity = $opacity * 100; - - // Determine position - switch (strtolower($position)) { - case 'top left': - $x = 0 + $x_offset; - $y = 0 + $y_offset; - break; - case 'top right': - $x = $this->width - $overlay->width + $x_offset; - $y = 0 + $y_offset; - break; - case 'top': - $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; - $y = 0 + $y_offset; - break; - case 'bottom left': - $x = 0 + $x_offset; - $y = $this->height - $overlay->height + $y_offset; - break; - case 'bottom right': - $x = $this->width - $overlay->width + $x_offset; - $y = $this->height - $overlay->height + $y_offset; - break; - case 'bottom': - $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; - $y = $this->height - $overlay->height + $y_offset; - break; - case 'left': - $x = 0 + $x_offset; - $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; - break; - case 'right': - $x = $this->width - $overlay->width + $x_offset; - $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; - break; - case 'center': - default: - $x = ($this->width / 2) - ($overlay->width / 2) + $x_offset; - $y = ($this->height / 2) - ($overlay->height / 2) + $y_offset; - break; - } - - // Perform the overlay - $this->imagecopymerge_alpha($this->image, $overlay->image, $x, $y, 0, 0, $overlay->width, $overlay->height, $opacity); - - return $this; - - } - - /** - * Pixelate - * - * @param int $block_size Size in pixels of each resulting block - * - * @return SimpleImage - * - */ - function pixelate($block_size = 10) { - imagefilter($this->image, IMG_FILTER_PIXELATE, $block_size, true); - return $this; - } - - /** - * Resize an image to the specified dimensions - * - * @param int $width - * @param int $height - * - * @return SimpleImage - * - */ - function resize($width, $height) { - - // Generate new GD image - $new = imagecreatetruecolor($width, $height); - - if( $this->original_info['format'] === 'gif' ) { - // Preserve transparency in GIFs - $transparent_index = imagecolortransparent($this->image); - $palletsize = imagecolorstotal($this->image); - if ($transparent_index >= 0 && $transparent_index < $palletsize) { - $transparent_color = imagecolorsforindex($this->image, $transparent_index); - $transparent_index = imagecolorallocate($new, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']); - imagefill($new, 0, 0, $transparent_index); - imagecolortransparent($new, $transparent_index); - } - } else { - // Preserve transparency in PNGs (benign for JPEGs) - imagealphablending($new, false); - imagesavealpha($new, true); - } - - // Resize - imagecopyresampled($new, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height); - - // Update meta data - $this->width = $width; - $this->height = $height; - $this->image = $new; - - return $this; - - } - - /** - * Rotate an image - * - * @param int $angle 0-360 - * @param string $bg_color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). - * Where red, green, blue - integers 0-255, alpha - integer 0-127 - * - * @return SimpleImage - * - */ - function rotate($angle, $bg_color = '#000000') { - - // Perform the rotation - $rgba = $this->normalize_color($bg_color); - $bg_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); - $new = imagerotate($this->image, -($this->keep_within($angle, -360, 360)), $bg_color); - imagesavealpha($new, true); - imagealphablending($new, true); - - // Update meta data - $this->width = imagesx($new); - $this->height = imagesy($new); - $this->image = $new; - - return $this; - - } - - /** - * Save an image - * - * The resulting format will be determined by the file extension. - * - * @param null|string $filename If omitted - original file will be overwritten - * @param null|int $quality Output image quality in percents 0-100 - * @param null|string $format The format to use; determined by file extension if null - * - * @return SimpleImage - * @throws Exception - * - */ - function save($filename = null, $quality = null, $format = null) { - - // Determine quality, filename, and format - $filename = $filename ?: $this->filename; - if( !$format ) - $format = $this->file_ext($filename) ?: $this->original_info['format']; - - list( $mimetype, $imagestring ) = $this->generate( $format, $quality ); - - // Save the image - $result = file_put_contents( $filename, $imagestring ); - if (!$result) - throw new Exception('Unable to save image: ' . $filename); - - return $this; - - } - - /** - * Sepia - * - * @return SimpleImage - * - */ - function sepia() { - imagefilter($this->image, IMG_FILTER_GRAYSCALE); - imagefilter($this->image, IMG_FILTER_COLORIZE, 100, 50, 0); - return $this; - } - - /** - * Sketch - * - * @return SimpleImage - * - */ - function sketch() { - imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); - return $this; - } - - /** - * Smooth - * - * @param int $level Min = -10, max = 10 - * - * @return SimpleImage - * - */ - function smooth($level) { - imagefilter($this->image, IMG_FILTER_SMOOTH, $this->keep_within($level, -10, 10)); - return $this; - } - - /** - * Add text to an image - * - * @param string $text - * @param string $font_file - * @param float|int $font_size - * @param string|array $color - * @param string $position - * @param int $x_offset - * @param int $y_offset - * @param string|array $stroke_color - * @param string $stroke_size - * @param string $alignment - * @param int $letter_spacing - * - * @return SimpleImage - * @throws Exception - * - */ - function text($text, $font_file, $font_size = 12, $color = '#000000', $position = 'center', $x_offset = 0, $y_offset = 0, $stroke_color = null, $stroke_size = null, $alignment = null, $letter_spacing = 0) { - - // todo - this method could be improved to support the text angle - $angle = 0; - - // Determine text color - if(is_array($color)) { - foreach($color as $var) { - $rgba = $this->normalize_color($var); - $color_arr[] = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); - } - } else { - $rgba = $this->normalize_color($color); - $color_arr[] = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); - } - - - // Determine textbox size - $box = imagettfbbox($font_size, $angle, $font_file, $text); - if (!$box) { - throw new Exception('Unable to load font: '.$font_file); - } - $box_width = abs($box[6] - $box[2]); - $box_height = abs($box[7] - $box[1]); - - // Determine position - switch (strtolower($position)) { - case 'top left': - $x = 0 + $x_offset; - $y = 0 + $y_offset + $box_height; - break; - case 'top right': - $x = $this->width - $box_width + $x_offset; - $y = 0 + $y_offset + $box_height; - break; - case 'top': - $x = ($this->width / 2) - ($box_width / 2) + $x_offset; - $y = 0 + $y_offset + $box_height; - break; - case 'bottom left': - $x = 0 + $x_offset; - $y = $this->height - $box_height + $y_offset + $box_height; - break; - case 'bottom right': - $x = $this->width - $box_width + $x_offset; - $y = $this->height - $box_height + $y_offset + $box_height; - break; - case 'bottom': - $x = ($this->width / 2) - ($box_width / 2) + $x_offset; - $y = $this->height - $box_height + $y_offset + $box_height; - break; - case 'left': - $x = 0 + $x_offset; - $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; - break; - case 'right'; - $x = $this->width - $box_width + $x_offset; - $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; - break; - case 'center': - default: - $x = ($this->width / 2) - ($box_width / 2) + $x_offset; - $y = ($this->height / 2) - (($box_height / 2) - $box_height) + $y_offset; - break; - } - - if($alignment === "left") { - // Left aligned text - $x = -($x * 2); - } else if($alignment === "right") { - // Right aligned text - $dimensions = imagettfbbox($font_size, $angle, $font_file, $text); - $alignment_offset = abs($dimensions[4] - $dimensions[0]); - $x = -(($x * 2) + $alignment_offset); - } - - // Add the text - imagesavealpha($this->image, true); - imagealphablending($this->image, true); - - if(isset($stroke_color) && isset($stroke_size)) { - - // Text with stroke - if(is_array($color) || is_array($stroke_color)) { - // Multi colored text and/or multi colored stroke - - if(is_array($stroke_color)) { - foreach($stroke_color as $key => $var) { - $rgba = $this->normalize_color($stroke_color[$key]); - $stroke_color[$key] = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); - } - } else { - $rgba = $this->normalize_color($stroke_color); - $stroke_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); - } - - $array_of_letters = str_split($text, 1); - - foreach($array_of_letters as $key => $var) { - - if($key > 0) { - $dimensions = imagettfbbox($font_size, $angle, $font_file, $array_of_letters[$key - 1]); - $x += abs($dimensions[4] - $dimensions[0]) + $letter_spacing; - } - - // If the next letter is empty, we just move forward to the next letter - if($var !== " ") { - $this->imagettfstroketext($this->image, $font_size, $angle, $x, $y, current($color_arr), current($stroke_color), $stroke_size, $font_file, $var); - - // #000 is 0, black will reset the array so we write it this way - if(next($color_arr) === false) { - reset($color_arr); - } - - // #000 is 0, black will reset the array so we write it this way - if(next($stroke_color) === false) { - reset($stroke_color); - } - } - } - - } else { - $rgba = $this->normalize_color($stroke_color); - $stroke_color = imagecolorallocatealpha($this->image, $rgba['r'], $rgba['g'], $rgba['b'], $rgba['a']); - $this->imagettfstroketext($this->image, $font_size, $angle, $x, $y, $color_arr[0], $stroke_color, $stroke_size, $font_file, $text); - } - - } else { - - // Text without stroke - - if(is_array($color)) { - // Multi colored text - - $array_of_letters = str_split($text, 1); - - foreach($array_of_letters as $key => $var) { - - if($key > 0) { - $dimensions = imagettfbbox($font_size, $angle, $font_file, $array_of_letters[$key - 1]); - $x += abs($dimensions[4] - $dimensions[0]) + $letter_spacing; - } - - // If the next letter is empty, we just move forward to the next letter - if($var !== " ") { - imagettftext($this->image, $font_size, $angle, $x, $y, current($color_arr), $font_file, $var); - - // #000 is 0, black will reset the array so we write it this way - if(next($color_arr) === false) { - reset($color_arr); - } - } - } - - } else { - imagettftext($this->image, $font_size, $angle, $x, $y, $color_arr[0], $font_file, $text); - } - } - - return $this; - - } - - /** - * Thumbnail - * - * This function attempts to get the image to as close to the provided dimensions as possible, and then crops the - * remaining overflow (from the center) to get the image to be the size specified. Useful for generating thumbnails. - * - * @param int $width - * @param int|null $height If omitted - assumed equal to $width - * @param string $focal - * - * @return SimpleImage - * - */ - public function thumbnail($width, $height = null, $focal = 'center') { - - // Determine height - $height = $height ?: $width; - - // Determine aspect ratios - $current_aspect_ratio = $this->height / $this->width; - $new_aspect_ratio = $height / $width; - - // Fit to height/width - if ($new_aspect_ratio > $current_aspect_ratio) { - $this->fit_to_height($height); - } else { - $this->fit_to_width($width); - } - - switch(strtolower($focal)) { - case 'top': - $left = floor(($this->width / 2) - ($width / 2)); - $right = $width + $left; - $top = 0; - $bottom = $height; - break; - case 'bottom': - $left = floor(($this->width / 2) - ($width / 2)); - $right = $width + $left; - $top = $this->height - $height; - $bottom = $this->height; - break; - case 'left': - $left = 0; - $right = $width; - $top = floor(($this->height / 2) - ($height / 2)); - $bottom = $height + $top; - break; - case 'right': - $left = $this->width - $width; - $right = $this->width; - $top = floor(($this->height / 2) - ($height / 2)); - $bottom = $height + $top; - break; - case 'top left': - $left = 0; - $right = $width; - $top = 0; - $bottom = $height; - break; - case 'top right': - $left = $this->width - $width; - $right = $this->width; - $top = 0; - $bottom = $height; - break; - case 'bottom left': - $left = 0; - $right = $width; - $top = $this->height - $height; - $bottom = $this->height; - break; - case 'bottom right': - $left = $this->width - $width; - $right = $this->width; - $top = $this->height - $height; - $bottom = $this->height; - break; - case 'center': - default: - $left = floor(($this->width / 2) - ($width / 2)); - $right = $width + $left; - $top = floor(($this->height / 2) - ($height / 2)); - $bottom = $height + $top; - break; - } - - // Return trimmed image - return $this->crop($left, $top, $right, $bottom); - } - - /** - * Returns the file extension of the specified file - * - * @param string $filename - * - * @return string - * - */ - protected function file_ext($filename) { - - if (!preg_match('/\./', $filename)) { - return ''; - } - - return preg_replace('/^.*\./', '', $filename); - - } - - /** - * Get meta data of image or base64 string - * - * @param string|null $imagestring If omitted treat as a normal image - * - * @return SimpleImage - * @throws Exception - * - */ - protected function get_meta_data() { - //gather meta data - if(empty($this->imagestring)) { - $info = getimagesize($this->filename); - - switch ($info['mime']) { - case 'image/gif': - $this->image = imagecreatefromgif($this->filename); - break; - case 'image/jpeg': - $this->image = imagecreatefromjpeg($this->filename); - break; - case 'image/png': - $this->image = imagecreatefrompng($this->filename); - break; - } - - if(!$this->image) { - throw new Exception('Invalid or corrupt image: ' . $this->filename); - } - } elseif (function_exists('getimagesizefromstring')) { - $info = getimagesizefromstring($this->imagestring); - } else { - throw new Exception('PHP 5.4 is required to use method getimagesizefromstring'); - } - - $this->original_info = array( - 'width' => $info[0], - 'height' => $info[1], - 'orientation' => $this->get_orientation(), - 'exif' => function_exists('exif_read_data') && $info['mime'] === 'image/jpeg' && $this->imagestring === null ? $this->exif = @exif_read_data($this->filename) : null, - 'format' => preg_replace('/^image\//', '', $info['mime']), - 'mime' => $info['mime'] - ); - $this->width = $info[0]; - $this->height = $info[1]; - - imagesavealpha($this->image, true); - imagealphablending($this->image, true); - - return $this; - - } - - /** - * Same as PHP's imagecopymerge() function, except preserves alpha-transparency in 24-bit PNGs - * - * @param $dst_im - * @param $src_im - * @param $dst_x - * @param $dst_y - * @param $src_x - * @param $src_y - * @param $src_w - * @param $src_h - * @param $pct - * - * @link http://www.php.net/manual/en/function.imagecopymerge.php#88456 - * - */ - protected function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { - // Merge truecolor images - if(imageistruecolor($dst_im) && imageistruecolor($src_im)) { - // Get image width and height and percentage - $pct /= 100; - $w = imagesx($src_im); - $h = imagesy($src_im); - - // Turn alpha blending off - imagealphablending($src_im, false); - - // Find the most opaque pixel in the image (the one with the smallest alpha value) - $minalpha = 127; - for ($x = 0; $x < $w; $x++) { - for ($y = 0; $y < $h; $y++) { - $alpha = (imagecolorat($src_im, $x, $y) >> 24) & 0xFF; - if ($alpha < $minalpha) { - $minalpha = $alpha; - } - } - } - - // Loop through image pixels and modify alpha for each - for ($x = 0; $x < $w; $x++) { - for ($y = 0; $y < $h; $y++) { - // Get current alpha value (represents the TANSPARENCY!) - $colorxy = imagecolorat($src_im, $x, $y); - $alpha = ($colorxy >> 24) & 0xFF; - // Calculate new alpha - if ($minalpha !== 127) { - $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minalpha); - } else { - $alpha += 127 * $pct; - } - // Get the color index with new alpha - $alphacolorxy = imagecolorallocatealpha($src_im, ($colorxy >> 16) & 0xFF, ($colorxy >> 8) & 0xFF, $colorxy & 0xFF, $alpha); - // Set pixel with the new color + opacity - if (!imagesetpixel($src_im, $x, $y, $alphacolorxy)) { - return; - } - } - } - - // Copy it - imagesavealpha($dst_im, true); - imagealphablending($dst_im, true); - imagesavealpha($src_im, true); - imagealphablending($src_im, true); - imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); - } else { - // If either image is not truecolor, fallback to standard version - if($pct === 100) { - // imagecopy handles antialiasing better at 100% - imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); - } else { - imagecopymerge($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct); - } - } - - } - - /** - * Same as imagettftext(), but allows for a stroke color and size - * - * @param object &$image A GD image object - * @param float $size The font size - * @param float $angle The angle in degrees - * @param int $x X-coordinate of the starting position - * @param int $y Y-coordinate of the starting position - * @param int &$textcolor The color index of the text - * @param int &$stroke_color The color index of the stroke - * @param int $stroke_size The stroke size in pixels - * @param string $fontfile The path to the font to use - * @param string $text The text to output - * - * @return array This method has the same return values as imagettftext() - * - */ - protected function imagettfstroketext(&$image, $size, $angle, $x, $y, &$textcolor, &$strokecolor, $stroke_size, $fontfile, $text) { - for( $c1 = ($x - abs($stroke_size)); $c1 <= ($x + abs($stroke_size)); $c1++ ) { - for($c2 = ($y - abs($stroke_size)); $c2 <= ($y + abs($stroke_size)); $c2++) { - $bg = imagettftext($image, $size, $angle, $c1, $c2, $strokecolor, $fontfile, $text); - } - } - return imagettftext($image, $size, $angle, $x, $y, $textcolor, $fontfile, $text); - } - - /** - * Ensures $value is always within $min and $max range. - * - * If lower, $min is returned. If higher, $max is returned. - * - * @param int|float $value - * @param int|float $min - * @param int|float $max - * - * @return int|float - * - */ - protected function keep_within($value, $min, $max) { - - if ($value < $min) { - return $min; - } - - if ($value > $max) { - return $max; - } - - return $value; - - } - - /** - * Converts a hex color value to its RGB equivalent - * - * @param string $color Hex color string, array(red, green, blue) or array(red, green, blue, alpha). - * Where red, green, blue - integers 0-255, alpha - integer 0-127 - * - * @return array|bool - * - */ - protected function normalize_color($color) { - - if (is_string($color)) { - - $color = trim($color, '#'); - - if (strlen($color) == 6) { - list($r, $g, $b) = array( - $color[0].$color[1], - $color[2].$color[3], - $color[4].$color[5] - ); - } elseif (strlen($color) == 3) { - list($r, $g, $b) = array( - $color[0].$color[0], - $color[1].$color[1], - $color[2].$color[2] - ); - } else { - return false; - } - return array( - 'r' => hexdec($r), - 'g' => hexdec($g), - 'b' => hexdec($b), - 'a' => 0 - ); - - } elseif (is_array($color) && (count($color) == 3 || count($color) == 4)) { - - if (isset($color['r'], $color['g'], $color['b'])) { - return array( - 'r' => $this->keep_within($color['r'], 0, 255), - 'g' => $this->keep_within($color['g'], 0, 255), - 'b' => $this->keep_within($color['b'], 0, 255), - 'a' => $this->keep_within(isset($color['a']) ? $color['a'] : 0, 0, 127) - ); - } elseif (isset($color[0], $color[1], $color[2])) { - return array( - 'r' => $this->keep_within($color[0], 0, 255), - 'g' => $this->keep_within($color[1], 0, 255), - 'b' => $this->keep_within($color[2], 0, 255), - 'a' => $this->keep_within(isset($color[3]) ? $color[3] : 0, 0, 127) - ); - } - - } - return false; - } - -} diff --git a/src/claviska/SimpleImage.php b/src/claviska/SimpleImage.php new file mode 100644 index 0000000..5e0fe93 --- /dev/null +++ b/src/claviska/SimpleImage.php @@ -0,0 +1,1563 @@ +. +// +// Copyright A Beautiful Site, LLC. +// +// Source: https://github.com/claviska/SimpleImage +// +// Licensed under the MIT license +// + +namespace claviska; + +class SimpleImage { + + const + ERR_FILE_NOT_FOUND = 1, + ERR_FONT_FILE = 2, + ERR_FREETYPE_NOT_ENABLED = 3, + ERR_GD_NOT_ENABLED = 4, + ERR_INVALID_COLOR = 5, + ERR_INVALID_DATA_URI = 6, + ERR_INVALID_IMAGE = 7, + ERR_UNSUPPORTED_FORMAT = 8, + ERR_WEBP_NOT_ENABLED = 9, + ERR_WRITE = 10; + + private $image, $mimeType, $exif; + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Magic methods + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Creates a new SimpleImage object. + // + // $image (string) - An image file or a data URI to load. + // + public function __construct($image = null) { + // Check for the required GD extension + if(extension_loaded('gd')) { + // Ignore JPEG warnings that cause imagecreatefromjpeg() to fail + ini_set('gd.jpeg_ignore_warning', 1); + } else { + throw new \Exception('Required extension GD is not loaded.', self::ERR_GD_NOT_ENABLED); + } + + // Load an image through the constructor + if(preg_match('/^data:(.*?);/', $image)) { + $this->fromDataUri($image); + } elseif($image) { + $this->fromFile($image); + } + } + + // + // Destroys the image resource + // + public function __destruct() { + if($this->image !== null && get_resource_type($this->image) === 'gd') { + imagedestroy($this->image); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Loaders + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Loads an image from a data URI. + // + // $uri* (string) - A data URI. + // + // Returns a SimpleImage instance. + // + public function fromDataUri($uri) { + // Basic formatting check + preg_match('/^data:(.*?);/', $uri, $matches); + if(!count($matches)) { + throw new \Exception('Invalid data URI.', self::ERR_INVALID_DATA_URI); + } + + // Determine mime type + $this->mimeType = $matches[1]; + if(!preg_match('/^image\/(gif|jpeg|png)$/', $this->mimeType)) { + throw new \Exception( + 'Unsupported format: ' . $this->mimeType, + self::ERR_UNSUPPORTED_FORMAT + ); + } + + // Get image data + $uri = base64_decode(preg_replace('/^data:(.*?);base64,/', '', $uri)); + $this->image = imagecreatefromstring($uri); + + return $this; + } + + // + // Loads an image from a file. + // + // $file* (string) - The image file to load. + // + // Returns a SimpleImage instance. + // + public function fromFile($file) { + // Does the file exist? + if(!file_exists($file)) { + throw new \Exception("File not found: $file", self::ERR_FILE_NOT_FOUND); + } + + // Get image info + $info = getimagesize($file); + if($info === false) { + throw new \Exception("Invalid image file: $file", self::ERR_INVALID_IMAGE); + } + $this->mimeType = $info['mime']; + + // Create image object from file + switch($this->mimeType) { + case 'image/gif': + // Load the gif + $gif = imagecreatefromgif($file); + if($gif) { + // Copy the gif over to a true color image to preserve its transparency. This is a + // workaround to prevent imagepalettetruecolor() from borking transparency. + $width = imagesx($gif); + $height = imagesy($gif); + $this->image = imagecreatetruecolor($width, $height); + $transparentColor = imagecolorallocatealpha($this->image, 0, 0, 0, 127); + imagecolortransparent($this->image, $transparentColor); + imagefill($this->image, 0, 0, $transparentColor); + imagecopy($this->image, $gif, 0, 0, 0, 0, $width, $height); + imagedestroy($gif); + } + break; + case 'image/jpeg': + $this->image = imagecreatefromjpeg($file); + break; + case 'image/png': + $this->image = imagecreatefrompng($file); + break; + case 'image/webp': + $this->image = imagecreatefromwebp($file); + break; + } + if(!$this->image) { + throw new \Exception("Unsupported image: $file", self::ERR_UNSUPPORTED_FORMAT); + } + + // Convert pallete images to true color images + imagepalettetotruecolor($this->image); + + // Load exif data from JPEG images + if($this->mimeType === 'image/jpeg' && function_exists('exif_read_data')) { + $this->exif = @exif_read_data($file); + } + + return $this; + } + + // + // Creates a new image. + // + // $width* (int) - The width of the image. + // $height* (int) - The height of the image. + // $color (string|array) - Optional fill color for the new image (default 'transparent'). + // + // Returns a SimpleImage instance. + // + public function fromNew($width, $height, $color = 'transparent') { + $this->image = imagecreatetruecolor($width, $height); + + // Use PNG for dynamically created images because it's lossless and supports transparency + $this->mimeType = 'image/png'; + + // Fill the image with color + $this->fill($color); + + return $this; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Savers + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Generates an image. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns an array containing the image data and mime type. + // + private function generate($mimeType = null, $quality = 100) { + // Format defaults to the original mime type + $mimeType = $mimeType ?: $this->mimeType; + + // Enforce quality range + $quality = $this->keepWithin($quality, 0, 100); + + // Capture output + ob_start(); + + // Generate the image + switch($mimeType) { + case 'image/gif': + imagesavealpha($this->image, true); + imagegif($this->image, null); + break; + case 'image/jpeg': + imageinterlace($this->image, true); + imagejpeg($this->image, null, $quality); + break; + case 'image/png': + imagesavealpha($this->image, true); + imagepng($this->image, null, round(9 * $quality / 100)); + break; + case 'image/webp': + // Not all versions of PHP will have webp support enabled + if(!function_exists('imagewebp')) { + throw new \Exception( + 'WEBP support is not enabled in your version of PHP.', + self::ERR_WEBP_NOT_ENABLED + ); + } + imagesavealpha($this->image, true); + imagewebp($this->image, null, $quality); + break; + default: + throw new \Exception('Unsupported format: ' . $mimeType, self::ERR_UNSUPPORTED_FORMAT); + } + + // Stop capturing + $data = ob_get_contents(); + ob_end_clean(); + + return [ + 'data' => $data, + 'mimeType' => $mimeType + ]; + } + + // + // Generates a data URI. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns a string containing a data URI. + // + public function toDataUri($mimeType = null, $quality = 100) { + $image = $this->generate($mimeType, $quality); + + return 'data:' . $image['mimeType'] . ';base64,' . base64_encode($image['data']); + } + + // + // Writes the image to a file. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns a SimpleImage instance. + // + public function toFile($file, $mimeType = null, $quality = 100) { + $image = $this->generate($mimeType, $quality); + + // Save the image to file + if(!file_put_contents($file, $image['data'])) { + throw new \Exception("Failed to write image to file: $file", self::ERR_WRITE); + } + + return $this; + } + + // + // Outputs the image to the screen. + // + // $mimeType (string) - The image format to output as a mime type (defaults to the original mime + // type). + // $quality (int) - Image quality as a percentage (default 100). + // + // Returns a SimpleImage instance. + // + public function toScreen($mimeType = null, $quality = 100) { + $image = $this->generate($mimeType, $quality); + + // Output the image to stdout + header('Content-Type: ' . $image['mimeType']); + echo $image['data']; + + return $this; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Utilities + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Ensures a numeric value is always within the min and max range. + // + // $value* (int|float) - A numeric value to test. + // $min* (int|float) - The minimum allowed value. + // $max* (int|float) - The maximum allowed value. + // + // Returns an int|float value. + // + private function keepWithin($value, $min, $max) { + if($value < $min) return $min; + if($value > $max) return $max; + return $value; + } + + // + // Gets the image's exif data. + // + // Returns an array of exif data or null if no data is available. + // + public function getExif() { + return isset($this->exif) ? $this->exif : null; + } + + // + // Gets the image's current height. + // + // Returns the height as an integer. + // + public function getHeight() { + return (int) imagesy($this->image); + } + + // + // Gets the mime type of the loaded image. + // + // Returns a mime type string. + // + public function getMimeType() { + return $this->mimeType; + } + + // + // Gets the image's current orientation. + // + // Returns a string: 'landscape', 'portrait', or 'square' + // + public function getOrientation() { + $width = $this->getWidth(); + $height = $this->getHeight(); + + if($width > $height) return 'landscape'; + if($width < $height) return 'portrait'; + return 'square'; + } + + // + // Gets the image's current width. + // + // Returns the width as an integer. + // + public function getWidth() { + return (int) imagesx($this->image); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Manipulation + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Same as PHP's imagecopymerge, but works with transparent images. Used internally for overlay. + // + private function imageCopyMergeAlpha($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH, $pct) { + // Get image width and height and percentage + $pct /= 100; + $w = imagesx($srcIm); + $h = imagesy($srcIm); + + // Turn alpha blending off + imagealphablending($srcIm, false); + + // Find the most opaque pixel in the image (the one with the smallest alpha value) + $minAlpha = 127; + for($x = 0; $x < $w; $x++) { + for($y = 0; $y < $h; $y++) { + $alpha = (imagecolorat($srcIm, $x, $y) >> 24) & 0xFF; + if($alpha < $minAlpha) { + $minAlpha = $alpha; + } + } + } + // Loop through image pixels and modify alpha for each + for($x = 0; $x < $w; $x++) { + for($y = 0; $y < $h; $y++) { + // Get current alpha value (represents the TANSPARENCY!) + $colorXY = imagecolorat($srcIm, $x, $y); + $alpha = ($colorXY >> 24) & 0xFF; + // Calculate new alpha + if($minAlpha !== 127) { + $alpha = 127 + 127 * $pct * ($alpha - 127) / (127 - $minAlpha); + } else { + $alpha += 127 * $pct; + } + // Get the color index with new alpha + $alphaColorXY = imagecolorallocatealpha( + $srcIm, + ($colorXY >> 16) & 0xFF, + ($colorXY >> 8) & 0xFF, + $colorXY & 0xFF, + $alpha + ); + // Set pixel with the new color + opacity + if(!imagesetpixel($srcIm, $x, $y, $alphaColorXY)) { + return false; + } + } + } + // Enable alpha blending and copy the image + imagealphablending($dstIm, true); + imagealphablending($srcIm, true); + imagecopy($dstIm, $srcIm, $dstX, $dstY, $srcX, $srcY, $srcW, $srcH); + + return true; + } + + // + // Rotates an image so the orientation will be correct based on its exif data. It is safe to call + // this method on images that don't have exif data (no changes will be made). + // + // Returns a SimpleImage object. + // + public function autoOrient() { + $exif = $this->getExif(); + + switch($exif['Orientation']) { + case 1: // Do nothing! + break; + case 2: // Flip horizontally + $this->flip('x'); + break; + case 3: // Rotate 180 degrees + $this->rotate(180); + break; + case 4: // Flip vertically + $this->flip('y'); + break; + case 5: // Rotate 90 degrees clockwise and flip vertically + $this->flip('y')->rotate(90); + break; + case 6: // Rotate 90 clockwise + $this->rotate(90); + break; + case 7: // Rotate 90 clockwise and flip horizontally + $this->flip('x')->rotate(90); + break; + case 8: // Rotate 90 counterclockwise + $this->rotate(-90); + break; + } + + return $this; + } + + // + // Proportionally resize the image to fit a specified width and height. + // + // $maxWidth* (int) - The maximum width the image can be. + // $maxHeight* (int) - The maximum height the image can be. + // + // Returns a SimpleImage object. + // + public function bestFit($maxWidth, $maxHeight) { + $width = $this->getWidth(); + $height = $this->getHeight(); + + if($width <= $maxWidth && $height <= $maxHeight) { + return $this; + } + + // Determine aspect ratio + $aspectRatio = $height / $width; + + // Make width fit into new dimensions + if($width > $maxWidth) { + $width = $maxWidth; + $height = $width * $aspectRatio; + } else { + $width = $width; + $height = $height; + } + + // Make height fit into new dimensions + if($height > $maxHeight) { + $height = $maxHeight; + $width = $height / $aspectRatio; + } + + return $this->resize($width, $height); + } + + // + // Crop the image. + // + // $x1 - Top left x coordinate. + // $y1 - Top left y coordinate. + // $x2 - Bottom right x coordinate. + // $y2 - Bottom right x coordinate. + // + // Returns a SimpleImage object. + // + public function crop($x1, $y1, $x2, $y2) { + // Keep crop within image dimensions + $x1 = $this->keepWithin($x1, 0, $this->getWidth()); + $x2 = $this->keepWithin($x2, 0, $this->getWidth()); + $y1 = $this->keepWithin($y1, 0, $this->getHeight()); + $y2 = $this->keepWithin($y2, 0, $this->getHeight()); + + // Crop it + $this->image = imagecrop($this->image, [ + 'x' => min($x1, $x2), + 'y' => min($y1, $y2), + 'width' => abs($x2 - $x1), + 'height' => abs($y2 - $y1) + ]); + + return $this; + } + + // + // Proportionally resize the image to a specific height. + // + // $height* (int) - The height to resize the image to. + // + // Returns a SimpleImage object. + // + public function fitToHeight($height) { + $aspectRatio = $this->getHeight() / $this->getWidth(); + $width = $height / $aspectRatio; + + return $this->resize($width, $height); + } + + // + // Proportionally resize the image to a specific width. + // + // $width* (int) - The width to resize the image to. + // + // Returns a SimpleImage object. + // + public function fitToWidth($width) { + $aspectRatio = $this->getHeight() / $this->getWidth(); + $height = $width * $aspectRatio; + + return $this->resize($width, $height); + } + + // + // Flip the image horizontally or vertically. + // + // $direction* (string) - The direction to flip: x|y|both + // + // Returns a SimpleImage object. + // + public function flip($direction) { + switch($direction) { + case 'x': + imageflip($this->image, IMG_FLIP_HORIZONTAL); + break; + case 'y': + imageflip($this->image, IMG_FLIP_VERTICAL); + break; + case 'both': + imageflip($this->image, IMG_FLIP_BOTH); + break; + } + + return $this; + } + + // + // Reduces the image to a maximum number of colors. + // + // $max* (int) - The maximum number of colors to use. + // $dither (bool) - Whether or not to use a dithering effect (default true). + // + // Returns a SimpleImage object. + // + public function maxColors($max, $dither = true) { + imagetruecolortopalette($this->image, $dither, max(1, $max)); + + return $this; + } + + // + // Place an image on top of the current image. + // + // $overlay* (string|SimpleImage) - The image to overlay. This can be a filename, a data URI, or + // a SimpleImage object. + // $anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', + // 'top right', 'bottom left', 'bottom right' (default 'center') + // $opacity (float) - The opacity level of the overlay 0-1 (default 1). + // $xOffset (int) - Horizontal offset in pixels (default 0). + // $yOffset (int) - Vertical offset in pixels (default 0). + // + // Returns a SimpleImage object. + // + public function overlay($overlay, $anchor = 'center', $opacity = 1, $xOffset = 0, $yOffset = 0) { + // Load overlay image + if(!($overlay instanceof SimpleImage)) { + $overlay = new SimpleImage($overlay); + } + + // Convert opacity + $opacity = $this->keepWithin($opacity, 0, 1) * 100; + + // Determine placement + switch($anchor) { + case 'top left': + $x = $xOffset; + $y = $yOffset; + break; + case 'top right': + $x = $this->getWidth() - $overlay->getWidth() + $xOffset; + $y = $yOffset; + break; + case 'top': + $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset; + $y = $yOffset; + break; + case 'bottom left': + $x = $xOffset; + $y = $this->getHeight() - $overlay->getHeight() + $yOffset; + break; + case 'bottom right': + $x = $this->getWidth() - $overlay->getWidth() + $xOffset; + $y = $this->getHeight() - $overlay->getHeight() + $yOffset; + break; + case 'bottom': + $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset; + $y = $this->getHeight() - $overlay->getHeight() + $yOffset; + break; + case 'left': + $x = $xOffset; + $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset; + break; + case 'right': + $x = $this->getWidth() - $overlay->getWidth() + $xOffset; + $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset; + break; + default: + $x = ($this->getWidth() / 2) - ($overlay->getWidth() / 2) + $xOffset; + $y = ($this->getHeight() / 2) - ($overlay->getHeight() / 2) + $yOffset; + break; + } + + // Perform the overlay + $this->imageCopyMergeAlpha( + $this->image, + $overlay->image, + $x, $y, + 0, 0, + $overlay->getWidth(), + $overlay->getHeight(), + $opacity + ); + + return $this; + } + + // + // Resize an image to the specified dimensions. This method WILL NOT maintain proportions. To + // resize an image without stretching it, use fitToWidth, fitToHeight, or bestFit. + // + // $width* (int) - The new image width. + // $height* (int) - The new image height. + // + // Returns a SimpleImage object. + // + public function resize($width, $height) { + // We can't use imagescale because it doesn't seem to preserve transparency properly. The + // workaround is to create a new truecolor image, allocate a transparent color, and copy the + // image over to it using imagecopyresampled. + + // Create a new true color image + $newImage = imagecreatetruecolor($width, $height); + $transparentColor = imagecolorallocatealpha($newImage, 0, 0, 0, 127); + imagecolortransparent($newImage, $transparentColor); + imagefill($newImage, 0, 0, $transparentColor); + imagecopyresampled( + $newImage, + $this->image, + 0, 0, 0, 0, + $width, + $height, + $this->getWidth(), + $this->getHeight() + ); + + // Swap out the new image + $this->image = $newImage; + + return $this; + } + + // + // Rotates the image. + // + // $angle* (int) - The angle of rotation (-360 - 360). + // $backgroundColor (string|array) - The background color to use for the uncovered zone area + // after rotation (default 'transparent'). + // + // Returns a SimpleImage object. + // + public function rotate($angle, $backgroundColor = 'transparent') { + // Rotate the image on a canvas with the desired background color + $backgroundColor = $this->allocateColor($backgroundColor); + + $this->image = imagerotate( + $this->image, + -($this->keepWithin($angle, -360, 360)), + $backgroundColor + ); + + return $this; + } + + // + // Adds text to the image. + // + // $text* (string) - The desired text. + // $options (array) - An array of options. + // - fontFile* (string) - The TrueType (or compatible) font file to use. + // - size (int) - The size of the font in pixels (default 12). + // - color (string|array) - The text color (default black). + // - anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', + // 'top left', 'top right', 'bottom left', 'bottom right' (default 'center'). + // - xOffset (int) - The horizontal offset in pixels (default 0). + // - yOffset (int) - The vertical offset in pixels (default 0). + // - shadow (array) - Text shadow params. + // - x* (int) - Horizontal offset in pixels. + // - y* (int) - Vertical offset in pixels. + // - color* (string|array) - The text shadow color. + // + // Returns a SimpleImage object. + // + public function text($text, $options) { + // Check for freetype support + if(!function_exists('imagettftext')) { + throw new \Exception( + 'Freetype support is not enabled in your version of PHP.', + self::ERR_FREETYPE_NOT_ENABLED + ); + } + + // Default options + $options = array_merge([ + 'fontFile' => null, + 'size' => 12, + 'color' => 'black', + 'anchor' => 'center', + 'xOffset' => 0, + 'yOffset' => 0, + 'shadow' => null + ], $options); + + // Extract and normalize options + $fontFile = $options['fontFile']; + $size = ($options['size'] / 96) * 72; // Convert px to pt (72pt per inch, 96px per inch) + $color = $this->allocateColor($options['color']); + $anchor = $options['anchor']; + $xOffset = $options['xOffset']; + $yOffset = $options['yOffset']; + $angle = 0; + + // Determine textbox dimensions + $box = imagettfbbox($size, $angle, $fontFile, $text); + if(!$box) { + throw new \Exception("Unable to load font file: $fontFile", self::ERR_FONT_FILE); + } + $boxWidth = abs($box[6] - $box[2]); + $boxHeight = abs($box[7] - $box[1]); + + // Determine position + switch($anchor) { + case 'top left': + $x = $xOffset; + $y = $yOffset + $boxHeight; + break; + case 'top right': + $x = $this->getWidth() - $boxWidth + $xOffset; + $y = $yOffset + $boxHeight; + break; + case 'top': + $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; + $y = $yOffset + $boxHeight; + break; + case 'bottom left': + $x = $xOffset; + $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight; + break; + case 'bottom right': + $x = $this->getWidth() - $boxWidth + $xOffset; + $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight; + break; + case 'bottom': + $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; + $y = $this->getHeight() - $boxHeight + $yOffset + $boxHeight; + break; + case 'left': + $x = $xOffset; + $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; + break; + case 'right'; + $x = $this->getWidth() - $boxWidth + $xOffset; + $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; + break; + default: // center + $x = ($this->getWidth() / 2) - ($boxWidth / 2) + $xOffset; + $y = ($this->getHeight() / 2) - (($boxHeight / 2) - $boxHeight) + $yOffset; + break; + } + + // Text shadow + if(is_array($options['shadow'])) { + imagettftext( + $this->image, + $size, + $angle, + $x + $options['shadow']['x'], + $y + $options['shadow']['y'], + $this->allocateColor($options['shadow']['color']), + $fontFile, + $text + ); + } + + // Draw the text + imagettftext($this->image, $size, $angle, $x, $y, $color, $fontFile, $text); + + return $this; + } + + // + // Creates a thumbnail image. This function attempts to get the image as close to the provided + // dimensions as possible, then crops the remaining overflow to force the desired size. Useful + // for generating thumbnail images. + // + // $width* (int) - The thumbnail width. + // $height* (int) - The thumbnail height. + // $anchor (string) - The anchor point: 'center', 'top', 'bottom', 'left', 'right', 'top left', + // 'top right', 'bottom left', 'bottom right' (default 'center'). + // + // Returns a SimpleImage object. + // + public function thumbnail($width, $height, $anchor = 'center') { + // Determine aspect ratios + $currentRatio = $this->getHeight() / $this->getWidth(); + $targetRatio = $height / $width; + + // Fit to height/width + if($targetRatio > $currentRatio) { + $this->fitToHeight($height); + } else { + $this->fitToWidth($width); + } + + switch($anchor) { + case 'top': + $x1 = floor(($this->getWidth() / 2) - ($width / 2)); + $x2 = $width + $x1; + $y1 = 0; + $y2 = $height; + break; + case 'bottom': + $x1 = floor(($this->getWidth() / 2) - ($width / 2)); + $x2 = $width + $x1; + $y1 = $this->getHeight() - $height; + $y2 = $this->getHeight(); + break; + case 'left': + $x1 = 0; + $x2 = $width; + $y1 = floor(($this->getHeight() / 2) - ($height / 2)); + $y2 = $height + $y1; + break; + case 'right': + $x1 = $this->getWidth() - $width; + $x2 = $this->getWidth(); + $y1 = floor(($this->getHeight() / 2) - ($height / 2)); + $y2 = $height + $y1; + break; + case 'top left': + $x1 = 0; + $x2 = $width; + $y1 = 0; + $y2 = $height; + break; + case 'top right': + $x1 = $this->getWidth() - $width; + $x2 = $this->getWidth(); + $y1 = 0; + $y2 = $height; + break; + case 'bottom left': + $x1 = 0; + $x2 = $width; + $y1 = $this->getHeight() - $height; + $y2 = $this->getHeight(); + break; + case 'bottom right': + $x1 = $this->getWidth() - $width; + $x2 = $this->getWidth(); + $y1 = $this->getHeight() - $height; + $y2 = $this->getHeight(); + break; + default: + $x1 = floor(($this->getWidth() / 2) - ($width / 2)); + $x2 = $width + $x1; + $y1 = floor(($this->getHeight() / 2) - ($height / 2)); + $y2 = $height + $y1; + break; + } + + // Return the cropped thumbnail image + return $this->crop($x1, $y1, $x2, $y2); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Drawing + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Draws an arc. + // + // $x* (int) - The x coordinate of the arc's center. + // $y* (int) - The y coordinate of the arc's center. + // $width* (int) - The width of the arc. + // $height* (int) - The height of the arc. + // $start* (int) - The start of the arc in degrees. + // $end* (int) - The end of the arc in degrees. + // $color* (string|array) - The arc color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function arc($x, $y, $width, $height, $start, $end, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw an arc + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledarc($this->image, $x, $y, $width, $height, $start, $end, $color, IMG_ARC_PIE); + } else { + imagesetthickness($this->image, $thickness); + imagearc($this->image, $x, $y, $width, $height, $start, $end, $color); + } + + return $this; + } + + // + // Draws a border around the image. + // + // $color* (string|array) - The border color. + // $thickness (int) - The thickness of the border (default 1). + // + // Returns a SimpleImage object. + // + public function border($color, $thickness = 1) { + $x1 = 0; + $y1 = 0; + $x2 = $this->getWidth() - 1; + $y2 = $this->getHeight() - 1; + + // Draw a border rectangle until it reaches the correct width + for($i = 0; $i < $thickness; $i++) { + $this->rectangle($x1++, $y1++, $x2--, $y2--, $color); + } + + return $this; + } + + // + // Draws a single pixel dot. + // + // $x* (int) - The x coordinate of the dot. + // $y* (int) - The y coordinate of the dot. + // $color* (string|array) - The dot color. + // + // Returns a SimpleImage object. + // + public function dot($x, $y, $color) { + $color = $this->allocateColor($color); + imagesetpixel($this->image, $x, $y, $color); + + return $this; + } + + // + // Draws an ellipse. + // + // $x* (int) - The x coordinate of the center. + // $y* (int) - The y coordinate of the center. + // $width* (int) - The ellipse width. + // $height* (int) - The ellipse height. + // $color* (string|array) - The ellipse color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function ellipse($x, $y, $width, $height, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw an ellipse + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledellipse($this->image, $x, $y, $width, $height, $color); + } else { + // imagesetthickness doesn't appear to work with imageellipse, so we work around it. + imagesetthickness($this->image, 1); + $i = 0; + while($i++ < $thickness * 2 - 1) { + imageellipse($this->image, $x, $y, --$width, $height--, $color); + } + } + + return $this; + } + + // + // Fills the image with a solid color. + // + // $color (string|array) - The fill color. + // + // Returns a SimpleImage object. + // + public function fill($color) { + // Draw a filled rectangle over the entire image + $this->rectangle(0, 0, $this->getWidth(), $this->getHeight(), $color, 'filled'); + + return $this; + } + + // + // Draws a line. + // + // $x1* (int) - The x coordinate for the first point. + // $y1* (int) - The y coordinate for the first point. + // $x2* (int) - The x coordinate for the second point. + // $y2* (int) - The y coordinate for the second point. + // $color (string|array) - The line color. + // $thickness (int) - The line thickness (default 1). + // + // Returns a SimpleImage object. + // + public function line($x1, $y1, $x2, $y2, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw a line + imagesetthickness($this->image, $thickness); + imageline($this->image, $x1, $y1, $x2, $y2, $color); + + return $this; + } + + // + // Draws a polygon. + // + // $vertices* (array) - The polygon's vertices in an array of x/y arrays. Example: + // [ + // ['x' => x1, 'y' => y1], + // ['x' => x2, 'y' => y2], + // ['x' => xN, 'y' => yN] + // ] + // $color* (string|array) - The polygon color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function polygon($vertices, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Convert [['x' => x1, 'y' => x1], ['x' => x1, 'y' => y2], ...] to [x1, y1, x2, y2, ...] + $points = []; + foreach($vertices as $vals) { + $points[] = $vals['x']; + $points[] = $vals['y']; + } + + // Draw a polygon + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledpolygon($this->image, $points, count($vertices), $color); + } else { + imagesetthickness($this->image, $thickness); + imagepolygon($this->image, $points, count($vertices), $color); + } + + return $this; + } + + // + // Draws a rectangle. + // + // $x1* (int) - The upper left x coordinate. + // $y1* (int) - The upper left y coordinate. + // $x2* (int) - The bottom right x coordinate. + // $y2* (int) - The bottom right y coordinate. + // $color* (string|array) - The rectangle color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function rectangle($x1, $y1, $x2, $y2, $color, $thickness = 1) { + // Allocate the color + $color = $this->allocateColor($color); + + // Draw a rectangle + if($thickness === 'filled') { + imagesetthickness($this->image, 1); + imagefilledrectangle($this->image, $x1, $y1, $x2, $y2, $color); + } else { + imagesetthickness($this->image, $thickness); + imagerectangle($this->image, $x1, $y1, $x2, $y2, $color); + } + + return $this; + } + + // + // Draws a rounded rectangle. + // + // $x1* (int) - The upper left x coordinate. + // $y1* (int) - The upper left y coordinate. + // $x2* (int) - The bottom right x coordinate. + // $y2* (int) - The bottom right y coordinate. + // $radius* (int) - The border radius in pixels. + // $color* (string|array) - The rectangle color. + // $thickness (int|string) - Line thickness in pixels or 'filled' (default 1). + // + // Returns a SimpleImage object. + // + public function roundedRectangle($x1, $y1, $x2, $y2, $radius, $color, $thickness = 1) { + if($thickness === 'filled') { + // Draw the filled rectangle without edges + $this->rectangle($x1 + $radius + 1, $y1, $x2 - $radius - 1, $y2, $color, 'filled'); + $this->rectangle($x1, $y1 + $radius + 1, $x1 + $radius, $y2 - $radius - 1, $color, 'filled'); + $this->rectangle($x2 - $radius, $y1 + $radius + 1, $x2, $y2 - $radius - 1, $color, 'filled'); + // Fill in the edges with arcs + $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, 'filled'); + $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, 'filled'); + $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, 'filled'); + $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, 'filled'); + } else { + // Draw the rectangle outline without edges + $this->line($x1 + $radius, $y1, $x2 - $radius, $y1, $color, $thickness); + $this->line($x1 + $radius, $y2, $x2 - $radius, $y2, $color, $thickness); + $this->line($x1, $y1 + $radius, $x1, $y2 - $radius, $color, $thickness); + $this->line($x2, $y1 + $radius, $x2, $y2 - $radius, $color, $thickness); + // Fill in the edges with arcs + $this->arc($x1 + $radius, $y1 + $radius, $radius * 2, $radius * 2, 180, 270, $color, $thickness); + $this->arc($x2 - $radius, $y1 + $radius, $radius * 2, $radius * 2, 270, 360, $color, $thickness); + $this->arc($x1 + $radius, $y2 - $radius, $radius * 2, $radius * 2, 90, 180, $color, $thickness); + $this->arc($x2 - $radius, $y2 - $radius, $radius * 2, $radius * 2, 360, 90, $color, $thickness); + } + + return $this; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Filters + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Applies the blur filter. + // + // $type (string) - The blur algorithm to use: 'selective', 'gaussian' (default 'gaussian'). + // $passes (int) - The number of time to apply the filter, enhancing the effect (default 1). + // + // Returns a SimpleImage object. + // + public function blur($type = 'selective', $passes = 1) { + $filter = $type === 'gaussian' ? IMG_FILTER_GAUSSIAN_BLUR : IMG_FILTER_SELECTIVE_BLUR; + + for($i = 0; $i < $passes; $i++) { + imagefilter($this->image, $filter); + } + + return $this; + } + + // + // Applies the brightness filter to brighten the image. + // + // $percentage* (int) - Percentage to brighten the image (0 - 100). + // + // Returns a SimpleImage object. + // + public function brighten($percentage) { + $percentage = $this->keepWithin(255 * $percentage / 100, 0, 255); + + imagefilter($this->image, IMG_FILTER_BRIGHTNESS, $percentage); + + return $this; + } + + // + // Applies the colorize filter. + // + // $color* (string|array) - The filter color. + // + // Returns a SimpleImage object. + // + public function colorize($color) { + $color = $this->normalizeColor($color); + + imagefilter( + $this->image, + IMG_FILTER_COLORIZE, + $color['red'], + $color['green'], + $color['blue'], + $color['alpha'] + ); + + return $this; + } + + // + // Applies the contrast filter. + // + // $percentage* (int) - Percentage to adjust (-100 - 100). + // + // Returns a SimpleImage object. + // + public function contrast($percentage) { + imagefilter($this->image, IMG_FILTER_CONTRAST, $this->keepWithin($percentage, -100, 100)); + + return $this; + } + + // + // Applies the brightness filter to darken the image. + // + // $percentage* (int) - Percentage to darken the image (0 - 100). + // + // Returns a SimpleImage object. + // + public function darken($percentage) { + $percentage = $this->keepWithin(255 * $percentage / 100, 0, 255); + + imagefilter($this->image, IMG_FILTER_BRIGHTNESS, -$percentage); + + return $this; + } + + // + // Applies the desaturate (grayscale) filter. + // + // Returns a SimpleImage object. + // + public function desaturate() { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + + return $this; + } + + // + // Applies the edge detect filter. + // + // Returns a SimpleImage object. + // + public function edgeDetect() { + imagefilter($this->image, IMG_FILTER_EDGEDETECT); + + return $this; + } + + // + // Applies the emboss filter. + // + // Returns a SimpleImage object. + // + public function emboss() { + imagefilter($this->image, IMG_FILTER_EMBOSS); + + return $this; + } + + // + // Inverts the image's colors. + // + // Returns a SimpleImage object. + // + public function invert() { + imagefilter($this->image, IMG_FILTER_NEGATE); + + return $this; + } + + // + // Applies the pixelate filter. + // + // $size (int) - The size of the blocks in pixels (default 10). + // + // Returns a SimpleImage object. + // + public function pixelate($size = 10) { + imagefilter($this->image, IMG_FILTER_PIXELATE, $size, true); + + return $this; + } + + // + // Simulates a sepia effect by desaturating the image and applying a sepia tone. + // + // Returns a SimpleImage object. + // + public function sepia() { + imagefilter($this->image, IMG_FILTER_GRAYSCALE); + imagefilter($this->image, IMG_FILTER_COLORIZE, 70, 35, 0); + + return $this; + } + + // + // Applies the mean remove filter to produce a sketch effect. + // + // Returns a SimpleImage object. + // + public function sketch() { + imagefilter($this->image, IMG_FILTER_MEAN_REMOVAL); + + return $this; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // Color utilities + ////////////////////////////////////////////////////////////////////////////////////////////////// + + // + // Converts a "friendly color" into a color identifier for use with GD's image functions. + // + // $image (resource) - The target image. + // $color (string|array) - The color to allocate. + // + // Returns a color identifier. + // + private function allocateColor($color) { + $color = $this->normalizeColor($color); + + // Was this color already allocated? + $index = imagecolorexactalpha( + $this->image, + $color['red'], + $color['green'], + $color['blue'], + $color['alpha'] + ); + if($index > -1) { + // Yes, return this color index + return $index; + } + + // Allocate a new color index + return imagecolorallocatealpha( + $this->image, + $color['red'], + $color['green'], + $color['blue'], + $color['alpha'] + ); + } + + // + // Adjusts a color by increasing/decreasing red/green/blue/alpha values independently. + // + // $color* (string|array) - The color to adjust. + // $red* (int) - Red adjustment (-255 - 255). + // $green* (int) - Green adjustment (-255 - 255). + // $blue* (int) - Blue adjustment (-255 - 255). + // $alpha* (float) - Alpha adjustment (-1 - 1). + // + // Returns an RGBA color array. + // + public function adjustColor($color, $red, $green, $blue, $alpha) { + // Normalize to RGBA + $color = $this->normalizeColor($color); + + // Adjust each channel + return $this->normalizeColor([ + 'red' => $color['red'] + $red, + 'green' => $color['green'] + $green, + 'blue' => $color['blue'] + $blue, + 'alpha' => $color['alpha'] + $alpha + ]); + } + + // + // Darkens a color. + // + // $color* (string|array) - The color to darken. + // $amount* (int) - Amount to darken (0 - 255). + // + // Returns an RGBA color array. + // + public function darkenColor($color, $amount) { + return $this->adjustColor($color, -$amount, -$amount, -$amount, 0); + } + + // + // Lightens a color. + // + // $color* (string|array) - The color to lighten. + // $amount* (int) - Amount to darken (0 - 255). + // + // Returns an RGBA color array. + // + public function lightenColor($color, $amount) { + return $this->adjustColor($color, $amount, $amount, $amount, 0); + } + + // + // Normalizes a hex or array color value to a well-formatted RGBA array. + // + // $color* (string|array) - A CSS color name, hex string, or an array [red, green, blue, alpha]. + // + // Returns an array [red, green, blue, alpha] + // + public function normalizeColor($color) { + // 140 CSS color names and hex values + $cssColors = [ + 'aliceblue' => '#f0f8ff', 'antiquewhite' => '#faebd7', 'aqua' => '#00ffff', + 'aquamarine' => '#7fffd4', 'azure' => '#f0ffff', 'beige' => '#f5f5dc', 'bisque' => '#ffe4c4', + 'black' => '#000000', 'blanchedalmond' => '#ffebcd', 'blue' => '#0000ff', + 'blueviolet' => '#8a2be2', 'brown' => '#a52a2a', 'burlywood' => '#deb887', + 'cadetblue' => '#5f9ea0', 'chartreuse' => '#7fff00', 'chocolate' => '#d2691e', + 'coral' => '#ff7f50', 'cornflowerblue' => '#6495ed', 'cornsilk' => '#fff8dc', + 'crimson' => '#dc143c', 'cyan' => '#00ffff', 'darkblue' => '#00008b', 'darkcyan' => '#008b8b', + 'darkgoldenrod' => '#b8860b', 'darkgray' => '#a9a9a9', 'darkgrey' => '#a9a9a9', + 'darkgreen' => '#006400', 'darkkhaki' => '#bdb76b', 'darkmagenta' => '#8b008b', + 'darkolivegreen' => '#556b2f', 'darkorange' => '#ff8c00', 'darkorchid' => '#9932cc', + 'darkred' => '#8b0000', 'darksalmon' => '#e9967a', 'darkseagreen' => '#8fbc8f', + 'darkslateblue' => '#483d8b', 'darkslategray' => '#2f4f4f', 'darkslategrey' => '#2f4f4f', + 'darkturquoise' => '#00ced1', 'darkviolet' => '#9400d3', 'deeppink' => '#ff1493', + 'deepskyblue' => '#00bfff', 'dimgray' => '#696969', 'dimgrey' => '#696969', + 'dodgerblue' => '#1e90ff', 'firebrick' => '#b22222', 'floralwhite' => '#fffaf0', + 'forestgreen' => '#228b22', 'fuchsia' => '#ff00ff', 'gainsboro' => '#dcdcdc', + 'ghostwhite' => '#f8f8ff', 'gold' => '#ffd700', 'goldenrod' => '#daa520', 'gray' => '#808080', + 'grey' => '#808080', 'green' => '#008000', 'greenyellow' => '#adff2f', + 'honeydew' => '#f0fff0', 'hotpink' => '#ff69b4', 'indianred ' => '#cd5c5c', + 'indigo ' => '#4b0082', 'ivory' => '#fffff0', 'khaki' => '#f0e68c', 'lavender' => '#e6e6fa', + 'lavenderblush' => '#fff0f5', 'lawngreen' => '#7cfc00', 'lemonchiffon' => '#fffacd', + 'lightblue' => '#add8e6', 'lightcoral' => '#f08080', 'lightcyan' => '#e0ffff', + 'lightgoldenrodyellow' => '#fafad2', 'lightgray' => '#d3d3d3', 'lightgrey' => '#d3d3d3', + 'lightgreen' => '#90ee90', 'lightpink' => '#ffb6c1', 'lightsalmon' => '#ffa07a', + 'lightseagreen' => '#20b2aa', 'lightskyblue' => '#87cefa', 'lightslategray' => '#778899', + 'lightslategrey' => '#778899', 'lightsteelblue' => '#b0c4de', 'lightyellow' => '#ffffe0', + 'lime' => '#00ff00', 'limegreen' => '#32cd32', 'linen' => '#faf0e6', 'magenta' => '#ff00ff', + 'maroon' => '#800000', 'mediumaquamarine' => '#66cdaa', 'mediumblue' => '#0000cd', + 'mediumorchid' => '#ba55d3', 'mediumpurple' => '#9370db', 'mediumseagreen' => '#3cb371', + 'mediumslateblue' => '#7b68ee', 'mediumspringgreen' => '#00fa9a', + 'mediumturquoise' => '#48d1cc', 'mediumvioletred' => '#c71585', 'midnightblue' => '#191970', + 'mintcream' => '#f5fffa', 'mistyrose' => '#ffe4e1', 'moccasin' => '#ffe4b5', + 'navajowhite' => '#ffdead', 'navy' => '#000080', 'oldlace' => '#fdf5e6', 'olive' => '#808000', + 'olivedrab' => '#6b8e23', 'orange' => '#ffa500', 'orangered' => '#ff4500', + 'orchid' => '#da70d6', 'palegoldenrod' => '#eee8aa', 'palegreen' => '#98fb98', + 'paleturquoise' => '#afeeee', 'palevioletred' => '#db7093', 'papayawhip' => '#ffefd5', + 'peachpuff' => '#ffdab9', 'peru' => '#cd853f', 'pink' => '#ffc0cb', 'plum' => '#dda0dd', + 'powderblue' => '#b0e0e6', 'purple' => '#800080', 'rebeccapurple' => '#663399', + 'red' => '#ff0000', 'rosybrown' => '#bc8f8f', 'royalblue' => '#4169e1', + 'saddlebrown' => '#8b4513', 'salmon' => '#fa8072', 'sandybrown' => '#f4a460', + 'seagreen' => '#2e8b57', 'seashell' => '#fff5ee', 'sienna' => '#a0522d', + 'silver' => '#c0c0c0', 'skyblue' => '#87ceeb', 'slateblue' => '#6a5acd', + 'slategray' => '#708090', 'slategrey' => '#708090', 'snow' => '#fffafa', + 'springgreen' => '#00ff7f', 'steelblue' => '#4682b4', 'tan' => '#d2b48c', 'teal' => '#008080', + 'thistle' => '#d8bfd8', 'tomato' => '#ff6347', 'turquoise' => '#40e0d0', + 'violet' => '#ee82ee', 'wheat' => '#f5deb3', 'white' => '#ffffff', 'whitesmoke' => '#f5f5f5', + 'yellow' => '#ffff00', 'yellowgreen' => '#9acd32' + ]; + + // Translate CSS color names to hex values + if(is_string($color) && array_key_exists(strtolower($color), $cssColors)) { + $color = $cssColors[strtolower($color)]; + } + + // Translate transparent keyword to a transparent color + if($color === 'transparent') { + $color = ['red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => 0]; + } + + // Convert hex values to RGBA + if(is_string($color)) { + // Remove # + $hex = preg_replace('/^#/', '', $color); + + // Support short and standard hex codes + if(strlen($hex) === 3) { + list($red, $green, $blue) = [ + $hex[0] . $hex[0], + $hex[1] . $hex[1], + $hex[2] . $hex[2] + ]; + } elseif(strlen($hex) === 6) { + list($red, $green, $blue) = [ + $hex[0] . $hex[1], + $hex[2] . $hex[3], + $hex[4] . $hex[5] + ]; + } else { + throw new \Exception("Invalid color value: $color", self::ERR_INVALID_COLOR); + } + + // Turn color into an array + $color = [ + 'red' => hexdec($red), + 'green' => hexdec($green), + 'blue' => hexdec($blue), + 'alpha' => 1 + ]; + } + + // Enforce color value ranges + if(is_array($color)) { + // RGB default to 0 + $color['red'] = isset($color['red']) ? $color['red'] : 0; + $color['green'] = isset($color['green']) ? $color['green'] : 0; + $color['blue'] = isset($color['blue']) ? $color['blue'] : 0; + + // Alpha defaults to 1 (fully opaque) + $color['alpha'] = isset($color['alpha']) ? $color['alpha'] : 1; + + $arr = [ + 'red' => (int) $this->keepWithin((int) $color['red'], 0, 255), + 'green' => (int) $this->keepWithin((int) $color['green'], 0, 255), + 'blue' => (int) $this->keepWithin((int) $color['blue'], 0, 255), + // Convert 0-1 alpha values to 0-127 (to mimic the way CSS opacity works) + 'alpha' => $this->keepWithin(127 - round(127 * $color['alpha']), 0, 127) + ]; + + return [ + 'red' => (int) $this->keepWithin((int) $color['red'], 0, 255), + 'green' => (int) $this->keepWithin((int) $color['green'], 0, 255), + 'blue' => (int) $this->keepWithin((int) $color['blue'], 0, 255), + // Convert 0-1 alpha values to 0-127 (to mimic the way CSS opacity works) + 'alpha' => $this->keepWithin(127 - round(127 * $color['alpha']), 0, 127) + ]; + } + + throw new \Exception("Invalid color value: $color", self::ERR_INVALID_COLOR); + } + +}