初始化项目

This commit is contained in:
2026-09-01 22:15:50 +08:00
commit b5a26a8848
11217 changed files with 1710987 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
{
"name": "kosinix/grafika",
"description": "An image manipulation library for PHP.",
"keywords": ["grafika"],
"homepage": "http://kosinix.github.io/grafika",
"type": "library",
"license": [
"MIT",
"GPL-2.0+"
],
"authors": [
{
"name": "Nico Amarilla",
"homepage": "https://www.kosinix.com"
}
],
"require": {
"php": ">=5.3"
},
"autoload": {
"psr-4": {
"Grafika\\": "src/Grafika"
}
}
}
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2016 Nico G. Amarilla
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+105
View File
@@ -0,0 +1,105 @@
<?php
namespace Grafika;
/**
* Holds the color information.
* @package Grafika
*/
class Color {
/**
* @var string Hex string: #FFFFFF
*/
protected $hexString;
/**
* @var float Transparency value 0-1
*/
protected $alpha;
/**
* Color constructor.
*
* @param string $hexString Hex string
* @param float $alpha Transparency value 0-1
*/
public function __construct( $hexString = '', $alpha = 1.0 ){
$this->hexString = $hexString; // TODO: Validate hexstring
$this->alpha = $alpha;
}
/**
* Get RGB array
*
* @return array Contains array($r, $g, $b)
*/
public function getRgb(){
return $this->hexToRgb( $this->hexString );
}
/**
* Get RGBA array
*
* @return array Contains array($r, $g, $b, $a)
*/
public function getRgba(){
$rgba = $this->hexToRgb( $this->hexString );
$rgba[] = $this->alpha;
return $rgba;
}
/**
* Convert hex string to RGB
* @param string $hex Hex string. Possible values: #ffffff, #fff, fff
* @return array Contains (RGB) values red, green and blue
*/
public function hexToRgb( $hex ) {
$hex = ltrim($hex, '#'); // Remove #
if(strlen($hex) == 3) {
$r = hexdec(substr($hex,0,1).substr($hex,0,1));
$g = hexdec(substr($hex,1,1).substr($hex,1,1));
$b = hexdec(substr($hex,2,1).substr($hex,2,1));
} else {
$r = hexdec(substr($hex,0,2));
$g = hexdec(substr($hex,2,2));
$b = hexdec(substr($hex,4,2));
}
return array($r, $g, $b); // Returns an array with the rgb values
}
/**
* Get hex string.
*
* @return string
*/
public function getHexString() {
return $this->hexString;
}
/**
* Set hex string.
*
* @param string $hexString
*/
public function setHexString($hexString) {
$this->hexString = $hexString;
}
/**
* Alpha value.
* @return float
*/
public function getAlpha() {
return $this->alpha;
}
/**
* @param float $alpha
*/
public function setAlpha($alpha) {
$this->alpha = $alpha;
}
}
@@ -0,0 +1,105 @@
<?php
namespace Grafika\DrawingObject;
use Grafika\Color;
/**
* Base class
* @package Grafika
*/
abstract class CubicBezier
{
/**
* Starting point. Array of X Y values.
* @var array
*/
protected $point1;
/**
* Control point 1. Array of X Y values.
* @var array
*/
protected $control1;
/**
* Control point 2. Array of X Y values.
* @var array
*/
protected $control2;
/**
* End point. Array of X Y values.
* @var array
*/
protected $point2;
/**
* Color of curve.
*
* @var Color
*/
protected $color;
/**
* Creates a cubic bezier. Cubic bezier has 2 control points.
* @param array $point1 Array of X and Y value for start point.
* @param array $control1 Array of X and Y value for control point 1.
* @param array $control2 Array of X and Y value for control point 2.
* @param array $point2 Array of X and Y value for end point.
* @param Color|string $color Color of the curve. Accepts hex string or a Color object. Defaults to black.
*/
public function __construct($point1, $control1, $control2, $point2, $color = '#000000')
{
if (is_string($color)) {
$color = new Color($color);
}
$this->point1 = $point1;
$this->control1 = $control1;
$this->control2 = $control2;
$this->point2 = $point2;
$this->color = $color;
}
/**
* @return array
*/
public function getPoint1()
{
return $this->point1;
}
/**
* @return array
*/
public function getControl1()
{
return $this->control1;
}
/**
* @return array
*/
public function getControl2()
{
return $this->control2;
}
/**
* @return array
*/
public function getPoint2()
{
return $this->point2;
}
/**
* @return Color
*/
public function getColor()
{
return $this->color;
}
}
@@ -0,0 +1,128 @@
<?php
namespace Grafika\DrawingObject;
use Grafika\Color;
/**
* Base class
* @package Grafika
*/
abstract class Ellipse
{
/**
* Image width in pixels
* @var int
*/
protected $width;
/**
* Image height in pixels
* @var int
*/
protected $height;
/**
* X,Y pos.
* @var array
*/
protected $pos;
/**
* @var int
*/
protected $borderSize;
/**
* @var Color
*/
protected $fillColor;
/**
* @var Color
*/
protected $borderColor;
/**
* Creates an ellipse.
*
* @param int $width Width of ellipse in pixels.
* @param int $height Height of ellipse in pixels.
* @param array $pos Array containing int X and int Y position of the ellipse from top left of the canvass.
* @param int $borderSize Size of the border in pixels. Defaults to 1 pixel. Set to 0 for no border.
* @param Color|string|null $borderColor Border color. Defaults to black. Set to null for no color.
* @param Color|string|null $fillColor Fill color. Defaults to white. Set to null for no color.
*/
public function __construct(
$width,
$height,
array $pos,
$borderSize = 1,
$borderColor = '#000000',
$fillColor = '#FFFFFF'
) {
if (is_string($borderColor)) {
$borderColor = new Color($borderColor);
}
if (is_string($fillColor)) {
$fillColor = new Color($fillColor);
}
$this->width = $width;
$this->height = $height;
$this->pos = $pos;
$this->borderSize = $borderSize;
$this->borderColor = $borderColor;
$this->fillColor = $fillColor;
}
/**
* @return int
*/
public function getWidth()
{
return $this->width;
}
/**
* @return int
*/
public function getHeight()
{
return $this->height;
}
/**
* @return array
*/
public function getPos()
{
return $this->pos;
}
/**
* @return int
*/
public function getBorderSize()
{
return $this->borderSize;
}
/**
* @return Color
*/
public function getFillColor()
{
return $this->fillColor;
}
/**
* @return Color
*/
public function getBorderColor()
{
return $this->borderColor;
}
}
@@ -0,0 +1,86 @@
<?php
namespace Grafika\DrawingObject;
use Grafika\Color;
/**
* Base class
* @package Grafika
*/
abstract class Line
{
/**
* X,Y pos 1.
* @var array
*/
protected $point1;
/**
* X,Y pos 2.
* @var array
*/
protected $point2;
/**
* @var int Thickness of line.
*/
protected $thickness;
/**
* @var Color
*/
protected $color;
/**
* Creates a line.
*
* @param array $point1 Array containing int X and int Y position of the starting point.
* @param array $point2 Array containing int X and int Y position of the starting point.
* @param int $thickness Thickness in pixel. Note: This is currently ignored in GD editor and falls back to 1.
* @param Color|string $color Color of the line. Defaults to black.
*/
public function __construct(array $point1, array $point2, $thickness = 1, $color = '#000000')
{
if (is_string($color)) {
$color = new Color($color);
}
$this->point1 = $point1;
$this->point2 = $point2;
$this->thickness = $thickness;
$this->color = $color;
}
/**
* @return array
*/
public function getPoint1()
{
return $this->point1;
}
/**
* @return array
*/
public function getPoint2()
{
return $this->point2;
}
/**
* @return int
*/
public function getThickness()
{
return $this->thickness;
}
/**
* @return Color
*/
public function getColor()
{
return $this->color;
}
}
@@ -0,0 +1,116 @@
<?php
namespace Grafika\DrawingObject;
use Grafika\Color;
/**
* Base class
* @package Grafika
*/
abstract class Polygon
{
/**
* Image width in pixels
* @var int
*/
protected $width;
/**
* Image height in pixels
* @var int
*/
protected $height;
/**
* Array of all X and Y positions. Must have at least three positions (x,y).
* @var array
*/
protected $points;
/**
* @var int
*/
protected $borderSize;
/**
* @var Color
*/
protected $fillColor;
/**
* @var Color
*/
protected $borderColor;
/**
* Creates a polygon.
*
* @param array $points Array of all X and Y positions. Must have at least three positions.
* @param int $borderSize Size of the border in pixels. Defaults to 1 pixel. Set to 0 for no border.
* @param Color|string|bool $borderColor Border color. Defaults to black. Set to null for no color.
* @param Color|string|bool $fillColor Fill color. Defaults to white. Set to null for no color.
*/
public function __construct($points = array(array(0,0), array(0,0), array(0,0)), $borderSize = 1, $borderColor = '#000000', $fillColor = '#FFFFFF') {
if (is_string($borderColor)) {
$borderColor = new Color($borderColor);
}
if (is_string($fillColor)) {
$fillColor = new Color($fillColor);
}
$this->points = $points;
$this->borderSize = $borderSize;
$this->borderColor = $borderColor;
$this->fillColor = $fillColor;
}
/**
* @return int
*/
public function getWidth()
{
return $this->width;
}
/**
* @return int
*/
public function getHeight()
{
return $this->height;
}
/**
* @return array
*/
public function getPoints()
{
return $this->points;
}
/**
* @return int
*/
public function getBorderSize()
{
return $this->borderSize;
}
/**
* @return Color
*/
public function getFillColor()
{
return $this->fillColor;
}
/**
* @return Color
*/
public function getBorderColor()
{
return $this->borderColor;
}
}
@@ -0,0 +1,89 @@
<?php
namespace Grafika\DrawingObject;
use Grafika\Color;
/**
* Base class
* @package Grafika
*/
abstract class QuadraticBezier
{
/**
* Starting point.
* @var array
*/
protected $point1;
/**
* Control point.
* @var array
*/
protected $control;
/**
* End point.
* @var array
*/
protected $point2;
/**
* Color of curve.
*
* @var Color
*/
protected $color;
/**
* Creates a quadratic bezier. Quadratic bezier has 1 control point.
*
* @param array $point1 Array of X and Y value for start point.
* @param array $control Array of X and Y value for control point.
* @param array $point2 Array of X and Y value for end point.
* @param Color|string $color Color of the curve. Accepts hex string or a Color object. Defaults to black.
*/
public function __construct($point1, $control, $point2, $color = '#000000')
{
if (is_string($color)) {
$color = new Color($color);
}
$this->point1 = $point1;
$this->control = $control;
$this->point2 = $point2;
$this->color = $color;
}
/**
* @return array
*/
public function getPoint1()
{
return $this->point1;
}
/**
* @return array
*/
public function getControl()
{
return $this->control;
}
/**
* @return array
*/
public function getPoint2()
{
return $this->point2;
}
/**
* @return Color
*/
public function getColor()
{
return $this->color;
}
}
@@ -0,0 +1,126 @@
<?php
namespace Grafika\DrawingObject;
use Grafika\Color;
/**
* Base class
* @package Grafika
*/
abstract class Rectangle
{
/**
* Image width in pixels
* @var int
*/
protected $width;
/**
* Image height in pixels
* @var int
*/
protected $height;
/**
* X and Y position in an array.
* @var array
*/
protected $pos;
/**
* @var int
*/
protected $borderSize;
/**
* @var Color
*/
protected $fillColor;
/**
* @var Color
*/
protected $borderColor;
/**
* Creates a rectangle.
*
* @param int $width Width of rectangle in pixels.
* @param int $height Height in pixels.
* @param array $pos Array of X and Y position. X is the distance in pixels from the left of the canvass to the left of the rectangle. Y is the distance from the top of the canvass to the top of the rectangle. Defaults to array(0,0).
* @param int $borderSize Size of the border in pixels. Defaults to 1 pixel. Set to 0 for no border.
* @param Color|string|null $borderColor Border color. Defaults to black. Set to null for no color.
* @param Color|string|null $fillColor Fill color. Defaults to white. Set to null for no color.
*/
public function __construct(
$width,
$height,
$pos = array(0, 0),
$borderSize = 1,
$borderColor = '#000000',
$fillColor = '#FFFFFF'
) {
if (is_string($borderColor)) {
$borderColor = new Color($borderColor);
}
if (is_string($fillColor)) {
$fillColor = new Color($fillColor);
}
$this->width = $width;
$this->height = $height;
$this->pos = $pos;
$this->borderSize = $borderSize;
$this->borderColor = $borderColor;
$this->fillColor = $fillColor;
}
/**
* @return int
*/
public function getWidth()
{
return $this->width;
}
/**
* @return int
*/
public function getHeight()
{
return $this->height;
}
/**
* @return array
*/
public function getPos()
{
return $this->pos;
}
/**
* @return int
*/
public function getBorderSize()
{
return $this->borderSize;
}
/**
* @return Color
*/
public function getFillColor()
{
return $this->fillColor;
}
/**
* @return Color
*/
public function getBorderColor()
{
return $this->borderColor;
}
}
@@ -0,0 +1,17 @@
<?php
namespace Grafika;
/**
* Interface DrawingObjectInterface
* @package Grafika
*/
interface DrawingObjectInterface {
/**
* @param ImageInterface $image
*
* @return ImageInterface
*/
public function draw( $image );
}
@@ -0,0 +1,255 @@
<?php
namespace Grafika;
/**
* Interface EditorInterface
* @package Grafika
*/
interface EditorInterface {
/**
* Apply a filter to the image. See Filters section for a list of available filters.
*
* @param ImageInterface $image Instance of Image.
* @param FilterInterface $filter Instance implementing the FilterInterface.
*
* @return EditorInterface An instance of Editor.
*/
public function apply( &$image, $filter );
/**
* Blend two images together with the first image as the base and the second image on top. Supports several blend modes.
*
* @param ImageInterface $image1 The base image.
* @param ImageInterface $image2 The image placed on top of the base image.
* @param string $type The blend mode. Can be: normal, multiply, overlay or screen.
* @param float $opacity The opacity of $image2. Possible values 0.0 to 1.0 where 0.0 is fully transparent and 1.0 is fully opaque. Defaults to 1.0.
* @param string $position The position of $image2 on $image1. Possible values top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right and smart. Defaults to top-left.
* @param int $offsetX Number of pixels to add to the X position of $image2.
* @param int $offsetY Number of pixels to add to the Y position of $image2.
*
* @return EditorInterface An instance of Editor.
*/
public function blend(&$image1, $image2, $type='normal', $opacity = 1.0, $position = 'top-left', $offsetX = 0, $offsetY = 0 );
/**
* Compare two images and returns a hamming distance. A value of 0 indicates a likely similar picture. A value between 1 and 10 is potentially a variation. A value greater than 10 is likely a different image.
*
* @param string|ImageInterface $image1 Can be an instance of Image or string containing the file system path to image.
* @param string|ImageInterface $image2 Can be an instance of Image or string containing the file system path to image.
*
* @return int Hamming distance. Note: This breaks the chain if you are doing fluent api calls as it does not return an Editor.
* @throws \Exception
*/
public function compare( $image1, $image2 );
/**
* Crop the image to the given dimension and position.
*
* @param ImageInterface $image Instance of Image.
* @param int $cropWidth Crop width in pixels.
* @param int $cropHeight Crop Height in pixels.
* @param string $position The crop position. Possible values top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right and smart. Defaults to center.
* @param int $offsetX Number of pixels to add to the X position of the crop.
* @param int $offsetY Number of pixels to add to the Y position of the crop.
*
* @return EditorInterface An instance of Editor.
*/
public function crop( &$image, $cropWidth, $cropHeight, $position = 'center', $offsetX = 0, $offsetY = 0 );
/**
* Draw a DrawingObject on the image. See Drawing Objects section.
*
* @param ImageInterface $image Instance of Image.
* @param DrawingObjectInterface $drawingObject Instance of DrawingObject.
*
* @return EditorInterface An instance of Editor.
*/
public function draw( &$image, $drawingObject );
/**
* Compare if two images are equal. It will compare if the two images are of the same width and height. If the dimensions differ, it will return false. If the dimensions are equal, it will loop through each pixels. If one of the pixel don't match, it will return false. The pixels are compared using their RGB (Red, Green, Blue) values.
*
* @param string|ImageInterface $image1 Can be an instance of Image or string containing the file system path to image.
* @param string|ImageInterface $image2 Can be an instance of Image or string containing the file system path to image.
*
* @return bool True if equals false if not. Note: This breaks the chain if you are doing fluent api calls as it does not return an Editor.
* @throws \Exception
*/
public function equal( $image1, $image2 );
/**
* Fill entire image with color.
*
* @param ImageInterface $image Instance of Image.
* @param Color $color An instance of Grafika\Color class.
* @param int $x X-coordinate of start point.
* @param int $y Y-coordinate of start point.
*
* @return EditorInterface An instance of Editor.
*/
public function fill( &$image, $color, $x = 0, $y = 0 );
/**
* Flatten if animated GIF. Do nothing otherwise.
*
* @param ImageInterface $image Instance of Image.
*
* @return EditorInterface An instance of Editor.
*/
public function flatten( &$image );
/**
* Flip an image.
*
* @param ImageInterface $image Instance of Image.
* @param string $mode The type of flip: 'h' for horizontal flip or 'v' for vertical.
*
* @return EditorInterface An instance of Editor.
*/
public function flip( &$image, $mode);
/**
* Free the image clearing resources associated with it.
*
* @param ImageInterface $image Instance of Image.
*
* @return EditorInterface An instance of Editor.
*/
public function free( &$image );
/**
* Checks the PHP install if the editor is available.
*
* @return bool True if available false if not.
*/
public function isAvailable();
/**
* Change the image opacity.
*
* @param ImageInterface $image Instance of Image.
* @param float $opacity The opacity level where 1.0 is fully opaque and 0.0 is fully transparent.
*
* @return EditorInterface An instance of Editor.
*/
public function opacity( &$image, $opacity );
/**
* Open an image file and assign Image to first parameter. Grafika officially supports JPEG, PNG, GIF, and animated GIF. In theory, Grafika can open and edit other image formats as long as they are supported by GD and Imagick but it is currently untested.
*
* @param ImageInterface $image Instance of Image.
* @param string $imageFile File system path to image file.
*
* @return EditorInterface An instance of Editor.
*/
public function open( &$image, $imageFile );
/**
* Wrapper function for the resizeXXX family of functions. Resize an image to a given width, height and mode.
*
* @param ImageInterface $image Instance of Image.
* @param int $newWidth Width in pixels.
* @param int $newHeight Height in pixels.
* @param string $mode Resize mode. Possible values: "exact", "exactHeight", "exactWidth", "fill", "fit".
*
* @return EditorInterface An instance of Editor.
*/
public function resize( &$image, $newWidth, $newHeight, $mode='fit' );
/**
* Resize image to exact dimensions ignoring aspect ratio. Useful if you want to force exact width and height.
*
* @param ImageInterface $image Instance of Image.
* @param int $newWidth Width in pixels.
* @param int $newHeight Height in pixels.
*
* @return EditorInterface An instance of Editor.
*/
public function resizeExact( &$image, $newWidth, $newHeight );
/**
* Resize image to exact height. Width is auto calculated. Useful for creating row of images with the same height.
*
* @param ImageInterface $image Instance of Image.
* @param int $newHeight Height in pixels.
*
* @return EditorInterface An instance of Editor.
*/
public function resizeExactHeight( &$image, $newHeight );
/**
* Resize image to exact width. Height is auto calculated. Useful for creating column of images with the same width.
*
* @param ImageInterface $image Instance of Image.
* @param int $newWidth Width in pixels.
*
* @return EditorInterface An instance of Editor.
*/
public function resizeExactWidth( &$image, $newWidth );
/**
* Resize image to fill all the space in the given dimension. Excess parts are cropped.
*
* @param ImageInterface $image Instance of Image.
* @param int $newWidth Width in pixels.
* @param int $newHeight Height in pixels.
*
* @return EditorInterface An instance of Editor.
*/
public function resizeFill( &$image, $newWidth, $newHeight );
/**
* Resize an image to fit within the given width and height. The re-sized image will not exceed the given dimension. Useful if you want to preserve the aspect ratio.
*
* @param ImageInterface $image Instance of Image.
* @param int $newWidth Width in pixels.
* @param int $newHeight Width in pixels.
*
* @return EditorInterface An instance of Editor.
*/
public function resizeFit( &$image, $newWidth, $newHeight );
/**
* Rotate an image counter-clockwise.
*
* @param ImageInterface $image Instance of Image.
* @param int $angle The angle in degrees.
* @param Color|null $color The Color object containing the background color.
*
* @return EditorInterface An instance of Editor.
*/
public function rotate( &$image, $angle, $color = null );
/**
* Save the image to an image format.
*
* @param ImageInterface $image Instance of Image. Saving the image to a different format will have NO effect on the Image instance.
* @param string $file File path where to save the image.
* @param null|string $type The image format to use. Can be null, "gif", "png", or "jpeg". If null, an appropriate format will be chosen based on the output file name in $file.
* @param null|string $quality Quality of image. Applies to JPEG only. Accepts number 0 - 100 where 0 is lowest and 100 is the highest quality. Or null for default. Default quality if null is 75.
* @param bool $interlace Set to true for progressive JPEG. Applies to JPEG only. Default false.
* @param int $permission Default permission when creating non-existing target directory. Default is 0755. Note: Its using PHP's octal notation so you must prepend numbers with zero (0).
*
* @return EditorInterface An instance of Editor.
*/
public function save( $image, $file, $type = null, $quality = null, $interlace = false, $permission = 0755 );
/**
* Write text to image.
*
* @param ImageInterface $image Instance of Image.
* @param string $text The text to be written.
* @param int $size The font size. Defaults to 12.
* @param int $x The distance from the left edge of the image to the left of the text. Defaults to 0.
* @param int $y The distance from the top edge of the image to the baseline of the text. Defaults to 12 (equal to font size) so that the text is placed within the image.
* @param Color $color The Color object. Default text color is black.
* @param string $font Full path to font file. If blank, will default to Liberation Sans font.
* @param int $angle Angle of text from 0 - 359. Defaults to 0.
*
* @return EditorInterface An instance of Editor.
* @throws \Exception
*/
public function text( &$image, $text, $size = 12, $x = 0, $y = 12, $color = null, $font = '', $angle = 0 );
}
@@ -0,0 +1,17 @@
<?php
namespace Grafika;
/**
* Interface FilterInterface
* @package Grafika
*/
interface FilterInterface {
/**
* @param ImageInterface $image
*
* @return ImageInterface
*/
public function apply( $image );
}
@@ -0,0 +1,412 @@
<?php
namespace Grafika\Gd\DrawingObject;
use Grafika\DrawingObject\CubicBezier as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Gd\Image;
use Grafika\ImageInterface;
/**
* Class CubicBezier
* @package Grafika
*/
class CubicBezier extends Base implements DrawingObjectInterface
{
/**
* @param ImageInterface $image
* @return Image
*/
public function draw($image)
{
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$gd = $image->getCore();
list($x0, $y0) = $this->point1;
list($x1, $y1) = $this->control1;
list($x2, $y2) = $this->control2;
list($x3, $y3) = $this->point2;
$this->plot($gd, $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3);
$type = $image->getType();
$file = $image->getImageFile();
return new Image($gd, $file, $width, $height, $type); // Create new image with updated core
}
protected function plot($gd, $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
{ /* plot any cubic Bezier curve */
$n = 0;
$i = 0;
$xc = $x0 + $x1 - $x2 - $x3;
$xa = $xc - 4 * ($x1 - $x2);
$xb = $x0 - $x1 - $x2 + $x3;
$xd = $xb + 4 * ($x1 + $x2);
$yc = $y0 + $y1 - $y2 - $y3;
$ya = $yc - 4 * ($y1 - $y2);
$yb = $y0 - $y1 - $y2 + $y3;
$yd = $yb + 4 * ($y1 + $y2);
$fx0 = $x0;
$fx1 = 0;
$fx2 = 0;
$fx3 = 0;
$fy0 = $y0;
$fy1 = 0;
$fy2 = 0;
$fy3 = 0;
$t1 = $xb * $xb - $xa * $xc;
$t2 = 0;
$t = array();
/* sub-divide curve at gradient sign changes */
if ($xa == 0) { /* horizontal */
if (abs($xc) < 2 * abs($xb)) {
$t[$n++] = $xc / (2.0 * $xb);
} /* one change */
} else {
if ($t1 > 0.0) { /* two changes */
$t2 = sqrt($t1);
$t1 = ($xb - $t2) / $xa;
if (abs($t1) < 1.0) {
$t[$n++] = $t1;
}
$t1 = ($xb + $t2) / $xa;
if (abs($t1) < 1.0) {
$t[$n++] = $t1;
}
}
}
$t1 = $yb * $yb - $ya * $yc;
if ($ya == 0) { /* vertical */
if (abs($yc) < 2 * abs($yb)) {
$t[$n++] = $yc / (2.0 * $yb);
} /* one change */
} else {
if ($t1 > 0.0) { /* two changes */
$t2 = sqrt($t1);
$t1 = ($yb - $t2) / $ya;
if (abs($t1) < 1.0) {
$t[$n++] = $t1;
}
$t1 = ($yb + $t2) / $ya;
if (abs($t1) < 1.0) {
$t[$n++] = $t1;
}
}
}
for ($i = 1; $i < $n; $i++) /* bubble sort of 4 points */ {
if (($t1 = $t[$i - 1]) > $t[$i]) {
$t[$i - 1] = $t[$i];
$t[$i] = $t1;
$i = 0;
}
}
$t1 = -1.0;
$t[$n] = 1.0; /* begin / end point */
for ($i = 0; $i <= $n; $i++) { /* plot each segment separately */
$t2 = $t[$i]; /* sub-divide at $t[$i-1], $t[$i] */
$fx1 = ($t1 * ($t1 * $xb - 2 * $xc) - $t2 * ($t1 * ($t1 * $xa - 2 * $xb) + $xc) + $xd) / 8 - $fx0;
$fy1 = ($t1 * ($t1 * $yb - 2 * $yc) - $t2 * ($t1 * ($t1 * $ya - 2 * $yb) + $yc) + $yd) / 8 - $fy0;
$fx2 = ($t2 * ($t2 * $xb - 2 * $xc) - $t1 * ($t2 * ($t2 * $xa - 2 * $xb) + $xc) + $xd) / 8 - $fx0;
$fy2 = ($t2 * ($t2 * $yb - 2 * $yc) - $t1 * ($t2 * ($t2 * $ya - 2 * $yb) + $yc) + $yd) / 8 - $fy0;
$fx0 -= $fx3 = ($t2 * ($t2 * (3 * $xb - $t2 * $xa) - 3 * $xc) + $xd) / 8;
$fy0 -= $fy3 = ($t2 * ($t2 * (3 * $yb - $t2 * $ya) - 3 * $yc) + $yd) / 8;
$x3 = floor($fx3 + 0.5);
$y3 = floor($fy3 + 0.5); /* scale bounds to int */
if ($fx0 != 0.0) {
$fx1 *= $fx0 = ($x0 - $x3) / $fx0;
$fx2 *= $fx0;
}
if ($fy0 != 0.0) {
$fy1 *= $fy0 = ($y0 - $y3) / $fy0;
$fy2 *= $fy0;
}
if ($x0 != $x3 || $y0 != $y3) /* segment $t1 - $t2 */ {
$this->plotCubicSegment($gd, $x0, $y0, $x0 + $fx1, $y0 + $fy1, $x0 + $fx2, $y0 + $fy2, $x3, $y3);
}
$x0 = $x3;
$y0 = $y3;
$fx0 = $fx3;
$fy0 = $fy3;
$t1 = $t2;
}
}
protected function plotCubicSegment($gd, $x0, $y0, $x1, $y1, $x2, $y2, $x3, $y3)
{ /* plot limited anti-aliased cubic Bezier segment */
$f = 0;
$fx = 0;
$fy = 0;
$leg = 1;
$sx = $x0 < $x3 ? 1 : -1;
$sy = $y0 < $y3 ? 1 : -1; /* step direction */
$xc = -abs($x0 + $x1 - $x2 - $x3);
$xa = $xc - 4 * $sx * ($x1 - $x2);
$xb = $sx * ($x0 - $x1 - $x2 + $x3);
$yc = -abs($y0 + $y1 - $y2 - $y3);
$ya = $yc - 4 * $sy * ($y1 - $y2);
$yb = $sy * ($y0 - $y1 - $y2 + $y3);
$ab = 0;
$ac = 0;
$bc = 0;
$ba = 0;
$xx = 0;
$xy = 0;
$yy = 0;
$dx = 0;
$dy = 0;
$ex = 0;
$px = 0;
$py = 0;
$ed = 0;
$ip = 0;
$EP = 0.01;
/* check for curve restrains */
/* slope P0-P1 == P2-P3 and (P0-P3 == P1-P2 or no slope change) */
assert(($x1 - $x0) * ($x2 - $x3) < $EP && (($x3 - $x0) * ($x1 - $x2) < $EP || $xb * $xb < $xa * $xc + $EP));
assert(($y1 - $y0) * ($y2 - $y3) < $EP && (($y3 - $y0) * ($y1 - $y2) < $EP || $yb * $yb < $ya * $yc + $EP));
if ($xa == 0 && $ya == 0) { /* quadratic Bezier */
$sx = floor((3 * $x1 - $x0 + 1) / 2);
$sy = floor((3 * $y1 - $y0 + 1) / 2); /* new midpoint */
$this->plotQuadSegment($gd, $x0, $y0, $sx, $sy, $x3, $y3);
return;
}
$x1 = ($x1 - $x0) * ($x1 - $x0) + ($y1 - $y0) * ($y1 - $y0) + 1; /* line lengths */
$x2 = ($x2 - $x3) * ($x2 - $x3) + ($y2 - $y3) * ($y2 - $y3) + 1;
do { /* loop over both ends */
$ab = $xa * $yb - $xb * $ya;
$ac = $xa * $yc - $xc * $ya;
$bc = $xb * $yc - $xc * $yb;
$ip = 4 * $ab * $bc - $ac * $ac; /* self intersection loop at all? */
$ex = $ab * ($ab + $ac - 3 * $bc) + $ac * $ac; /* P0 part of self-intersection loop? */
$f = $ex > 0 ? 1 : sqrt(1 + 1024 / $x1); /* calculate resolution */
$ab *= $f;
$ac *= $f;
$bc *= $f;
$ex *= $f * $f; /* increase resolution */
$xy = 9 * ($ab + $ac + $bc) / 8;
$ba = 8 * ($xa - $ya);/* init differences of 1st degree */
$dx = 27 * (8 * $ab * ($yb * $yb - $ya * $yc) + $ex * ($ya + 2 * $yb + $yc)) / 64 - $ya * $ya * ($xy - $ya);
$dy = 27 * (8 * $ab * ($xb * $xb - $xa * $xc) - $ex * ($xa + 2 * $xb + $xc)) / 64 - $xa * $xa * ($xy + $xa);
/* init differences of 2nd degree */
$xx = 3 * (3 * $ab * (3 * $yb * $yb - $ya * $ya - 2 * $ya * $yc) - $ya * (3 * $ac * ($ya + $yb) + $ya * $ba)) / 4;
$yy = 3 * (3 * $ab * (3 * $xb * $xb - $xa * $xa - 2 * $xa * $xc) - $xa * (3 * $ac * ($xa + $xb) + $xa * $ba)) / 4;
$xy = $xa * $ya * (6 * $ab + 6 * $ac - 3 * $bc + $ba);
$ac = $ya * $ya;
$ba = $xa * $xa;
$xy = 3 * ($xy + 9 * $f * ($ba * $yb * $yc - $xb * $xc * $ac) - 18 * $xb * $yb * $ab) / 8;
if ($ex < 0) { /* negate values if inside self-intersection loop */
$dx = -$dx;
$dy = -$dy;
$xx = -$xx;
$yy = -$yy;
$xy = -$xy;
$ac = -$ac;
$ba = -$ba;
} /* init differences of 3rd degree */
$ab = 6 * $ya * $ac;
$ac = -6 * $xa * $ac;
$bc = 6 * $ya * $ba;
$ba = -6 * $xa * $ba;
$dx += $xy;
$ex = $dx + $dy;
$dy += $xy; /* error of 1st step */
for ($fx = $fy = $f; $x0 != $x3 && $y0 != $y3;) {
$y1 = min($xy - $dx, $dy - $xy);
$ed = max($xy - $dx, $dy - $xy); /* approximate error distance */
$ed = $f * ($ed + 2 * $ed * $y1 * $y1 / (4 * $ed * $ed + $y1 * $y1));
$y1 = 255 * abs($ex - ($f - $fx + 1) * $dx - ($f - $fy + 1) * $dy + $f * $xy) / $ed;
if ($y1 < 256) {
$this->setPixel($gd, $x0, $y0, $y1 / 255);
} /* plot curve */
$px = abs($ex - ($f - $fx + 1) * $dx + ($fy - 1) * $dy); /* pixel intensity x move */
$py = abs($ex + ($fx - 1) * $dx - ($f - $fy + 1) * $dy); /* pixel intensity y move */
$y2 = $y0;
do { /* move sub-steps of one pixel */
if ($ip >= -$EP) /* intersection possible? -> check.. */ {
if ($dx + $xx > $xy || $dy + $yy < $xy) {
goto exits;
}
} /* two x or y steps */
$y1 = 2 * $ex + $dx; /* save value for test of y step */
if (2 * $ex + $dy > 0) { /* x sub-step */
$fx--;
$ex += $dx += $xx;
$dy += $xy += $ac;
$yy += $bc;
$xx += $ab;
} else {
if ($y1 > 0) {
goto exits;
}
} /* tiny nearly cusp */
if ($y1 <= 0) { /* y sub-step */
$fy--;
$ex += $dy += $yy;
$dx += $xy += $bc;
$xx += $ac;
$yy += $ba;
}
} while ($fx > 0 && $fy > 0); /* pixel complete? */
if (2 * $fy <= $f) { /* x+ anti-aliasing pixel */
if ($py < $ed) {
$this->setPixel($gd, $x0 + $sx, $y0, $py / $ed);
} /* plot curve */
$y0 += $sy;
$fy += $f; /* y step */
}
if (2 * $fx <= $f) { /* y+ anti-aliasing pixel */
if ($px < $ed) {
$this->setPixel($gd, $x0, $y2 + $sy, $px / $ed);
} /* plot curve */
$x0 += $sx;
$fx += $f; /* x step */
}
}
break; /* finish curve by line */
exits:
if (2 * $ex < $dy && 2 * $fy <= $f + 2) { /* round x+ approximation pixel */
if ($py < $ed) {
$this->setPixel($gd, $x0 + $sx, $y0, $py / $ed);
} /* plot curve */
$y0 += $sy;
}
if (2 * $ex > $dx && 2 * $fx <= $f + 2) { /* round y+ approximation pixel */
if ($px < $ed) {
$this->setPixel($gd, $x0, $y2 + $sy, $px / $ed);
} /* plot curve */
$x0 += $sx;
}
$xx = $x0;
$x0 = $x3;
$x3 = $xx;
$sx = -$sx;
$xb = -$xb; /* swap legs */
$yy = $y0;
$y0 = $y3;
$y3 = $yy;
$sy = -$sy;
$yb = -$yb;
$x1 = $x2;
} while ($leg--); /* try other end */
$this->plotLine($gd, $x0, $y0, $x3, $y3); /* remaining part in case of cusp or crunode */
}
protected function plotQuadSegment($gd, $x0, $y0, $x1, $y1, $x2, $y2)
{ /* draw an limited anti-aliased quadratic Bezier segment */
$sx = $x2 - $x1;
$sy = $y2 - $y1;
$xx = $x0 - $x1;
$yy = $y0 - $y1;
$xy = $dx = $dy = $err = $ed = 0;
$cur = $xx * $sy - $yy * $sx; /* $curvature */
if ($sx * (int)$sx + $sy * (int)$sy > $xx * $xx + $yy * $yy) { /* begin with longer part */
$x2 = $x0;
$x0 = $sx + $x1;
$y2 = $y0;
$y0 = $sy + $y1;
$cur = -$cur; /* swap P0 P2 */
}
if ($cur != 0) { /* no straight line */
$xx += $sx;
$xx *= $sx = $x0 < $x2 ? 1 : -1; /* x step direction */
$yy += $sy;
$yy *= $sy = $y0 < $y2 ? 1 : -1; /* y step direction */
$xy = 2 * $xx * $yy;
$xx *= $xx;
$yy *= $yy; /* differences 2nd degree */
if ($cur * $sx * $sy < 0) { /* negat$ed $curvature? */
$xx = -$xx;
$yy = -$yy;
$xy = -$xy;
$cur = -$cur;
}
$dx = 4.0 * $sy * ($x1 - $x0) * $cur + $xx - $xy; /* differences 1st degree */
$dy = 4.0 * $sx * ($y0 - $y1) * $cur + $yy - $xy;
$xx += $xx;
$yy += $yy;
$err = $dx + $dy + $xy; /* $error 1st step */
do {
$cur = min($dx + $xy, -$xy - $dy);
$ed = max($dx + $xy, -$xy - $dy); /* approximate $error distance */
$ed += 2 * $ed * $cur * $cur / (4 * $ed * $ed + $cur * $cur);
$this->setPixel($gd, $x0, $y0, abs($err - $dx - $dy - $xy) / $ed); /* plot $curve */
if ($x0 == $x2 || $y0 == $y2) {
break;
} /* $curve finish$ed */
$x1 = $x0;
$cur = $dx - $err;
$y1 = 2 * $err + $dy < 0;
if (2 * $err + $dx > 0) { /* x step */
if ($err - $dy < $ed) {
$this->setPixel($gd, $x0, $y0 + $sy, abs($err - $dy) / $ed);
}
$x0 += $sx;
$dx -= $xy;
$err += $dy += $yy;
}
if ($y1) { /* y step */
if ($cur < $ed) {
$this->setPixel($gd, $x1 + $sx, $y0, abs($cur) / $ed);
}
$y0 += $sy;
$dy -= $xy;
$err += $dx += $xx;
}
} while ($dy < $dx); /* gradient negates -> close curves */
}
$this->plotLine($gd, $x0, $y0, $x2, $y2); /* plot remaining needle to end */
}
protected function plotLine($gd, $x0, $y0, $x1, $y1)
{ /* draw a black (0) anti-aliased line on white (255) background */
$dx = abs($x1 - $x0);
$sx = $x0 < $x1 ? 1 : -1;
$dy = -abs($y1 - $y0);
$sy = $y0 < $y1 ? 1 : -1;
$err = $dx + $dy;
$e2 = $x2 = 0; /* $error value e_xy */
$ed = $dx - $dy == 0 ? 1 : sqrt((float)$dx * $dx + (float)$dy * $dy);
for (; ;) { /* pixel loop */
$this->setPixel($gd, $x0, $y0, abs($err - $dx - $dy) / $ed);
$e2 = $err;
$x2 = $x0;
if (2 * $e2 + $dx >= 0) { /* x step */
if ($x0 == $x1) {
break;
}
if ($e2 - $dy < $ed) {
$this->setPixel($gd, $x0, $y0 + $sy, ($e2 - $dy) / $ed);
}
$err += $dy;
$x0 += $sx;
}
if (2 * $e2 + $dy <= 0) { /* y step */
if ($y0 == $y1) {
break;
}
if ($dx - $e2 < $ed) {
$this->setPixel($gd, $x2 + $sx, $y0, ($dx - $e2) / $ed);
}
$err += $dx;
$y0 += $sy;
}
}
}
/**
* @param resource $gd
* @param int $x
* @param int $y
* @param float $ar Alpha ratio
*/
protected function setPixel($gd, $x, $y, $ar)
{
list($r, $g, $b) = $this->color->getRgb();
$c = imagecolorallocatealpha($gd, $r, $g, $b, 127 * $ar);
imagesetpixel($gd, $x, $y, $c);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Grafika\Gd\DrawingObject;
use Grafika\DrawingObject\Ellipse as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Gd\Editor;
use Grafika\ImageInterface;
/**
* Class Ellipse
* @package Grafika
*/
class Ellipse extends Base implements DrawingObjectInterface
{
/**
* TODO: Anti-aliased curves
* @param ImageInterface $image
* @return ImageInterface
*/
public function draw($image)
{
list($x, $y) = $this->pos;
$left = $x + $this->width / 2;
$top = $y + $this->height / 2;
if( null !== $this->fillColor ){
list($r, $g, $b, $alpha) = $this->fillColor->getRgba();
$fillColorResource = imagecolorallocatealpha($image->getCore(), $r, $g, $b, Editor::gdAlpha($alpha));
imagefilledellipse($image->getCore(), $left, $top, $this->width, $this->height, $fillColorResource);
}
// Create borders. It will be placed on top of the filled ellipse (if present)
if ( 0 < $this->getBorderSize() and null !== $this->borderColor) { // With border > 0 AND borderColor !== null
list($r, $g, $b, $alpha) = $this->borderColor->getRgba();
$borderColorResource = imagecolorallocatealpha($image->getCore(), $r, $g, $b, Editor::gdAlpha($alpha));
imageellipse($image->getCore(), $left, $top, $this->width, $this->height, $borderColorResource);
}
return $image;
}
}
@@ -0,0 +1,36 @@
<?php
namespace Grafika\Gd\DrawingObject;
use Grafika\DrawingObject\Line as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Gd\Image;
/**
* Class Line
* @package Grafika
*/
class Line extends Base implements DrawingObjectInterface
{
/**
* @param Image $image
*
* @return Image
*/
public function draw($image)
{
list( $x1, $y1 ) = $this->point1;
list( $x2, $y2 ) = $this->point2;
list( $r, $g, $b ) = $this->color->getRgb();
$color = imagecolorallocate( $image->getCore(), $r, $g, $b );
if ( function_exists( 'imageantialias' ) ) { // Not available on some if PHP is not precompiled with it even if GD is enabled
imageantialias( $image->getCore(), true );
}
imageline( $image->getCore(), $x1, $y1, $x2, $y2, $color );
return $image;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Grafika\Gd\DrawingObject;
use Grafika\DrawingObject\Polygon as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Gd\Editor;
/**
* Class Rectangle
* @package Grafika
*/
class Polygon extends Base implements DrawingObjectInterface
{
public function draw($image)
{
if(function_exists('imageantialias')){
imageantialias($image->getCore(), true);
}
$points = $this->points();
$count = count($this->points);
// Create filled polygon
if( null !== $this->fillColor){
list($r, $g, $b, $alpha) = $this->getFillColor()->getRgba();
$fillColorResource = imagecolorallocatealpha(
$image->getCore(), $r, $g, $b,
Editor::gdAlpha($alpha)
);
imagefilledpolygon($image->getCore(), $points,
$count,
$fillColorResource
);
}
// Create polygon borders. It will be placed on top of the filled polygon (if present)
if ( 0 < $this->getBorderSize() and null !== $this->borderColor) { // With border > 0 AND borderColor !== null
list($r, $g, $b, $alpha) = $this->getBorderColor()->getRgba();
$borderColorResource = imagecolorallocatealpha(
$image->getCore(), $r, $g, $b,
Editor::gdAlpha($alpha)
);
imagepolygon($image->getCore(), $points,
$count,
$borderColorResource
);
}
return $image;
}
protected function points(){
$points = array();
foreach($this->points as $point){
$points[] = $point[0];
$points[] = $point[1];
}
if( count($points) < 6 ){
throw new \Exception('Polygon needs at least 3 points.');
}
return $points;
}
}
@@ -0,0 +1,208 @@
<?php
namespace Grafika\Gd\DrawingObject;
use Grafika\DrawingObject\QuadraticBezier as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Gd\Image;
use Grafika\ImageInterface;
/**
* Class QuadraticBezier
* @package Grafika
*/
class QuadraticBezier extends Base implements DrawingObjectInterface
{
/**
* @link http://members.chello.at/easyfilter/bresenham.pdf
* @param ImageInterface $image
* @return Image
*/
public function draw($image)
{
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$gd = $image->getCore();
list($x0, $y0) = $this->point1;
list($x1, $y1) = $this->control;
list($x2, $y2) = $this->point2;
$this->plot($gd, $x0, $y0, $x1, $y1, $x2, $y2);
$type = $image->getType();
$file = $image->getImageFile();
return new Image($gd, $file, $width, $height, $type); // Create new image with updated core
}
protected function plot($gd, $x0, $y0, $x1, $y1, $x2, $y2)
{
/* plot any quadratic Bezier curve */
$x = $x0 - $x1;
$y = $y0 - $y1;
$t = $x0 - 2 * $x1 + $x2; //double
if ((int)$x * ($x2 - $x1) > 0) { /* horizontal cut at P4? */
if ((int)$y * ($y2 - $y1) > 0) /* vertical cut at P6 too? */ {
if (abs(($y0 - 2 * $y1 + $y2) / $t * $x) > abs($y)) { /* which first? */
$x0 = $x2;
$x2 = $x + $x1;
$y0 = $y2;
$y2 = $y + $y1; /* swap points */
}
} /* now horizontal cut at P4 comes first */
$t = ($x0 - $x1) / $t;
$r = (1 - $t) * ((1 - $t) * $y0 + 2.0 * $t * $y1) + $t * $t * $y2; /* By(t=P4) */
$t = ($x0 * $x2 - $x1 * $x1) * $t / ($x0 - $x1); /* gradient dP4/dx=0 */
$x = floor($t + 0.5);
$y = floor($r + 0.5);
$r = ($y1 - $y0) * ($t - $x0) / ($x1 - $x0) + $y0; /* intersect P3 | P0 P1 */
$this->plotSegment($gd, $x0, $y0, $x, floor($r + 0.5), $x, $y);
$r = ($y1 - $y2) * ($t - $x2) / ($x1 - $x2) + $y2; /* intersect P4 | P1 P2 */
$x0 = $x1 = $x;
$y0 = $y;
$y1 = floor($r + 0.5); /* P0 = P4, P1 = P8 */
}
if ((int)($y0 - $y1) * ($y2 - $y1) > 0) { /* vertical cut at P6? */
$t = $y0 - 2 * $y1 + $y2;
$t = ($y0 - $y1) / $t;
$r = (1 - $t) * ((1 - $t) * $x0 + 2.0 * $t * $x1) + $t * $t * $x2; /* Bx(t=P6) */
$t = ($y0 * $y2 - $y1 * $y1) * $t / ($y0 - $y1); /* gradient dP6/dy=0 */
$x = floor($r + 0.5);
$y = floor($t + 0.5);
$r = ($x1 - $x0) * ($t - $y0) / ($y1 - $y0) + $x0; /* intersect P6 | P0 P1 */
$this->plotSegment($gd, $x0, $y0, floor($r + 0.5), $y, $x, $y);
$r = ($x1 - $x2) * ($t - $y2) / ($y1 - $y2) + $x2; /* intersect P7 | P1 P2 */
$x0 = $x;
$x1 = floor($r + 0.5);
$y0 = $y1 = $y; /* P0 = P6, P1 = P7 */
}
$this->plotSegment($gd, $x0, $y0, $x1, $y1, $x2, $y2); /* remaining part */
}
/**
* Draw an limited anti-aliased quadratic Bezier segment.
* @param $gd
* @param $x0
* @param $y0
* @param $x1
* @param $y1
* @param $x2
* @param $y2
*/
protected function plotSegment($gd, $x0, $y0, $x1, $y1, $x2, $y2)
{
$sx = $x2 - $x1;
$sy = $y2 - $y1;
$xx = $x0 - $x1;
$yy = $y0 - $y1;
$cur = $xx * $sy - $yy * $sx; /* $curvature */
assert($xx * $sx <= 0 && $yy * $sy <= 0);
if ($sx * (int)$sx + $sy * (int)$sy > $xx * $xx + $yy * $yy) { /* begin with longer part */
$x2 = $x0;
$x0 = $sx + $x1;
$y2 = $y0;
$y0 = $sy + $y1;
$cur = -$cur; /* swap P0 P2 */
}
if ($cur != 0) { /* no straight line */
$xx += $sx;
$xx *= $sx = $x0 < $x2 ? 1 : -1; /* x step direction */
$yy += $sy;
$yy *= $sy = $y0 < $y2 ? 1 : -1; /* y step direction */
$xy = 2 * $xx * $yy;
$xx *= $xx;
$yy *= $yy; /* differences 2nd degree */
if ($cur * $sx * $sy < 0) { /* negat$ed $curvature? */
$xx = -$xx;
$yy = -$yy;
$xy = -$xy;
$cur = -$cur;
}
$dx = 4.0 * $sy * ($x1 - $x0) * $cur + $xx - $xy; /* differences 1st degree */
$dy = 4.0 * $sx * ($y0 - $y1) * $cur + $yy - $xy;
$xx += $xx;
$yy += $yy;
$err = $dx + $dy + $xy; /* $error 1st step */
do {
$cur = min($dx + $xy, -$xy - $dy);
$ed = max($dx + $xy, -$xy - $dy); /* approximate $error distance */
$ed += 2 * $ed * $cur * $cur / (4 * $ed * $ed + $cur * $cur);
$this->setPixel($gd, $x0, $y0, abs($err - $dx - $dy - $xy) / $ed); /* plot $curve */
if ($x0 == $x2 || $y0 == $y2) {
break;
} /* $curve finish$ed */
$x1 = $x0;
$cur = $dx - $err;
$y1 = 2 * $err + $dy < 0;
if (2 * $err + $dx > 0) { /* x step */
if ($err - $dy < $ed) {
$this->setPixel($gd, $x0, $y0 + $sy, abs($err - $dy) / $ed);
}
$x0 += $sx;
$dx -= $xy;
$err += $dy += $yy;
}
if ($y1) { /* y step */
if ($cur < $ed) {
$this->setPixel($gd, $x1 + $sx, $y0, abs($cur) / $ed);
}
$y0 += $sy;
$dy -= $xy;
$err += $dx += $xx;
}
} while ($dy < $dx); /* gradient negates -> close curves */
}
$this->plotLine($gd, $x0, $y0, $x2, $y2); /* plot remaining needle to end */
}
protected function plotLine($gd, $x0, $y0, $x1, $y1)
{
$dx = abs($x1 - $x0);
$sx = $x0 < $x1 ? 1 : -1;
$dy = -abs($y1 - $y0);
$sy = $y0 < $y1 ? 1 : -1;
$err = $dx + $dy;
$ed = $dx - $dy == 0 ? 1 : sqrt((float)$dx * $dx + (float)$dy * $dy);
for (; ;) { /* pixel loop */
$this->setPixel($gd, $x0, $y0, abs($err - $dx - $dy) / $ed);
$e2 = $err;
$x2 = $x0;
if (2 * $e2 + $dx >= 0) { /* x step */
if ($x0 == $x1) {
break;
}
if ($e2 - $dy < $ed) {
$this->setPixel($gd, $x0, $y0 + $sy, ($e2 - $dy) / $ed);
}
$err += $dy;
$x0 += $sx;
}
if (2 * $e2 + $dy <= 0) { /* y step */
if ($y0 == $y1) {
break;
}
if ($dx - $e2 < $ed) {
$this->setPixel($gd, $x2 + $sx, $y0, ($dx - $e2) / $ed);
}
$err += $dx;
$y0 += $sy;
}
}
}
/**
* @param resource $gd
* @param int $x
* @param int $y
* @param float $ar Alpha ratio
*/
protected function setPixel($gd, $x, $y, $ar)
{
list($r, $g, $b) = $this->color->getRgb();
$c = imagecolorallocatealpha($gd, $r, $g, $b, 127 * $ar);
imagesetpixel($gd, $x, $y, $c);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Grafika\Gd\DrawingObject;
use Grafika\DrawingObject\Rectangle as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Gd\Editor;
/**
* Class Rectangle
* @package Grafika
*/
class Rectangle extends Base implements DrawingObjectInterface
{
public function draw($image)
{
$x1 = $this->pos[0];
$x2 = $x1 + $this->getWidth();
$y1 = $this->pos[1];
$y2 = $y1 + $this->getHeight();
if( null !== $this->fillColor ){
list($r, $g, $b, $alpha) = $this->fillColor->getRgba();
$fillColorResource = imagecolorallocatealpha($image->getCore(), $r, $g, $b, Editor::gdAlpha($alpha));
imagefilledrectangle($image->getCore(), $x1, $y1, $x2, $y2, $fillColorResource);
}
// Create borders. It will be placed on top of the filled rectangle (if present)
if ( 0 < $this->getBorderSize() and null !== $this->borderColor) { // With border > 0 AND borderColor !== null
list($r, $g, $b, $alpha) = $this->borderColor->getRgba();
$borderColorResource = imagecolorallocatealpha($image->getCore(), $r, $g, $b, Editor::gdAlpha($alpha));
imagerectangle($image->getCore(), $x1, $y1, $x2, $y2, $borderColorResource);
}
return $image;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Blurs the image.
*/
class Blur implements FilterInterface
{
/**
* @var int
*/
protected $amount;
/**
* Blur constructor.
* @param int $amount The amount of blur to apply. Possible values 1-100.
*/
public function __construct($amount = 1)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply($image)
{
for ($i=0; $i < $this->amount; $i++) {
imagefilter($image->getCore(), IMG_FILTER_GAUSSIAN_BLUR);
}
return $image;
}
}
@@ -0,0 +1,39 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Change the image brightness.
*
* TODO: param checks
*/
class Brightness implements FilterInterface{
/**
* @var int
*/
protected $amount; // -100 >= 0 >= 100
/**
* Brightness constructor.
* @param int $amount The amount of brightness to apply. >= -100 and <= -1 to darken. 0 for no change. >= 1 and <= 100 to brighten.
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
imagefilter($image->getCore(), IMG_FILTER_BRIGHTNESS, ($this->amount * 2.55));
return $image;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Change the values for red, green and blue in an image.
*/
class Colorize implements FilterInterface{
/**
* @var int
*/
protected $red; // -100 >= 0 >= 100
/**
* @var int
*/
protected $green; // -100 >= 0 >= 100
/**
* @var int
*/
protected $blue; // -100 >= 0 >= 100
/**
* Colorize constructor.
* @param int $red The amount of red colors. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to add.
* @param int $green The amount of green colors. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to add.
* @param int $blue The amount of blue colors. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to add.
*/
public function __construct($red, $green, $blue)
{
$this->red = round($red * 2.55);
$this->green = round($green * 2.55);
$this->blue = round($blue * 2.55);
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
imagefilter($image->getCore(), IMG_FILTER_COLORIZE, $this->red, $this->green, $this->blue);
return $image;
}
}
@@ -0,0 +1,38 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Change the contrast of an image. Contrast is the difference in luminance or colour that makes an object distinguishable.
*/
class Contrast implements FilterInterface{
/**
* @var int
*/
protected $amount; // -100 >= 0 >= 100
/**
* Contrast constructor.
* @param int $amount The amount of contrast to apply. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to increase.
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
imagefilter($image->getCore(), IMG_FILTER_CONTRAST, ($this->amount * -1));
return $image;
}
}
@@ -0,0 +1,190 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Dither image. Dithering will reduce the color to black and white and add noise.
*/
class Dither implements FilterInterface{
/**
* @var string Dithering algorithm to use.
*/
private $type;
/**
* Dither an image.
*
* @param string $type Dithering algorithm to use. Options: diffusion, ordered. Defaults to diffusion.
*/
public function __construct( $type = 'diffusion' )
{
$this->type = $type;
}
/**
* Apply filter.
*
* @param Image $image
*
* @return Image
* @throws \Exception
*/
public function apply( $image ) {
if ( $this->type === 'ordered' ) {
return $this->ordered( $image );
} else if ( $this->type === 'diffusion' ) {
return $this->diffusion( $image );
}
throw new \Exception( sprintf( 'Invalid dither type "%s".', $this->type ) );
}
/**
* Dither using error diffusion.
*
* @param Image $image
*
* @return Image
*/
private function diffusion( $image ){
$pixel = array();
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$old = $image->getCore();
$new = imagecreatetruecolor($width, $height);
for ( $y = 0; $y < $height; $y+=1 ) {
for ( $x = 0; $x < $width; $x+=1 ) {
$color = imagecolorat( $old, $x, $y );
$r = ($color >> 16) & 0xFF;
$g = ($color >> 8) & 0xFF;
$b = $color & 0xFF;
$gray = round($r * 0.3 + $g * 0.59 + $b * 0.11);
if(isset($pixel[$x][$y])){ // Add errors to color if there are
$gray += $pixel[$x][$y];
}
if ( $gray <= 127 ) { // Determine if black or white. Also has the benefit of clipping excess val due to adding the error
$blackOrWhite = 0;
} else {
$blackOrWhite = 255;
}
$oldPixel = $gray;
$newPixel = $blackOrWhite;
// Current pixel
imagesetpixel( $new, $x, $y,
imagecolorallocate( $new,
$newPixel,
$newPixel,
$newPixel
)
);
$qError = $oldPixel - $newPixel; // Quantization error
// Propagate error on neighbor pixels
if ( $x + 1 < $width ) {
$pixel[$x+1][$y] = (isset($pixel[$x+1][$y]) ? $pixel[$x+1][$y] : 0) + ($qError * (7 / 16));
}
if ( $x - 1 > 0 and $y + 1 < $height ) {
$pixel[$x-1][$y+1] = (isset($pixel[$x-1][$y+1]) ? $pixel[$x-1][$y+1] : 0) + ($qError * (3 / 16));
}
if ( $y + 1 < $height ) {
$pixel[$x][$y+1] = (isset($pixel[$x][$y+1]) ? $pixel[$x][$y+1] : 0) + ($qError * (5 / 16));
}
if ( $x + 1 < $width and $y + 1 < $height ) {
$pixel[$x+1][$y+1] = (isset($pixel[$x+1][$y+1]) ? $pixel[$x+1][$y+1] : 0) + ($qError * (1 / 16));
}
}
}
imagedestroy($old); // Free resource
// Create new image with updated core
return new Image(
$new,
$image->getImageFile(),
$width,
$height,
$image->getType()
);
}
/**
* Dither by applying a threshold map.
*
* @param Image $image
*
* @return Image
*/
private function ordered( $image ) {
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$old = $image->getCore();
$new = imagecreatetruecolor( $width, $height );
$thresholdMap = array(
array( 15, 135, 45, 165 ),
array( 195, 75, 225, 105 ),
array( 60, 180, 30, 150 ),
array( 240, 120, 210, 90 )
);
for ( $y = 0; $y < $height; $y += 1 ) {
for ( $x = 0; $x < $width; $x += 1 ) {
$color = imagecolorat( $old, $x, $y );
$r = ( $color >> 16 ) & 0xFF;
$g = ( $color >> 8 ) & 0xFF;
$b = $color & 0xFF;
$gray = round( $r * 0.3 + $g * 0.59 + $b * 0.11 );
$threshold = $thresholdMap[ $x % 4 ][ $y % 4 ];
$oldPixel = ( $gray + $threshold ) / 2;
if ( $oldPixel <= 127 ) { // Determine if black or white. Also has the benefit of clipping excess value
$newPixel = 0;
} else {
$newPixel = 255;
}
// Current pixel
imagesetpixel( $new, $x, $y,
imagecolorallocate( $new,
$newPixel,
$newPixel,
$newPixel
)
);
}
}
imagedestroy( $old ); // Free resource
// Create new image with updated core
return new Image(
$new,
$image->getImageFile(),
$width,
$height,
$image->getType()
);
}
}
@@ -0,0 +1,38 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Performs a gamma correction on an image.
*/
class Gamma implements FilterInterface{
/**
* @var float
*/
protected $amount; // >= 1.0
/**
* Gamma constructor.
* @param float $amount The amount of gamma correction to apply. >= 1.0
*/
public function __construct($amount)
{
$this->amount = (float) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
imagegammacorrect($image->getCore(), 1, $this->amount);
return $image;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Turn image into grayscale.
*/
class Grayscale implements FilterInterface{
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
imagefilter($image->getCore(), IMG_FILTER_GRAYSCALE);
return $image;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Invert the image colors.
*/
class Invert implements FilterInterface{
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
imagefilter($image->getCore(), IMG_FILTER_NEGATE);
return $image;
}
}
@@ -0,0 +1,38 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Pixelate an image.
*/
class Pixelate implements FilterInterface{
/**
* @var int $amount Pixelate size from >= 1
*/
protected $amount;
/**
* Pixelate constructor.
* @param int $amount The size of pixelation. >= 1
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
imagefilter($image->getCore(), IMG_FILTER_PIXELATE, $this->amount, true);
return $image;
}
}
@@ -0,0 +1,49 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Sharpen an image.
*/
class Sharpen implements FilterInterface{
/**
* @var int $amount
*/
protected $amount;
/**
* Sharpen constructor.
* @param int $amount Amount of sharpening from >= 1 to <= 100
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$amount = $this->amount;
// build matrix
$min = $amount >= 10 ? $amount * -0.01 : 0;
$max = $amount * -0.025;
$abs = ((4 * $min + 4 * $max) * -1) + 1;
$div = 1;
$matrix = array(
array($min, $max, $min),
array($max, $abs, $max),
array($min, $max, $min)
);
// apply the matrix
imageconvolution($image->getCore(), $matrix, $div, 0);
return $image;
}
}
@@ -0,0 +1,128 @@
<?php
namespace Grafika\Gd\Filter;
use Grafika\FilterInterface;
use Grafika\Gd\Image;
/**
* Sobel filter is an edge detection filter.
* @link https://en.wikipedia.org/wiki/Sobel_operator
*/
class Sobel implements FilterInterface
{
/**
* @param Image $image
*
* @return Image
*/
public function apply($image)
{
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$old = $image->getCore();
$pixels = array();
$new = imagecreatetruecolor($width, $height);
for ($y = 0; $y < $height; $y++) {
for ($x = 0; $x < $width; $x++) {
// row 0
if ($x > 0 and $y > 0) {
$matrix[0][0] = $this->getColor($old, $pixels,$x - 1, $y - 1);
} else {
$matrix[0][0] = $this->getColor($old, $pixels, $x, $y);
}
if ($y > 0) {
$matrix[1][0] = $this->getColor($old, $pixels, $x, $y - 1);
} else {
$matrix[1][0] = $this->getColor($old, $pixels, $x, $y);
}
if ($x + 1 < $width and $y > 0) {
$matrix[2][0] = $this->getColor($old, $pixels, $x + 1, $y - 1);
} else {
$matrix[2][0] = $this->getColor($old, $pixels, $x, $y);
}
// row 1
if ($x > 0) {
$matrix[0][1] = $this->getColor($old, $pixels, $x - 1, $y);
} else {
$matrix[0][1] = $this->getColor($old, $pixels, $x, $y);
}
if ($x + 1 < $width) {
$matrix[2][1] = $this->getColor($old, $pixels, $x + 1, $y);
} else {
$matrix[2][1] = $this->getColor($old, $pixels, $x, $y);
}
// row 1
if ($x > 0 and $y + 1 < $height) {
$matrix[0][2] = $this->getColor($old, $pixels, $x - 1, $y + 1);
} else {
$matrix[0][2] = $this->getColor($old, $pixels, $x, $y);
}
if ($y + 1 < $height) {
$matrix[1][2] = $this->getColor($old, $pixels, $x, $y + 1);
} else {
$matrix[1][2] = $this->getColor($old, $pixels, $x, $y);
}
if ($x + 1 < $width and $y + 1 < $height) {
$matrix[2][2] = $this->getColor($old, $pixels, $x + 1, $y + 1);
} else {
$matrix[2][2] = $this->getColor($old, $pixels, $x, $y);
}
$edge = $this->convolve($matrix);
$edge = intval($edge / 2);
if ($edge > 255) {
$edge = 255;
}
$color = imagecolorallocate($new, $edge, $edge, $edge);
imagesetpixel($new, $x, $y, $color);
}
}
imagedestroy($old); // Free resource
// Create and return new image with updated core
return new Image(
$new,
$image->getImageFile(),
$width,
$height,
$image->getType()
);
}
private function convolve($matrix)
{
$gx = $matrix[0][0] + ($matrix[2][0] * -1) +
($matrix[0][1] * 2) + ($matrix[2][1] * -2) +
$matrix[0][2] + ($matrix[2][2] * -1);
$gy = $matrix[0][0] + ($matrix[1][0] * 2) + $matrix[2][0] +
($matrix[0][2] * -1) + ($matrix[1][2] * -2) + ($matrix[2][2] * -1);
return sqrt(($gx * $gx) + ($gy * $gy));
}
private function getColor($gd, &$pixels, $x, $y)
{
if (isset($pixels[$x][$y])) {
return $pixels[$x][$y];
}
$color = imagecolorat($gd, $x, $y);
$r = ($color >> 16) & 0xFF;
$g = ($color >> 8) & 0xFF;
$b = $color & 0xFF;
return $pixels[$x][$y] = round($r * 0.3 + $g * 0.59 + $b * 0.11); // gray
}
}
@@ -0,0 +1,123 @@
<?php
namespace Grafika\Gd\Helper;
/**
* Class GifByteStream
* Normalize string operations.
* Treat string as byte stream where 2 string characters are treated as 1 hex string (byte).
* Eg. String ffff with length 4 is 0xff 0xff in bytes with length of 2.
*/
final class GifByteStream
{
/**
* @var int
*/
private $position;
/**
* @var string
*/
private $bytes;
/**
* GifByteStream constructor.
*
* @param string $bytes Accepts only the string created by unpack('H*')
*/
public function __construct($bytes)
{
$this->position = 0;
$this->bytes = $bytes;
}
/**
* Take a bite from the byte stream.
*
* @param int $size Byte size in integer.
*
* @return string
*/
public function bite($size)
{
$str = substr($this->bytes, $this->position * 2, $size * 2);
$this->position += $size;
return $str;
}
/**
* @param $byteString
* @param $offset
*
* @return bool|float
*/
public function find($byteString, $offset)
{
$pos = strpos($this->bytes, $byteString, $offset * 2);
if ($pos !== false) {
return $pos / 2;
}
return false;
}
/**
* @param int $step
*/
public function back($step = 1)
{
$this->position -= $step;
}
/**
* @param int $step
*/
public function next($step = 1)
{
$this->position += $step;
}
/**
* @return float
*/
public function length()
{
return strlen($this->bytes) / 2;
}
/**
* @param $position
*/
public function setPosition($position)
{
$this->position = $position;
}
/**
* @return int
*/
public function getPosition()
{
return $this->position;
}
/**
* @return mixed
*/
public function getBytes()
{
return $this->bytes;
}
/**
* @return bool
*/
public function isEnd()
{
if ($this->position > $this->length() - 1) {
return true;
}
return false;
}
}
@@ -0,0 +1,605 @@
<?php
namespace Grafika\Gd\Helper;
final class GifHelper {
/**
* @param $imageFile
*
* @return GifByteStream
* @throws \Exception
*/
public function open($imageFile){
$fp = fopen( $imageFile, 'rb'); // Binary read
if($fp === false ) {
throw new \Exception(sprintf('Error loading file: "%s".', $imageFile));
}
$size = filesize( $imageFile );
$bytes = fread($fp, $size);
$bytes = unpack('H*', $bytes); // Unpack as hex
$bytes = $bytes[1];
fclose($fp);
return new GifByteStream($bytes);
}
/**
* @param string $bin Raw binary data from imagegif or file_get_contents
*
* @return GifByteStream
*/
public function load($bin){
$bytes = unpack('H*', $bin); // Unpack as hex
$bytes = $bytes[1];
return new GifByteStream($bytes);
}
/**
* @param GifByteStream $bytes
*
* @return bool
*/
public function isAnimated($bytes){
$bytes->setPosition(13);
$lastPos = $bytes->getPosition();
$gceCount = 0;
while (($lastPos = $bytes->find('21f904', $lastPos))!== false) {
$gceCount++;
if($gceCount>1){
return true;
}
}
return false;
}
/**
* Encode data into GIF hex string.
*
* @param array $data The array returned by decode.
*
* @return string Hex string of GIF
*/
public function encode($data){
$hex = '';
// header block
$hex .= $this->_fixSize($this->_asciiToHex($data['signature']),3);
$hex .= $this->_fixSize($this->_asciiToHex($data['version']),3);
// logical screen descriptor block
$hex .= $this->_switchEndian($this->_fixSize(dechex($data['canvasWidth']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($data['canvasHeight']), 4));
$packedField = decbin($data['globalColorTableFlag']);
$packedField .= $this->_fixSize(decbin($data['colorResolution']), 3);
$packedField .= decbin($data['sortFlag']);
$packedField .= $this->_fixSize(decbin($data['sizeOfGlobalColorTable']), 3);
$hex .= $this->_fixSize(dechex(bindec($packedField)), 2);
$hex .= $this->_fixSize(dechex($data['backgroundColorIndex']), 2);
$hex .= $this->_fixSize(dechex($data['pixelAspectRatio']), 2);
// global color table optional
if($data['globalColorTableFlag']>0) {
$hex .= $data['globalColorTable'];
}
// app ext optional
if(isset($data['applicationExtension'])){
foreach($data['applicationExtension'] as $app){
$hex .= '21ff0b';
$hex .= $this->_fixSize($this->_asciiToHex($app['appId']),8);
$hex .= $this->_fixSize($this->_asciiToHex($app['appCode']),3);
foreach($app['subBlocks'] as $subBlock){
$len = $this->_fixSize(dechex(strlen($subBlock)/2),2);
$hex .= $len.$subBlock;
}
$hex .= '00';
}
}
foreach($data['frames'] as $i=>$frame){
// graphics control optional
if(isset($frame['delayTime'])) {
$hex .= '21f904';
$packedField = '000'; // reserved
$packedField .= $this->_fixSize(decbin($frame['disposalMethod']), 3);
$packedField .= decbin($frame['userInputFlag']);
$packedField .= decbin($frame['transparentColorFlag']);
$hex .= $this->_fixSize(dechex(bindec($packedField)), 2);
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['delayTime']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['transparentColorIndex']), 2));
$hex .= '00';
}
//image desc
$hex .= '2c';
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageLeft']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageTop']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageWidth']), 4));
$hex .= $this->_switchEndian($this->_fixSize(dechex($frame['imageHeight']), 4));
$packedField = decbin($frame['localColorTableFlag']);
$packedField .= decbin($frame['interlaceFlag']);
$packedField .= decbin($frame['sortFlag']);
$packedField .= '00'; // reserved
$packedField .= $this->_fixSize(decbin($frame['sizeOfLocalColorTable']), 3);
$hex .= $this->_fixSize(dechex(bindec($packedField)), 2);
// local color table optional
if($frame['localColorTableFlag']>0){
$hex .= $frame['localColorTable'];
}
$hex .= $frame['imageData'];
}
$hex .= $data['trailer'];
return $hex;
}
/**
* Decode GIF into array of data for easy use in PHP userland.
*
* @param GifByteStream $bytes Decode byte stream into array of GIF blocks.
*
* @return array Array containing GIF data
* @throws \Exception
*
*/
public function decode($bytes){
$bytes->setPosition(0);
$blocks = $this->decodeToBlocks($bytes);
return $this->expandBlocks($blocks);
}
/**
* Decompose GIF into its block components. The GIF blocks are in the order that they appear in the byte stream.
*
* @param GifByteStream $bytes
*
* @return array
* @throws \Exception
*/
public function decodeToBlocks($bytes){
$bytes->setPosition(0);
$blocks = array();
// Header block
$blocks['header'] = $bytes->bite(6);
// Logical screen descriptor block
$part = $bytes->bite(2); // canvass w
$hex = $part;
$part = $bytes->bite(2); // canvass h
$hex .= $part;
$part = $bytes->bite(1); // packed field
$hex .= $part;
$bin = $this->_fixSize($this->_hexToBin($part),8);
$globalColorTableFlag = bindec(substr($bin, 0 ,1));
$sizeOfGlobalColorTable = bindec(substr($bin, 5 ,3));
$part = $bytes->bite(1); // backgroundColorIndex
$hex .= $part;
$part = $bytes->bite(1); // pixelAspectRatio
$hex .= $part;
$blocks['logicalScreenDescriptor'] = $hex;
// Global color table is optional so check its existence
if($globalColorTableFlag > 0){
// Formula: 3 * (2^(N+1))
$colorTableLength = 3*(pow(2,($sizeOfGlobalColorTable+1)));
$part = $bytes->bite($colorTableLength);
$blocks['globalColorTable'] = $part;
}
$commentC = $plainTextC = $appCount = $gce = $dc = 0; // index count
while(!$bytes->isEnd()){
$part = $bytes->bite(1);
if('21'===$part){ // block tests
$hex = $part;
$part = $bytes->bite(1);
if('ff'===$part) { // App extension block
$hex .= $part;
$part = $bytes->bite(1); // app name length should be 0x0b or int 11 but we check anyways
$size = hexdec($part); // turn it to int
$hex .= $part;
$part = $bytes->bite($size); // app name
$hex .= $part;
while (!$bytes->isEnd()) { // loop thru all app sub blocks
$nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$hex .= $nextSize;
$size = hexdec($nextSize);
$part = $bytes->bite($size);
$hex .= $part;
} else {
$hex .= $nextSize;
$blocks['applicationExtension-'.$appCount] = $hex;
break;
}
}
$appCount++;
} else if('f9'===$part){ // graphic
$hex .= $part;
$part = $bytes->bite(1); // size
$hex .= $part;
$part = $bytes->bite(1); // packed field
$hex .= $part;
$part = $bytes->bite(2); // delay time
$hex .= $part;
$part = $bytes->bite(1); // trans color index
$hex .= $part;
$part = $bytes->bite(1); // terminator
$hex .= $part;
$blocks['graphicControlExtension-'.$gce] = $hex;
$gce++;
} else if('01' === $part){ // plain text ext
$hex .= $part;
while (!$bytes->isEnd()) { // loop thru all app sub blocks
$nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$hex .= $nextSize;
$size = hexdec($nextSize);
$part = $bytes->bite($size);
$hex .= $part;
} else {
$hex .= $nextSize;
$blocks['plainTextExtension-'.$plainTextC] = $hex;
break;
}
}
$plainTextC++;
} else if('fe' === $part){ // comment ext
$hex .= $part;
while (!$bytes->isEnd()) { // loop thru all app sub blocks
$nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$hex .= $nextSize;
$size = hexdec($nextSize);
$part = $bytes->bite($size);
$hex .= $part;
} else {
$hex .= $nextSize;
$blocks['commentExtension-'.$commentC] = $hex;
break;
}
}
$commentC++;
}
} else if ('2c'===$part){ // image descriptors
$hex = $part;
$part = $bytes->bite(2); // imageLeft
$hex .= $part;
$part = $bytes->bite(2); // imageTop
$hex .= $part;
$part = $bytes->bite(2); // imageWidth
$hex .= $part;
$part = $bytes->bite(2); // imageHeight
$hex .= $part;
$part = $bytes->bite(1); // packed field
$hex .= $part;
$blocks['imageDescriptor-'.$dc] = $hex;
$bin = $this->_fixSize($this->_hexToBin($part), 8);
$localColorTableFlag = bindec(substr($bin, 0, 1));
$sizeOfLocalColorTable = bindec(substr($bin, 5, 3));
//LC
if($localColorTableFlag){
// Formula: 3 * (2^(N+1))
$localColorTableLen = 3 * (pow(2, ($sizeOfLocalColorTable + 1)));
$part = $bytes->bite($localColorTableLen);
$blocks['localColorTable-'.$dc] = $part;
}
// Image data
$part = $bytes->bite(1); // LZW code
$hex = $part;
while ($bytes->isEnd()===false) {
$nextSize = $bytes->bite(1);
$hex .= $nextSize;
if($nextSize !== '00') {
$subBlockLen = hexdec($nextSize);
$subBlock = $bytes->bite($subBlockLen);
$hex .= $subBlock;
} else {
$blocks['imageData-'.$dc] = $hex;
break;
}
}
$dc++;
} else {
$blocks['trailer'] = $part;
break;
}
}
if($blocks['trailer']!=='3b'){
throw new \Exception('Error decoding GIF. Stopped at '.$bytes->getPosition().'. Length is '.$bytes->length().'.');
}
return $blocks;
}
/**
* Expand GIF blocks into useful info.
*
* @param array $blocks Accepts the array returned by decodeToBlocks
*
* @return array
*/
public function expandBlocks($blocks){
$decoded = array();
foreach($blocks as $blockName=>$block){
$bytes = new GifByteStream($block);
if(false !== strpos($blockName, 'header')){
$part = $bytes->bite(3);
$decoded['signature'] = $this->_hexToAscii($part);
$part = $bytes->bite(3);
$decoded['version'] = $this->_hexToAscii($part);
} else if(false !== strpos($blockName, 'logicalScreenDescriptor')){
$part = $bytes->bite(2);
$decoded['canvasWidth'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['canvasHeight'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(1);
$bin = $this->_fixSize($this->_hexToBin($part), 8); // Make sure len is correct
$decoded['globalColorTableFlag'] = bindec(substr($bin, 0 ,1));
$decoded['colorResolution'] = bindec(substr($bin, 1 ,3));
$decoded['sortFlag'] = bindec(substr($bin, 4 ,1));
$decoded['sizeOfGlobalColorTable'] = bindec(substr($bin, 5 ,3));
$part = $bytes->bite(1);
$decoded['backgroundColorIndex'] = hexdec($part);
$part = $bytes->bite(1);
$decoded['pixelAspectRatio'] = hexdec($part);
} else if(false !== strpos($blockName, 'globalColorTable')){
$decoded['globalColorTable'] = $block;
} else if(false !== strpos($blockName, 'applicationExtension')){
$index = explode('-', $blockName, 2);
$index = $index[1];
$bytes->next(2); // Skip ext intro and label: 21 ff
$appNameSize = $bytes->bite(1); // 0x0b or 11 according to spec but we check anyways
$appNameSize = hexdec($appNameSize);
$appName = $this->_hexToAscii($bytes->bite($appNameSize));
$subBlocks = array();
while (!$bytes->isEnd()) { // loop thru all app sub blocks
$nextSize = $bytes->bite(1);
if($nextSize !== '00'){
$size = hexdec($nextSize);
$subBlocks[] = $bytes->bite($size);
}
}
if($appName==='NETSCAPE2.0'){
$decoded['applicationExtension'][$index]['appId'] = 'NETSCAPE';
$decoded['applicationExtension'][$index]['appCode'] = '2.0';
$decoded['applicationExtension'][$index]['subBlocks'] = $subBlocks;
$decoded['loopCount'] = hexdec($this->_switchEndian(substr($subBlocks[0], 2, 4)));
} else {
$decoded['applicationExtension'][$index]['appId'] = substr($appName, 0, 8);
$decoded['applicationExtension'][$index]['appCode'] = substr($appName, 8, 3);
$decoded['applicationExtension'][$index]['subBlocks'] = $subBlocks;
}
} else if(false !== strpos($blockName, 'graphicControlExtension')) {
$index = explode('-', $blockName, 2);
$index = $index[1];
$bytes->next(3); // Skip ext intro, label, and block size which is always 4: 21 f9 04
$part = $bytes->bite(1); // packed field
$bin = $this->_fixSize($this->_hexToBin($part), 8); // Make sure len is correct
$decoded['frames'][$index]['disposalMethod'] = bindec(substr($bin, 3 ,3));
$decoded['frames'][$index]['userInputFlag'] = bindec(substr($bin, 6 ,1));
$decoded['frames'][$index]['transparentColorFlag'] = bindec(substr($bin, 7 ,1));
$part = $bytes->bite(2);
$decoded['frames'][$index]['delayTime'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(1);
$decoded['frames'][$index]['transparentColorIndex'] = hexdec($part);
} else if(false !== strpos($blockName, 'imageDescriptor')) {
$index = explode('-', $blockName, 2);
$index = $index[1];
$bytes->next(1); // skip separator: 2c
$part = $bytes->bite(2);
$decoded['frames'][$index]['imageLeft'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['frames'][$index]['imageTop'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['frames'][$index]['imageWidth'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(2);
$decoded['frames'][$index]['imageHeight'] = hexdec($this->_switchEndian($part));
$part = $bytes->bite(1); // packed field
$bin = $this->_fixSize($this->_hexToBin($part),
8);
$decoded['frames'][$index]['localColorTableFlag'] = bindec(substr($bin, 0, 1));
$decoded['frames'][$index]['interlaceFlag'] = bindec(substr($bin, 1, 1));
$decoded['frames'][$index]['sortFlag'] = bindec(substr($bin, 2, 1));
$decoded['frames'][$index]['sizeOfLocalColorTable'] = bindec(substr($bin, 5, 3));
} else if(false !== strpos($blockName, 'localColorTable')){
$index = explode('-', $blockName, 2);
$index = $index[1];
$decoded['frames'][$index]['localColorTable'] = $block;
} else if(false !== strpos($blockName, 'imageData')) {
$index = explode('-', $blockName, 2);
$index = $index[1];
$decoded['frames'][$index]['imageData'] = $block;
} else if($blockName === 'trailer') {
$decoded['trailer'] = $block;
}
unset($bytes);
}
return $decoded;
}
/**
* @param array $blocks The array returned by decode.
*
* @return array Array of images each containing 1 of each frames of the original image.
*/
public function splitFrames($blocks){
$images = array();
if (isset($blocks['frames'])){
foreach($blocks['frames'] as $a=>$unused){
$images[$a] = $blocks;
unset($images[$a]['frames']); // remove all frames.
foreach($blocks['frames'] as $b=>$frame){
if($a===$b){
$images[$a]['frames'][0] = $frame; // Re-add frames but use only 1 frame and discard others
break;
}
}
}
}
return $images;
}
/**
* @param $blocks
* @param $newW
* @param $newH
*
* @return array $blocks
*/
public function resize($blocks, $newW, $newH){
$images = $this->splitFrames($blocks);
// Loop on individual images and resize them using Gd
$firstFrameGd = null;
foreach($images as $imageIndex=>$image){
$hex = $this->encode($image);
$binaryRaw = pack('H*', $hex);
// Utilize gd for resizing
$old = imagecreatefromstring($binaryRaw);
$width = imagesx($old);
$height = imagesy($old);
$new = imagecreatetruecolor($newW, $newH); // Create a blank image
if($firstFrameGd){
$new = $firstFrameGd;
}
// Account for frame imageLeft and imageTop
$cX = $newW / $blocks['canvasWidth']; // change x
$dX = $image['frames'][0]['imageLeft'];
$cY = $newH / $blocks['canvasHeight'];
$dY = $image['frames'][0]['imageTop'];
imagecopyresampled(
$new,
$old,
$dX * $cX,// dx
$dY * $cY, // dy
0,
0,
$image['frames'][0]['imageWidth'] * $cX,
$image['frames'][0]['imageHeight'] * $cY,
$width,
$height
);
ob_start();
imagegif($new);
$binaryRaw = ob_get_contents();
ob_end_clean();
if($firstFrameGd===null){
$firstFrameGd = $new;
}
// Hex of resized
$bytes = $this->load($binaryRaw);
$hexNew = $this->decode($bytes);
// Update original frames with hex from resized frames
$blocks['frames'][$imageIndex]['imageWidth'] = $hexNew['frames'][0]['imageWidth'];
$blocks['frames'][$imageIndex]['imageHeight'] = $hexNew['frames'][0]['imageHeight'];
$blocks['frames'][$imageIndex]['imageLeft'] = $hexNew['frames'][0]['imageLeft'];
$blocks['frames'][$imageIndex]['imageTop'] = $hexNew['frames'][0]['imageTop'];
$blocks['frames'][$imageIndex]['imageData'] = $hexNew['frames'][0]['imageData'];
// We use local color tables on each frame. This will result in faster processing since we dont have to process the global color table at the cost of a larger file size.
$blocks['frames'][$imageIndex]['localColorTableFlag'] = $hexNew['globalColorTableFlag'];
$blocks['frames'][$imageIndex]['localColorTable'] = $hexNew['globalColorTable'];
$blocks['frames'][$imageIndex]['sizeOfLocalColorTable'] = $hexNew['sizeOfGlobalColorTable'];
$blocks['frames'][$imageIndex]['transparentColorFlag'] = 0;
}
// Update dimensions or else imagecreatefromgif will choke.
$blocks['canvasWidth'] = $newW;
$blocks['canvasHeight'] = $newH;
// Disable flickering bug. Also we are using localColorTable anyways.
$blocks['globalColorTableFlag'] = 0;
$blocks['globalColorTable'] = '';
return $blocks;
}
/**
* @param $asciiString
*
* @return string
*/
private function _asciiToHex($asciiString){
$chars = str_split($asciiString, 1);
$string = '';
foreach($chars as $char){
$string .= dechex(ord($char));
}
return $string;
}
/**
* @param $hexString
*
* @return string
*/
private function _hexToAscii($hexString){
$bytes = str_split($hexString, 2);
$string = '';
foreach($bytes as $byte){
$string .= chr(hexdec($byte)); // convert hex to dec to ascii character. See http://www.ascii.cl/
}
return $string;
}
/**
* @param $hexString
*
* @return string
*/
private function _hexToBin($hexString){
return base_convert($hexString, 16, 2);
}
/**
* @param $string
* @param $size
* @param string $char
*
* @return string
*/
private function _fixSize($string, $size, $char='0'){
return str_pad($string, $size, $char, STR_PAD_LEFT);
}
/**
* @param $hexString
*
* @return string
*/
private function _switchEndian($hexString) {
return implode('', array_reverse(str_split($hexString, 2)));
}
}
@@ -0,0 +1,457 @@
<?php
namespace Grafika\Gd;
use Grafika\Gd\Helper\GifHelper;
use Grafika\ImageType;
use Grafika\ImageInterface;
/**
* Image class for GD.
* @package Grafika\Gd
*/
final class Image implements ImageInterface {
/**
* @var resource GD resource ID.
*/
private $gd;
/**
* @var string File path to image.
*/
private $imageFile;
/**
* @var int Image width in pixels.
*/
private $width;
/**
* @var int Image height in pixels.
*/
private $height;
/**
* @var string Image type. See \Grafika\ImageType
*/
private $type;
/**
* @var string Contains array of animated GIF data.
*/
private $blocks;
/**
* @var bool True if animated GIF.
*/
private $animated;
/**
* Image constructor.
*
* @param resource $gd Must use GD's imagecreate* family of functions to create a GD resource.
* @param string $imageFile
* @param int $width
* @param int $height
* @param string $type
* @param string $blocks
* @param bool $animated
*/
public function __construct( $gd, $imageFile, $width, $height, $type, $blocks = '', $animated = false ) {
$this->gd = $gd;
$this->imageFile = $imageFile;
$this->width = $width;
$this->height = $height;
$this->type = $type;
$this->blocks = $blocks;
$this->animated = $animated;
}
/**
* Method called when 'clone' keyword is used.
*/
public function __clone()
{
$original = $this->gd;
$copy = imagecreatetruecolor($this->width, $this->height);
imagecopy($copy, $original, 0, 0, 0, 0, $this->width, $this->height);
$this->gd = $copy;
}
/**
* Output a binary raw dump of an image in a specified format.
*
* @param string|ImageType $type Image format of the dump.
*
* @throws \Exception When unsupported type.
*/
public function blob( $type = 'PNG' ) {
$type = strtoupper($type);
if ( ImageType::GIF == $type ) {
imagegif( $this->gd );
} else if ( ImageType::JPEG == $type ) {
imagejpeg( $this->gd );
} else if ( ImageType::PNG == $type ) {
imagepng( $this->gd );
} else if ( ImageType::WBMP == $type ) {
imagewbmp( $this->gd );
} else {
throw new \Exception( sprintf( 'File type "%s" not supported.', $type ) );
}
}
/**
* Create Image from image file.
*
* @param string $imageFile Path to image.
*
* @return Image
* @throws \Exception
*/
public static function createFromFile( $imageFile ) {
if ( ! file_exists( $imageFile ) ) {
throw new \Exception( sprintf( 'Could not open "%s". File does not exist.', $imageFile ) );
}
$type = self::_guessType( $imageFile );
if ( ImageType::GIF == $type ) {
return self::_createGif( $imageFile );
} else if ( ImageType::JPEG == $type ) {
return self::_createJpeg( $imageFile );
} else if ( ImageType::PNG == $type ) {
return self::_createPng( $imageFile );
} else if ( ImageType::WBMP == $type ) {
return self::_createWbmp( $imageFile );
} else {
throw new \Exception( sprintf( 'Could not open "%s". File type not supported.', $imageFile ) );
}
}
/**
* Create an Image from a GD resource. The file type defaults to unknown.
*
* @param resource $gd GD resource.
*
* @return Image
*/
public static function createFromCore( $gd ) {
return new self( $gd, '', imagesx( $gd ), imagesy( $gd ), ImageType::UNKNOWN );
}
/**
* Create a blank image.
*
* @param int $width Width in pixels.
* @param int $height Height in pixels.
*
* @return Image
*/
public static function createBlank($width = 1, $height = 1){
return new self(imagecreatetruecolor($width, $height), '', $width, $height, ImageType::UNKNOWN);
}
/**
* Set the blending mode for an image. Allows transparent overlays on top of an image.
*
* @param bool $flag True to enable blending mode.
* @return self
*/
public function alphaBlendingMode( $flag ){
imagealphablending( $this->gd, $flag );
return $this;
}
/**
* Enable/Disable transparency
*
* @param bool $flag True to enable alpha mode.
* @return self
*/
public function fullAlphaMode( $flag ){
if( true === $flag ){
$this->alphaBlendingMode( false ); // Must be false for full alpha mode to work
}
imagesavealpha( $this->gd, $flag );
return $this;
}
/**
* Returns animated flag.
*
* @return bool True if animated GIF.
*/
public function isAnimated() {
return $this->animated;
}
/**
* Get GD resource ID.
*
* @return resource
*/
public function getCore() {
return $this->gd;
}
/**
* Get image file path.
*
* @return string File path to image.
*/
public function getImageFile() {
return $this->imageFile;
}
/**
* Get image width in pixels.
*
* @return int
*/
public function getWidth() {
return $this->width;
}
/**
* Get image height in pixels.
*
* @return int
*/
public function getHeight() {
return $this->height;
}
/**
* Get image type.
*
* @return string
*/
public function getType() {
return $this->type;
}
/**
* Get blocks.
*
* @return string.
*/
public function getBlocks() {
return $this->blocks;
}
/**
* Get histogram from an entire image or its sub-region.
*
* @param array|null $slice Array of slice information. array( array( 0,0), array(100,50)) means x,y is 0,0 and width,height is 100,50
*
* @return array Returns array containing RGBA bins array('r'=>array(), 'g'=>array(), 'b'=>array(), 'a'=>array())
*/
public function histogram($slice = null)
{
$gd = $this->getCore();
if(null === $slice){
$sliceX = 0;
$sliceY = 0;
$sliceW = $this->getWidth();
$sliceH = $this->getHeight();
} else {
$sliceX = $slice[0][0];
$sliceY = $slice[0][1];
$sliceW = $slice[1][0];
$sliceH = $slice[1][1];
}
$rBin = array();
$gBin = array();
$bBin = array();
$aBin = array();
for ($y = $sliceY; $y < $sliceY+$sliceH; $y++) {
for ($x = $sliceX; $x < $sliceX+$sliceW; $x++) {
$rgb = imagecolorat($gd, $x, $y);
$a = ($rgb >> 24) & 0x7F; // 127 in hex. These are binary operations.
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
if ( ! isset($rBin[$r])) {
$rBin[$r] = 1;
} else {
$rBin[$r]++;
}
if ( ! isset($gBin[$g])) {
$gBin[$g] = 1;
} else {
$gBin[$g]++;
}
if ( ! isset($bBin[$b])) {
$bBin[$b] = 1;
} else {
$bBin[$b]++;
}
if ( ! isset($aBin[$a])) {
$aBin[$a] = 1;
} else {
$aBin[$a]++;
}
}
}
return array(
'r' => $rBin,
'g' => $gBin,
'b' => $bBin,
'a' => $aBin
);
}
/**
* Load a GIF image.
*
* @param string $imageFile
*
* @return Image
* @throws \Exception
*/
private static function _createGif( $imageFile ){
$gift = new GifHelper();
$bytes = $gift->open($imageFile);
$animated = $gift->isAnimated($bytes);
$blocks = '';
if($animated){
$blocks = $gift->decode($bytes);
}
$gd = @imagecreatefromgif( $imageFile );
if(!$gd){
throw new \Exception( sprintf('Could not open "%s". Not a valid %s file.', $imageFile, ImageType::GIF) );
}
return new self(
$gd,
$imageFile,
imagesx( $gd ),
imagesy( $gd ),
ImageType::GIF,
$blocks,
$animated
);
}
/**
* Load a JPEG image.
*
* @param string $imageFile File path to image.
*
* @return Image
* @throws \Exception
*/
private static function _createJpeg( $imageFile ){
$gd = @imagecreatefromjpeg( $imageFile );
if(!$gd){
throw new \Exception( sprintf('Could not open "%s". Not a valid %s file.', $imageFile, ImageType::JPEG ) );
}
return new self( $gd, $imageFile, imagesx( $gd ), imagesy( $gd ), ImageType::JPEG );
}
/**
* Load a PNG image.
*
* @param string $imageFile File path to image.
*
* @return Image
* @throws \Exception
*/
private static function _createPng( $imageFile ){
$gd = @imagecreatefrompng( $imageFile );
if(!$gd){
throw new \Exception( sprintf('Could not open "%s". Not a valid %s file.', $imageFile, ImageType::PNG) );
}
$image = new self( $gd, $imageFile, imagesx( $gd ), imagesy( $gd ), ImageType::PNG );
$image->fullAlphaMode( true );
return $image;
}
/**
* Load a WBMP image.
*
* @param string $imageFile
*
* @return Image
* @throws \Exception
*/
private static function _createWbmp( $imageFile ){
$gd = @imagecreatefromwbmp( $imageFile );
if(!$gd){
throw new \Exception( sprintf('Could not open "%s". Not a valid %s file.', $imageFile, ImageType::WBMP) );
}
return new self( $gd, $imageFile, imagesx( $gd ), imagesy( $gd ), ImageType::WBMP );
}
/**
* @param $imageFile
*
* @return string
*/
private static function _guessType( $imageFile ){
// Values from http://php.net/manual/en/image.constants.php starting with IMAGETYPE_GIF.
// 0 - unknown,
// 1 - GIF,
// 2 - JPEG,
// 3 - PNG
// 15 - WBMP
list($width, $height, $type) = getimagesize( $imageFile );
unset($width, $height);
if ( 1 == $type) {
return ImageType::GIF;
} else if ( 2 == $type) {
return ImageType::JPEG;
} else if ( 3 == $type) {
return ImageType::PNG;
} else if ( 15 == $type) {
return ImageType::WBMP;
}
return ImageType::UNKNOWN;
}
}
@@ -0,0 +1,71 @@
<?php
namespace Grafika\Gd\ImageHash;
use Grafika\Gd\Editor;
use Grafika\Gd\Image;
/**
* AverageHash
*
* Algorithm:
* Reduce size. Remove high frequencies and detail by shrinking to 8x8 so that there are 64 total pixels.
* Reduce color. The tiny 8x8 picture is converted to a grayscale.
* Average the colors. Compute the mean value of the 64 colors.
* Compute the bits. Each bit is simply set based on whether the color value is above or below the mean.
* Construct the hash. Set the 64 bits into a 64-bit integer. The order does not matter, just as long as you are consistent.
*
* http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
*
* @package Grafika\Gd\ImageHash
*/
class AverageHash
{
/**
* Generate and get the average hash of the image.
*
* @param Image $image
*
* @param Editor $editor
*
* @return string
*/
public function hash($image, $editor)
{
// Resize the image.
$width = 8;
$height = 8;
$image = clone $image; // Make sure we are working on the clone if Image is passed
$editor->resizeExact($image, $width, $height); // Resize to exactly 8x8
$gd = $image->getCore();
// Create an array of greyscale pixel values.
$pixels = array();
for ($y = 0; $y < $height; $y++) {
for ($x = 0; $x < $width; $x++) {
$rgba = imagecolorat($gd, $x, $y);
$r = ($rgba >> 16) & 0xFF;
$g = ($rgba >> 8) & 0xFF;
$b = $rgba & 0xFF;
$pixels[] = floor(($r + $g + $b) / 3); // Gray
}
}
// Get the average pixel value.
$average = floor(array_sum($pixels) / count($pixels));
// Each hash bit is set based on whether the current pixels value is above or below the average.
$hash = '';
foreach ($pixels as $pixel) {
if ($pixel > $average) {
$hash .= '1';
} else {
$hash .= '0';
}
}
return $hash;
}
}
@@ -0,0 +1,73 @@
<?php
namespace Grafika\Gd\ImageHash;
use Grafika\Gd\Editor;
use Grafika\Gd\Image;
/**
* DifferenceHash
*
* Algorithm:
* Reduce size. The fastest way to remove high frequencies and detail is to shrink the image. In this case, shrink it to 9x8 so that there are 72 total pixels.
* Reduce color. Convert the image to a grayscale picture. This changes the hash from 72 pixels to a total of 72 colors.
* Compute the difference. The algorithm works on the difference between adjacent pixels. This identifies the relative gradient direction. In this case, the 9 pixels per row yields 8 differences between adjacent pixels. Eight rows of eight differences becomes 64 bits.
* Assign bits. Each bit is simply set based on whether the left pixel is brighter than the right pixel.
*
* http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html
*
* @package Grafika\Gd\ImageHash
*/
class DifferenceHash
{
/**
* Generate and get the difference hash of image.
*
* @param Image $image
*
* @param Editor $editor
*
* @return string
*/
public function hash($image, $editor)
{
$width = 9;
$height = 8;
$image = clone $image; // Make sure we are working on the clone if Image is passed
$editor->resizeExact($image, $width, $height); // Resize to exactly 9x8
$gd = $image->getCore();
// Build hash
$hash = '';
for ($y = 0; $y < $height; $y++) {
// Get the pixel value for the leftmost pixel.
$rgba = imagecolorat($gd, 0, $y);
$r = ($rgba >> 16) & 0xFF;
$g = ($rgba >> 8) & 0xFF;
$b = $rgba & 0xFF;
$left = floor(($r + $g + $b) / 3);
for ($x = 1; $x < $width; $x++) {
// Get the pixel value for each pixel starting from position 1.
$rgba = imagecolorat($gd, $x, $y);
$r = ($rgba >> 16) & 0xFF;
$g = ($rgba >> 8) & 0xFF;
$b = $rgba & 0xFF;
$right = floor(($r + $g + $b) / 3);
// Each hash bit is set based on whether the left pixel is brighter than the right pixel.
if ($left > $right) {
$hash .= '1';
} else {
$hash .= '0';
}
// Prepare the next loop.
$left = $right;
}
}
$editor->free( $image );
return $hash;
}
}
@@ -0,0 +1,394 @@
<?php
namespace Grafika;
use Grafika\Gd\DrawingObject\CubicBezier as GdCubicBezier;
use Grafika\Gd\DrawingObject\Ellipse as GdEllipse;
use Grafika\Gd\DrawingObject\Line as GdLine;
use Grafika\Gd\DrawingObject\Polygon as GdPolygon;
use Grafika\Gd\DrawingObject\QuadraticBezier as GdQuadraticBezier;
use Grafika\Gd\DrawingObject\Rectangle as GdRectangle;
use Grafika\Gd\Editor as GdEditor;
use Grafika\Gd\Filter\Dither as GdDither;
use Grafika\Gd\Filter\Blur as GdBlur;
use Grafika\Gd\Filter\Brightness as GdBrightness;
use Grafika\Gd\Filter\Colorize as GdColorize;
use Grafika\Gd\Filter\Contrast as GdContrast;
use Grafika\Gd\Filter\Gamma as GdGamma;
use Grafika\Gd\Filter\Grayscale as GdGrayscale;
use Grafika\Gd\Filter\Invert as GdInvert;
use Grafika\Gd\Filter\Pixelate as GdPixelate;
use Grafika\Gd\Filter\Sharpen as GdSharpen;
use Grafika\Gd\Filter\Sobel as GdSobel;
use Grafika\Gd\Image as GdImage;
use Grafika\Imagick\DrawingObject\CubicBezier as ImagickCubicBezier;
use Grafika\Imagick\DrawingObject\Ellipse as ImagickEllipse;
use Grafika\Imagick\DrawingObject\Line as ImagickLine;
use Grafika\Imagick\DrawingObject\Polygon as ImagickPolygon;
use Grafika\Imagick\DrawingObject\QuadraticBezier as ImagickQuadraticBezier;
use Grafika\Imagick\DrawingObject\Rectangle as ImagickRectangle;
use Grafika\Imagick\Editor as ImagickEditor;
use Grafika\Imagick\Filter\Blur as ImagickBlur;
use Grafika\Imagick\Filter\Brightness as ImagickBrightness;
use Grafika\Imagick\Filter\Colorize as ImagickColorize;
use Grafika\Imagick\Filter\Contrast as ImagickContrast;
use Grafika\Imagick\Filter\Gamma as ImagickGamma;
use Grafika\Imagick\Filter\Dither as ImagickDither;
use Grafika\Imagick\Filter\Grayscale as ImagickGrayscale;
use Grafika\Imagick\Filter\Invert as ImagickInvert;
use Grafika\Imagick\Filter\Pixelate as ImagickPixelate;
use Grafika\Imagick\Filter\Sharpen as ImagickSharpen;
use Grafika\Imagick\Filter\Sobel as ImagickSobel;
use Grafika\Imagick\Image as ImagickImage;
/**
* Contains factory methods for detecting editors, creating editors and images.
* @package Grafika
*/
class Grafika
{
/**
* Grafika root directory
*/
const DIR = __DIR__;
/**
* @var array $editorList List of editors to evaluate.
*/
private static $editorList = array('Imagick', 'Gd');
/**
* Return path to directory containing fonts used in text operations.
*
* @return string
*/
public static function fontsDir()
{
$ds = DIRECTORY_SEPARATOR;
return realpath(self::DIR . $ds . '..' . $ds . '..') . $ds . 'fonts';
}
/**
* Change the editor list order of evaluation globally.
*
* @param array $editorList
*
* @throws \Exception
*/
public static function setEditorList($editorList){
if(!is_array($editorList)){
throw new \Exception('$editorList must be an array.');
}
self::$editorList = $editorList;
}
/**
* Detects and return the name of the first supported editor which can either be "Imagick" or "Gd".
*
* @param array $editorList Array of editor list names. Use this to change the order of evaluation for editors for this function call only. Default order of evaluation is Imagick then GD.
*
* @return string Name of available editor.
* @throws \Exception Throws exception if there are no supported editors.
*/
public static function detectAvailableEditor($editorList = null)
{
if(null === $editorList){
$editorList = self::$editorList;
}
/* Get first supported editor instance. Order of editorList matter. */
foreach ($editorList as $editorName) {
if ('Imagick' === $editorName) {
$editorInstance = new ImagickEditor();
} else {
$editorInstance = new GdEditor();
}
/** @var EditorInterface $editorInstance */
if (true === $editorInstance->isAvailable()) {
return $editorName;
}
}
throw new \Exception('No supported editor.');
}
/**
* Creates the first available editor.
*
* @param array $editorList Array of editor list names. Use this to change the order of evaluation for editors. Default order of evaluation is Imagick then GD.
*
* @return EditorInterface
* @throws \Exception
*/
public static function createEditor($editorList = array('Imagick', 'Gd'))
{
$editorName = self::detectAvailableEditor($editorList);
if ('Imagick' === $editorName) {
return new ImagickEditor();
} else {
return new GdEditor();
}
}
/**
* Create an image.
* @param string $imageFile Path to image file.
*
* @return ImageInterface
* @throws \Exception
*/
public static function createImage($imageFile)
{
$editorName = self::detectAvailableEditor();
if ('Imagick' === $editorName) {
return ImagickImage::createFromFile($imageFile);
} else {
return GdImage::createFromFile($imageFile);
}
}
/**
* Create a blank image.
*
* @param int $width Width of image in pixels.
* @param int $height Height of image in pixels.
*
* @return ImageInterface
* @throws \Exception
*/
public static function createBlankImage($width = 1, $height = 1)
{
$editorName = self::detectAvailableEditor();
if ('Imagick' === $editorName) {
return ImagickImage::createBlank($width, $height);
} else {
return GdImage::createBlank($width, $height);
}
}
/**
* Create a filter. Detects available editor to use.
*
* @param string $filterName The name of the filter.
*
* @return FilterInterface
* @throws \Exception
*/
public static function createFilter($filterName)
{
$editorName = self::detectAvailableEditor();
$p = func_get_args();
if ('Imagick' === $editorName) {
switch ($filterName){
case 'Blur':
return new ImagickBlur(
(array_key_exists(1,$p) ? $p[1] : 1)
);
case 'Brightness':
return new ImagickBrightness(
$p[1]
);
case 'Colorize':
return new ImagickColorize(
$p[1], $p[2], $p[3]
);
case 'Contrast':
return new ImagickContrast(
$p[1]
);
case 'Dither':
return new ImagickDither(
$p[1]
);
case 'Gamma':
return new ImagickGamma(
$p[1]
);
case 'Grayscale':
return new ImagickGrayscale();
case 'Invert':
return new ImagickInvert();
case 'Pixelate':
return new ImagickPixelate(
$p[1]
);
case 'Sharpen':
return new ImagickSharpen(
$p[1]
);
case 'Sobel':
return new ImagickSobel();
}
throw new \Exception('Invalid filter name.');
} else {
switch ($filterName){
case 'Blur':
return new GdBlur(
(array_key_exists(1,$p) ? $p[1] : 1)
);
case 'Brightness':
return new GdBrightness(
$p[1]
);
case 'Colorize':
return new GdColorize(
$p[1], $p[2], $p[3]
);
case 'Contrast':
return new GdContrast(
$p[1]
);
case 'Dither':
return new GdDither(
$p[1]
);
case 'Gamma':
return new GdGamma(
$p[1]
);
case 'Grayscale':
return new GdGrayscale();
case 'Invert':
return new GdInvert();
case 'Pixelate':
return new GdPixelate(
$p[1]
);
case 'Sharpen':
return new GdSharpen(
$p[1]
);
case 'Sobel':
return new GdSobel();
}
throw new \Exception('Invalid filter name.');
}
}
/**
* Draws an object. Detects available editor to use.
*
* @param string $drawingObjectName The name of the DrawingObject.
*
* @return DrawingObjectInterface
* @throws \Exception
*
* We use array_key_exist() instead of isset() to be able to detect a parameter with a NULL value.
*/
public static function createDrawingObject($drawingObjectName)
{
$editorName = self::detectAvailableEditor();
$p = func_get_args();
if ('Imagick' === $editorName) {
switch ($drawingObjectName){
case 'CubicBezier':
return new ImagickCubicBezier(
$p[1],
$p[2],
$p[3],
$p[4],
(array_key_exists(5,$p) ? $p[5] : '#000000')
);
case 'Ellipse':
return new ImagickEllipse(
$p[1],
$p[2],
(array_key_exists(3,$p) ? $p[3] : array(0,0)),
(array_key_exists(4,$p) ? $p[4] : 1),
(array_key_exists(5,$p) ? $p[5] : '#000000'),
(array_key_exists(6,$p) ? $p[6] : '#FFFFFF')
);
case 'Line':
return new ImagickLine(
$p[1],
$p[2],
(array_key_exists(3,$p) ? $p[3] : 1),
(array_key_exists(4,$p) ? $p[4] : '#000000')
);
case 'Polygon':
return new ImagickPolygon(
$p[1],
(array_key_exists(2,$p) ? $p[2] : 1),
(array_key_exists(3,$p) ? $p[3] : '#000000'),
(array_key_exists(4,$p) ? $p[4] : '#FFFFFF')
);
case 'Rectangle':
return new ImagickRectangle(
$p[1],
$p[2],
(array_key_exists(3,$p) ? $p[3] : array(0,0)),
(array_key_exists(4,$p) ? $p[4] : 1),
(array_key_exists(5,$p) ? $p[5] : '#000000'),
(array_key_exists(6,$p) ? $p[6] : '#FFFFFF')
);
case 'QuadraticBezier':
return new ImagickQuadraticBezier(
$p[1],
$p[2],
$p[3],
(array_key_exists(4,$p) ? $p[4] : '#000000')
);
}
throw new \Exception('Invalid drawing object name.');
} else {
switch ($drawingObjectName) {
case 'CubicBezier':
return new GdCubicBezier(
$p[1],
$p[2],
$p[3],
$p[4],
(array_key_exists(5,$p) ? $p[5] : '#000000')
);
case 'Ellipse':
return new GdEllipse(
$p[1],
$p[2],
(array_key_exists(3,$p) ? $p[3] : array(0,0)),
(array_key_exists(4,$p) ? $p[4] : 1),
(array_key_exists(5,$p) ? $p[5] : '#000000'),
(array_key_exists(6,$p) ? $p[6] : '#FFFFFF')
);
case 'Line':
return new GdLine(
$p[1],
$p[2],
(array_key_exists(3,$p) ? $p[3] : 1),
(array_key_exists(4,$p) ? $p[4] : '#000000')
);
case 'Polygon':
return new GdPolygon(
$p[1],
(array_key_exists(2,$p) ? $p[2] : 1),
(array_key_exists(3,$p) ? $p[3] : '#000000'),
(array_key_exists(4,$p) ? $p[4] : '#FFFFFF')
);
case 'Rectangle':
return new GdRectangle(
$p[1],
$p[2],
(array_key_exists(3,$p) ? $p[3] : array(0,0)),
(array_key_exists(4,$p) ? $p[4] : 1),
(array_key_exists(5,$p) ? $p[5] : '#000000'),
(array_key_exists(6,$p) ? $p[6] : '#FFFFFF')
);
case 'QuadraticBezier':
return new GdQuadraticBezier(
$p[1],
$p[2],
$p[3],
(array_key_exists(4,$p) ? $p[4] : '#000000')
);
}
throw new \Exception('Invalid drawing object name.');
}
}
}
@@ -0,0 +1,80 @@
<?php
namespace Grafika;
/**
* Interface ImageInterface
* @package Grafika
*/
interface ImageInterface
{
/**
* Output a binary raw dump of an image in a specified format.
*
* @param string|ImageType $type Image format of the dump. See Grafika\ImageType for supported formats.
*/
public function blob($type);
/**
* Create a blank image.
*
* @param int $width Width of image in pixels.
* @param int $height Height of image in pixels.
*
* @return ImageInterface Instance of image.
*/
public static function createBlank($width = 1, $height = 1);
/**
* Create Image from core.
*
* @param resource|\Imagick $core GD resource for GD editor or Imagick instance for Imagick editor
*
* @return ImageInterface Instance of image.
*/
public static function createFromCore($core);
/**
* Create Image from image file.
*
* @param string $imageFile Path to image file.
*
* @return ImageInterface Instance of image.
*/
public static function createFromFile($imageFile);
/**
* Get Image core.
*
* @return resource|\Imagick GD resource or Imagick instance
*/
public function getCore();
/**
* @return int Height in pixels.
*/
public function getHeight();
/**
* @return string File path to image if Image was created from an image file.
*/
public function getImageFile();
/**
* @return string Type of image. See ImageType.
*/
public function getType();
/**
* @return int Width in pixels.
*/
public function getWidth();
/**
* Returns animated flag.
*
* @return bool True if animated GIF or false otherwise.
*/
public function isAnimated();
}
@@ -0,0 +1,21 @@
<?php
namespace Grafika;
/**
* Class ImageType. Represent the different image types for GD and Imagick consistently.
*
* @package Grafika
*/
class ImageType {
const UNKNOWN = '';
const GIF = 'GIF';
const JPEG = 'JPEG';
const PNG = 'PNG';
const WBMP = 'WBMP';
}
@@ -0,0 +1,51 @@
<?php
namespace Grafika\Imagick\DrawingObject;
use Grafika\DrawingObject\CubicBezier as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Imagick\Image;
use Grafika\ImageInterface;
/**
* Class CubicBezier
* @package Grafika
*/
class CubicBezier extends Base implements DrawingObjectInterface
{
/**
* @param ImageInterface $image
* @return Image
*/
public function draw($image)
{
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$imagick = $image->getCore();
$draw = new \ImagickDraw();
$strokeColor = new \ImagickPixel($this->getColor()->getHexString());
$fillColor = new \ImagickPixel('rgba(0,0,0,0)');
$draw->setStrokeOpacity(1);
$draw->setStrokeColor($strokeColor);
$draw->setFillColor($fillColor);
$points = array(
array('x'=> $this->point1[0], 'y'=> $this->point1[1]),
array('x'=> $this->control1[0], 'y'=> $this->control1[1]),
array('x'=> $this->control2[0], 'y'=> $this->control2[1]),
array('x'=> $this->point2[0], 'y'=> $this->point2[1]),
);
$draw->bezier($points);
// Render the draw commands in the ImagickDraw object
$imagick->drawImage($draw);
$type = $image->getType();
$file = $image->getImageFile();
return new Image($imagick, $file, $width, $height, $type); // Create new image with updated core
}
}
@@ -0,0 +1,40 @@
<?php
namespace Grafika\Imagick\DrawingObject;
use Grafika\DrawingObject\Ellipse as Base;
use Grafika\DrawingObjectInterface;
use Grafika\ImageInterface;
/**
* Class Ellipse
* @package Grafika
*/
class Ellipse extends Base implements DrawingObjectInterface
{
/**
* @param ImageInterface $image
* @return ImageInterface
*/
public function draw($image)
{
$strokeColor = new \ImagickPixel($this->getBorderColor()->getHexString());
$fillColor = new \ImagickPixel($this->getFillColor()->getHexString());
$draw = new \ImagickDraw();
$draw->setStrokeColor($strokeColor);
$draw->setFillColor($fillColor);
$draw->setStrokeWidth($this->borderSize);
list($x, $y) = $this->pos;
$left = $x + $this->width / 2;
$top = $y + $this->height / 2;
$draw->ellipse($left, $top, $this->width/2, $this->height/2, 0, 360);
$image->getCore()->drawImage($draw);
return $image;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Grafika\Imagick\DrawingObject;
use Grafika\DrawingObject\Line as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Imagick\Image;
/**
* Class Line
* @package Grafika
*/
class Line extends Base implements DrawingObjectInterface
{
/**
* @param Image $image
*
* @return Image
*/
public function draw($image)
{
$strokeColor = new \ImagickPixel($this->getColor()->getHexString());
$draw = new \ImagickDraw();
$draw->setStrokeColor($strokeColor);
$draw->setStrokeWidth($this->thickness);
list($x1, $y1) = $this->point1;
list($x2, $y2) = $this->point2;
$draw->line($x1, $y1, $x2, $y2);
$image->getCore()->drawImage($draw);
return $image;
}
}
@@ -0,0 +1,52 @@
<?php
namespace Grafika\Imagick\DrawingObject;
use Grafika\DrawingObject\Polygon as Base;
use Grafika\DrawingObjectInterface;
/**
* Class Polygon
* @package Grafika
*/
class Polygon extends Base implements DrawingObjectInterface{
public function draw( $image ) {
$draw = new \ImagickDraw();
$draw->setStrokeWidth($this->borderSize);
if(null !== $this->fillColor) {
$fillColor = new \ImagickPixel( $this->fillColor->getHexString() );
$draw->setFillColor($fillColor);
} else {
$draw->setFillOpacity(0);
}
if(null !== $this->borderColor) {
$borderColor = new \ImagickPixel( $this->borderColor->getHexString() );
$draw->setStrokeColor($borderColor);
} else {
$draw->setStrokeOpacity(0);
}
$draw->polygon($this->points());
$image->getCore()->drawImage($draw);
return $image;
}
protected function points(){
$points = array();
foreach($this->points as $i=>$pos){
$points[$i] = array(
'x' => $pos[0],
'y' => $pos[1]
);
}
if( count($points) < 3 ){
throw new \Exception('Polygon needs at least 3 points.');
}
return $points;
}
}
@@ -0,0 +1,55 @@
<?php
namespace Grafika\Imagick\DrawingObject;
use Grafika\DrawingObject\QuadraticBezier as Base;
use Grafika\DrawingObjectInterface;
use Grafika\Imagick\Image;
use Grafika\ImageInterface;
/**
* Class QuadraticBezier
* @package Grafika
*/
class QuadraticBezier extends Base implements DrawingObjectInterface
{
/**
* @param ImageInterface $image
* @return Image
*/
public function draw($image)
{
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$imagick = $image->getCore();
$draw = new \ImagickDraw();
$strokeColor = new \ImagickPixel($this->getColor()->getHexString());
$fillColor = new \ImagickPixel('rgba(0,0,0,0)');
$draw->setStrokeOpacity(1);
$draw->setStrokeColor($strokeColor);
$draw->setFillColor($fillColor);
list($x1, $y1) = $this->point1;
list($x2, $y2) = $this->control;
list($x3, $y3) = $this->point2;
$draw->pathStart();
$draw->pathMoveToAbsolute($x1, $y1);
$draw->pathCurveToQuadraticBezierAbsolute(
$x2, $y2,
$x3, $y3
);
$draw->pathFinish();
// Render the draw commands in the ImagickDraw object
$imagick->drawImage($draw);
$type = $image->getType();
$file = $image->getImageFile();
return new Image($imagick, $file, $width, $height, $type); // Create new image with updated core
}
}
@@ -0,0 +1,46 @@
<?php
namespace Grafika\Imagick\DrawingObject;
use Grafika\DrawingObject\Rectangle as Base;
use Grafika\DrawingObjectInterface;
/**
* Class Rectangle
* @package Grafika
*/
class Rectangle extends Base implements DrawingObjectInterface{
public function draw( $image ) {
$draw = new \ImagickDraw();
$draw->setStrokeWidth($this->borderSize);
if(null !== $this->fillColor) {
$fillColor = new \ImagickPixel( $this->fillColor->getHexString() );
$draw->setFillColor($fillColor);
} else {
$draw->setFillOpacity(0);
}
if(null !== $this->borderColor) {
$borderColor = new \ImagickPixel( $this->borderColor->getHexString() );
$draw->setStrokeColor($borderColor);
} else {
$draw->setStrokeOpacity(0);
}
$x1 = $this->pos[0];
$x2 = $x1 + $this->getWidth();
$y1 = $this->pos[1];
$y2 = $y1 + $this->getHeight();
$draw->rectangle( $x1, $y1, $x2, $y2 );
$image->getCore()->drawImage($draw);
return $image;
}
}
@@ -0,0 +1,827 @@
<?php
namespace Grafika\Imagick;
use Grafika\DrawingObjectInterface;
use Grafika\EditorInterface;
use Grafika\FilterInterface;
use Grafika\Grafika;
use Grafika\ImageInterface;
use Grafika\ImageType;
use Grafika\Color;
use Grafika\Imagick\ImageHash\DifferenceHash;
use Grafika\Position;
/**
* Imagick Editor class. Uses the PHP Imagick library.
* @package Grafika\Imagick
*/
final class Editor implements EditorInterface
{
/**
* Apply a filter to the image. See Filters section for a list of available filters.
*
* @param Image $image
* @param FilterInterface $filter
*
* @return Editor
*/
public function apply(&$image, $filter)
{
if ($image->isAnimated()) { // Ignore animated GIF for now
return $this;
}
$image = $filter->apply($image);
return $this;
}
/**
* Blend two images together with the first image as the base and the second image on top. Supports several blend modes.
*
* @param Image $image1 The base image.
* @param Image $image2 The image placed on top of the base image.
* @param string $type The blend mode. Can be: normal, multiply, overlay or screen.
* @param float $opacity The opacity of $image2. Possible values 0.0 to 1.0 where 0.0 is fully transparent and 1.0 is fully opaque. Defaults to 1.0.
* @param string $position The position of $image2 on $image1. Possible values top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right and smart. Defaults to top-left.
* @param int $offsetX Number of pixels to add to the X position of $image2.
* @param int $offsetY Number of pixels to add to the Y position of $image2.
*
* @return Editor
* @throws \Exception When added image is outside of canvas or invalid blend type
*/
public function blend(&$image1, $image2, $type='normal', $opacity = 1.0, $position = 'top-left', $offsetX = 0, $offsetY = 0 ){
// Turn into position object
$position = new Position($position, $offsetX, $offsetY);
// Position is for $image2. $image1 is canvas.
list($offsetX, $offsetY) = $position->getXY($image1->getWidth(), $image1->getHeight(), $image2->getWidth(), $image2->getHeight());
// Check if it overlaps
if( ($offsetX >= $image1->getWidth() ) or
($offsetX + $image2->getWidth() <= 0) or
($offsetY >= $image1->getHeight() ) or
($offsetY + $image2->getHeight() <= 0)){
throw new \Exception('Invalid blending. Image 2 is outside the canvas.');
}
// Loop start X
$loopStartX = 0;
$canvasStartX = $offsetX;
if($canvasStartX < 0){
$diff = 0 - $canvasStartX;
$loopStartX += $diff;
}
// Loop start Y
$loopStartY = 0;
$canvasStartY = $offsetY;
if($canvasStartY < 0){
$diff = 0 - $canvasStartY;
$loopStartY += $diff;
}
if ( $opacity !== 1 ) {
$this->opacity($image2, $opacity);
}
$type = strtolower( $type );
if($type==='normal') {
$image1->getCore()->compositeImage($image2->getCore(), \Imagick::COMPOSITE_OVER, $loopStartX + $offsetX, $loopStartY + $offsetY);
} else if($type==='multiply'){
$image1->getCore()->compositeImage($image2->getCore(), \Imagick::COMPOSITE_MULTIPLY, $loopStartX + $offsetX, $loopStartY + $offsetY);
} else if($type==='overlay'){
$image1->getCore()->compositeImage($image2->getCore(), \Imagick::COMPOSITE_OVERLAY, $loopStartX + $offsetX, $loopStartY + $offsetY);
} else if($type==='screen'){
$image1->getCore()->compositeImage($image2->getCore(), \Imagick::COMPOSITE_SCREEN, $loopStartX + $offsetX, $loopStartY + $offsetY);
} else {
throw new \Exception(sprintf('Invalid blend type "%s".', $type));
}
return $this;
}
/**
* Compare two images and returns a hamming distance. A value of 0 indicates a likely similar picture. A value between 1 and 10 is potentially a variation. A value greater than 10 is likely a different image.
*
* @param ImageInterface|string $image1
* @param ImageInterface|string $image2
*
* @return int Hamming distance. Note: This breaks the chain if you are doing fluent api calls as it does not return an Editor.
* @throws \Exception
*/
public function compare($image1, $image2)
{
if (is_string($image1)) { // If string passed, turn it into a Image object
$image1 = Image::createFromFile($image1);
$this->flatten( $image1 );
}
if (is_string($image2)) { // If string passed, turn it into a Image object
$image2 = Image::createFromFile($image2);
$this->flatten( $image2 );
}
$hash = new DifferenceHash();
$bin1 = $hash->hash($image1, $this);
$bin2 = $hash->hash($image2, $this);
$str1 = str_split($bin1);
$str2 = str_split($bin2);
$distance = 0;
foreach ($str1 as $i => $char) {
if ($char !== $str2[$i]) {
$distance++;
}
}
return $distance;
}
/**
* Crop the image to the given dimension and position.
*
* @param Image $image
* @param int $cropWidth Crop width in pixels.
* @param int $cropHeight Crop Height in pixels.
* @param string $position The crop position. Possible values top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right and smart. Defaults to center.
* @param int $offsetX Number of pixels to add to the X position of the crop.
* @param int $offsetY Number of pixels to add to the Y position of the crop.
*
* @return Editor
* @throws \Exception
*/
public function crop(&$image, $cropWidth, $cropHeight, $position = 'center', $offsetX = 0, $offsetY = 0)
{
if ($image->isAnimated()) { // Ignore animated GIF for now
return $this;
}
if ( 'smart' === $position ) { // Smart crop
list( $x, $y ) = $this->_smartCrop( $image, $cropWidth, $cropHeight );
} else {
// Turn into an instance of Position
$position = new Position( $position, $offsetX, $offsetY );
// Crop position as x,y coordinates
list( $x, $y ) = $position->getXY( $image->getWidth(), $image->getHeight(), $cropWidth, $cropHeight );
}
$image->getCore()->cropImage($cropWidth, $cropHeight, $x, $y);
return $this;
}
/**
* Draw a DrawingObject on the image. See Drawing Objects section.
*
* @param Image $image
* @param DrawingObjectInterface $drawingObject
*
* @return $this
*/
public function draw(&$image, $drawingObject)
{
if ($image->isAnimated()) { // Ignore animated GIF for now
return $this;
}
$image = $drawingObject->draw($image);
return $this;
}
/**
* Compare if two images are equal. It will compare if the two images are of the same width and height. If the dimensions differ, it will return false. If the dimensions are equal, it will loop through each pixels. If one of the pixel don't match, it will return false. The pixels are compared using their RGB (Red, Green, Blue) values.
*
* @param string|ImageInterface $image1 Can be an instance of Image or string containing the file system path to image.
* @param string|ImageInterface $image2 Can be an instance of Image or string containing the file system path to image.
*
* @return bool True if equals false if not. Note: This breaks the chain if you are doing fluent api calls as it does not return an Editor.
* @throws \Exception
*/
public function equal($image1, $image2)
{
if (is_string($image1)) { // If string passed, turn it into a Image object
$image1 = Image::createFromFile($image1);
$this->flatten( $image1 );
}
if (is_string($image2)) { // If string passed, turn it into a Image object
$image2 = Image::createFromFile($image2);
$this->flatten( $image2 );
}
// Check if image dimensions are equal
if ($image1->getWidth() !== $image2->getWidth() or $image1->getHeight() !== $image2->getHeight()) {
return false;
} else {
// Loop using image1
$pixelIterator = $image1->getCore()->getPixelIterator();
foreach ($pixelIterator as $row => $pixels) { /* Loop through pixel rows */
foreach ($pixels as $column => $pixel) { /* Loop through the pixels in the row (columns) */
/**
* Get image1 pixel
* @var $pixel \ImagickPixel
*/
$rgba1 = $pixel->getColor();
// Get image2 pixel
$rgba2 = $image2->getCore()->getImagePixelColor($column, $row)->getColor();
// Compare pixel value
if (
$rgba1['r'] !== $rgba2['r'] or
$rgba1['g'] !== $rgba2['g'] or
$rgba1['b'] !== $rgba2['b']
) {
return false;
}
}
$pixelIterator->syncIterator(); /* Sync the iterator, this is important to do on each iteration */
}
}
return true;
}
/**
* Fill entire image with color.
*
* @param Image $image
* @param Color $color Color object
* @param int $x X-coordinate of start point
* @param int $y Y-coordinate of start point
*
* @return self
*/
public function fill(&$image, $color, $x = 0, $y = 0)
{
if ($image->isAnimated()) { // Ignore animated GIF for now
return $this;
}
$target = $image->getCore()->getImagePixelColor($x, $y);
$image->getCore()->floodfillPaintImage($color->getHexString(), 1, $target, $x, $y, false);
return $this;
}
/**
* Flatten if animated GIF. Do nothing otherwise.
*
* @param Image $image
*
* @return self
*/
public function flatten(&$image){
if($image->isAnimated()){
$imagick = $image->getCore()->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
$image = new Image(
$imagick,
$image->getImageFile(),
$image->getWidth(),
$image->getHeight(),
$image->getType(),
'', // blocks
false // animated
);
}
return $this;
}
/**
* Flip or mirrors the image.
*
* @param Image $image
* @param string $mode The type of flip: 'h' for horizontal flip or 'v' for vertical.
*
* @return Editor
* @throws \Exception
*/
public function flip(&$image, $mode){
if ($mode === 'h') {
$image->getCore()->flopImage();
} else if ($mode === 'v') {
$image->getCore()->flipImage();
} else {
throw new \Exception(sprintf('Unsupported mode "%s"', $mode));
}
return $this;
}
/**
* Free the image clearing resources associated with it.
*
* @param Image $image
*
* @return Editor
*/
public function free( &$image )
{
$image->getCore()->clear();
return $this;
}
/**
* Checks if the editor is available on the current PHP install.
*
* @return bool True if available false if not.
*/
public function isAvailable()
{
// First, test Imagick's extension and classes.
if (false === extension_loaded('imagick') ||
false === class_exists('Imagick') ||
false === class_exists('ImagickDraw') ||
false === class_exists('ImagickPixel') ||
false === class_exists('ImagickPixelIterator')
) {
return false;
}
return true;
}
/**
* Sets the image to the specified opacity level where 1.0 is fully opaque and 0.0 is fully transparent.
*
* @param Image $image
* @param float $opacity
*
* @return self
* @throws \Exception
*/
public function opacity(&$image, $opacity)
{
if ($image->isAnimated()) { // Ignore animated GIF for now
return $this;
}
// Bounds checks
$opacity = ($opacity > 1) ? 1 : $opacity;
$opacity = ($opacity < 0) ? 0 : $opacity;
$image->getCore()->setImageOpacity($opacity);
return $this;
}
/**
* Open an image file and assign Image to first parameter.
*
* @param Image $image
* @param string $imageFile
*
* @return Editor
*/
public function open(&$image, $imageFile){
$image = Image::createFromFile( $imageFile );
return $this;
}
/**
* Wrapper function for the resizeXXX family of functions. Resize image given width, height and mode.
*
* @param Image $image
* @param int $newWidth Width in pixels.
* @param int $newHeight Height in pixels.
* @param string $mode Resize mode. Possible values: "exact", "exactHeight", "exactWidth", "fill", "fit".
*
* @return Editor
* @throws \Exception
*/
public function resize(&$image, $newWidth, $newHeight, $mode = 'fit')
{
/*
* Resize formula:
* ratio = w / h
* h = w / ratio
* w = h * ratio
*/
switch ($mode) {
case 'exact':
$this->resizeExact($image, $newWidth, $newHeight);
break;
case 'fill':
$this->resizeFill($image, $newWidth, $newHeight);
break;
case 'exactWidth':
$this->resizeExactWidth($image, $newWidth);
break;
case 'exactHeight':
$this->resizeExactHeight($image, $newHeight);
break;
case 'fit':
$this->resizeFit($image, $newWidth, $newHeight);
break;
default:
throw new \Exception(sprintf('Invalid resize mode "%s".', $mode));
}
return $this;
}
/**
* Resize image to exact dimensions ignoring aspect ratio. Useful if you want to force exact width and height.
*
* @param Image $image
* @param int $newWidth Width in pixels.
* @param int $newHeight Height in pixels.
*
* @return self
*/
public function resizeExact(&$image, $newWidth, $newHeight)
{
$this->_resize($image, $newWidth, $newHeight);
return $this;
}
/**
* Resize image to exact height. Width is auto calculated. Useful for creating row of images with the same height.
*
* @param Image $image
* @param int $newHeight Height in pixels.
*
* @return self
*/
public function resizeExactHeight(&$image, $newHeight)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
$resizeHeight = $newHeight;
$resizeWidth = $newHeight * $ratio;
$this->_resize($image, $resizeWidth, $resizeHeight);
return $this;
}
/**
* Resize image to exact width. Height is auto calculated. Useful for creating column of images with the same width.
*
* @param Image $image
* @param int $newWidth Width in pixels.
*
* @return self
*/
public function resizeExactWidth(&$image, $newWidth)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
$resizeWidth = $newWidth;
$resizeHeight = round($newWidth / $ratio);
$this->_resize($image, $resizeWidth, $resizeHeight);
return $this;
}
/**
* Resize image to fill all the space in the given dimension. Excess parts are cropped.
*
* @param Image $image
* @param int $newWidth Width in pixels.
* @param int $newHeight Height in pixels.
*
* @return self
*/
public function resizeFill(&$image, $newWidth, $newHeight)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
// Base optimum size on new width
$optimumWidth = $newWidth;
$optimumHeight = round($newWidth / $ratio);
if (($optimumWidth < $newWidth) or ($optimumHeight < $newHeight)) { // Oops, where trying to fill and there are blank areas
// So base optimum size on height instead
$optimumWidth = $newHeight * $ratio;
$optimumHeight = $newHeight;
}
$this->_resize($image, $optimumWidth, $optimumHeight);
$this->crop($image, $newWidth, $newHeight); // Trim excess parts
return $this;
}
/**
* Resize image to fit inside the given dimension. No part of the image is lost.
*
* @param Image $image
* @param int $newWidth Width in pixels.
* @param int $newHeight Height in pixels.
*
* @return self
*/
public function resizeFit(&$image, $newWidth, $newHeight)
{
$width = $image->getWidth();
$height = $image->getHeight();
$ratio = $width / $height;
// Try basing it on width first
$resizeWidth = $newWidth;
$resizeHeight = round($newWidth / $ratio);
if (($resizeWidth > $newWidth) or ($resizeHeight > $newHeight)) { // Oops, either with or height does not fit
// So base on height instead
$resizeHeight = $newHeight;
$resizeWidth = $newHeight * $ratio;
}
$this->_resize($image, $resizeWidth, $resizeHeight);
return $this;
}
/**
* Rotate an image counter-clockwise.
*
* @param Image $image
* @param int $angle The angle in degrees.
* @param Color|null $color The Color object containing the background color.
*
* @return EditorInterface An instance of image editor.
*/
public function rotate(&$image, $angle, $color = null)
{
if ($image->isAnimated()) { // Ignore animated GIF for now
return $this;
}
$color = ($color !== null) ? $color : new Color('#000000');
list($r, $g, $b, $alpha) = $color->getRgba();
$image->getCore()->rotateImage(new \ImagickPixel("rgba($r, $g, $b, $alpha)"), $angle * -1);
return $this;
}
/**
* Save the image to an image format.
*
* @param Image $image
* @param string $file File path where to save the image.
* @param null|string $type Type of image. Can be null, "GIF", "PNG", or "JPEG".
* @param null|string $quality Quality of image. Applies to JPEG only. Accepts number 0 - 100 where 0 is lowest and 100 is the highest quality. Or null for default.
* @param bool|false $interlace Set to true for progressive JPEG. Applies to JPEG only.
* @param int $permission Default permission when creating non-existing target directory.
*
* @return Editor
* @throws \Exception
*/
public function save( $image, $file, $type = null, $quality = null, $interlace = false, $permission = 0755)
{
if (null === $type) {
$type = $this->_getImageTypeFromFileName($file); // Null given, guess type from file extension
if (ImageType::UNKNOWN === $type) {
$type = $image->getType(); // 0 result, use original image type
}
}
$targetDir = dirname($file); // $file's directory
if (false === is_dir($targetDir)) { // Check if $file's directory exist
// Create and set default perms to 0755
if ( ! mkdir($targetDir, $permission, true)) {
throw new \Exception(sprintf('Cannot create %s', $targetDir));
}
}
switch (strtoupper($type)) {
case ImageType::GIF :
$image->getCore()->writeImages($file, true); // Support animated image. Eg. GIF
break;
case ImageType::PNG :
// PNG is lossless and does not need compression. Although GD allow values 0-9 (0 = no compression), we leave it alone.
$image->getCore()->setImageFormat($type);
$image->getCore()->writeImage($file);
break;
default: // Defaults to jpeg
$quality = ($quality === null) ? 75 : $quality; // Default to 75 (GDs default) if null.
$quality = ($quality > 100) ? 100 : $quality;
$quality = ($quality <= 0) ? 1 : $quality; // Note: If 0 change it to 1. The lowest quality in Imagick is 1 whereas in GD its 0.
if ($interlace) {
$image->getCore()->setImageInterlaceScheme(\Imagick::INTERLACE_JPEG);
}
$image->getCore()->setImageFormat($type);
$image->getCore()->setImageCompression(\Imagick::COMPRESSION_JPEG);
$image->getCore()->setImageCompressionQuality($quality);
$image->getCore()->writeImage($file); // Single frame image. Eg. JPEG
}
return $this;
}
/**
* Write text to image.
*
* @param Image $image
* @param string $text The text to be written.
* @param int $size The font size. Defaults to 12.
* @param int $x The distance from the left edge of the image to the left of the text. Defaults to 0.
* @param int $y The distance from the top edge of the image to the top of the text. Defaults to 12 (equal to font size) so that the text is placed within the image.
* @param Color $color The Color object. Default text color is black.
* @param string $font Full path to font file. If blank, will default to Liberation Sans font.
* @param int $angle Angle of text from 0 - 359. Defaults to 0.
*
* @return EditorInterface
* @throws \Exception
*/
public function text(&$image, $text, $size = 12, $x = 0, $y = 0, $color = null, $font = '', $angle = 0)
{
if ($image->isAnimated()) { // Ignore animated GIF for now
return $this;
}
$y += $size;
$color = ($color !== null) ? $color : new Color('#000000');
$font = ($font !== '') ? $font : Grafika::fontsDir() . DIRECTORY_SEPARATOR . 'LiberationSans-Regular.ttf';
list($r, $g, $b, $alpha) = $color->getRgba();
// Set up draw properties
$draw = new \ImagickDraw();
// Text color
$draw->setFillColor(new \ImagickPixel("rgba($r, $g, $b, $alpha)"));
// Font properties
$draw->setFont($font);
$draw->setFontSize($size);
// Write text
$image->getCore()->annotateImage(
$draw,
$x,
$y,
$angle,
$text
);
return $this;
}
/**
* Calculate entropy based on histogram.
*
* @param array $hist Histogram returned by Image->histogram
*
* @return float|int
*/
private function _entropy($hist){
$entropy = 0;
$hist_size = array_sum($hist['r']) + array_sum($hist['g']) + array_sum($hist['b']);
foreach($hist['r'] as $p){
$p = $p / $hist_size;
$entropy += $p * log($p, 2);
}
foreach($hist['g'] as $p){
$p = $p / $hist_size;
$entropy += $p * log($p, 2);
}
foreach($hist['b'] as $p){
$p = $p / $hist_size;
$entropy += $p * log($p, 2);
}
return $entropy * -1;
}
/**
* Crop based on entropy.
*
* @param Image $oldImage
* @param $cropW
* @param $cropH
*
* @return array
*/
private function _smartCrop($oldImage, $cropW, $cropH){
$image = clone $oldImage;
$this->resizeFit($image, 30, 30);
$origW = $oldImage->getWidth();
$origH = $oldImage->getHeight();
$resizeW = $image->getWidth();
$resizeH = $image->getHeight();
$smallCropW = round(($resizeW / $origW) * $cropW);
$smallCropH = round(($resizeH / $origH) * $cropH);
$step = 1;
for($y = 0; $y < $resizeH-$smallCropH; $y+=$step){
for($x = 0; $x < $resizeW-$smallCropW; $x+=$step){
$hist[$x.'-'.$y] = $this->_entropy($image->histogram(array(array($x, $y), array($smallCropW, $smallCropH))));
}
if($resizeW-$smallCropW <= 0){
$hist['0-'.$y] = $this->_entropy($image->histogram(array(array(0, 0), array($smallCropW, $smallCropH))));
}
}
if($resizeH-$smallCropH <= 0){
$hist['0-0'] = $this->_entropy($image->histogram(array(array(0, 0), array($smallCropW, $smallCropH))));
}
asort($hist);
end($hist);
$pos = key($hist); // last key
list($x, $y) = explode('-', $pos);
$x = round($x*($origW / $resizeW));
$y = round($y*($origH / $resizeH));
return array($x,$y);
}
/**
* Resize helper function.
*
* @param Image $image
* @param int $newWidth
* @param int $newHeight
*
* @return self
* @throws \Exception
*/
private function _resize(&$image, $newWidth, $newHeight)
{
if ('GIF' == $image->getType()) { // Animated image. Eg. GIF
$imagick = $image->getCore()->coalesceImages();
foreach ($imagick as $frame) {
$frame->resizeImage($newWidth, $newHeight, \Imagick::FILTER_BOX, 1, false);
$frame->setImagePage($newWidth, $newHeight, 0, 0);
}
// Assign new image with frames
$image = new Image($imagick->deconstructImages(), $image->getImageFile(), $newWidth, $newHeight,
$image->getType());
} else { // Single frame image. Eg. JPEG, PNG
$image->getCore()->resizeImage($newWidth, $newHeight, \Imagick::FILTER_LANCZOS, 1, false);
// Assign new image
$image = new Image($image->getCore(), $image->getImageFile(), $newWidth, $newHeight,
$image->getType());
}
}
/**
* Get image type base on file extension.
*
* @param int $imageFile File path to image.
*
* @return ImageType string Type of image.
*/
private function _getImageTypeFromFileName($imageFile)
{
$ext = strtolower((string)pathinfo($imageFile, PATHINFO_EXTENSION));
if ('jpg' == $ext or 'jpeg' == $ext) {
return ImageType::JPEG;
} else if ('gif' == $ext) {
return ImageType::GIF;
} else if ('png' == $ext) {
return ImageType::PNG;
} else {
return ImageType::UNKNOWN;
}
}
}
@@ -0,0 +1,37 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Blurs the image.
*/
class Blur implements FilterInterface{
/**
* @var int
*/
protected $amount;
/**
* Blur constructor.
* @param int $amount The amount of blur to apply. Possible values 1-100.
*/
public function __construct($amount = 1)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$image->getCore()->blurImage(1 * $this->amount, 0.5 * $this->amount);
return $image;
}
}
@@ -0,0 +1,39 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Change the image brightness.
*
* TODO: param checks
*/
class Brightness implements FilterInterface{
/**
* @var int
*/
protected $amount; // -100 >= 0 >= 100
/**
* Brightness constructor.
* @param int $amount The amount of brightness to apply. >= -100 and <= -1 to darken. 0 for no change. >= 1 and <= 100 to brighten.
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$image->getCore()->modulateImage(100 + $this->amount, 100, 100);
return $image;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Change the values for red, green and blue in an image.
*/
class Colorize implements FilterInterface{
/**
* @var int
*/
protected $red; // -100 >= 0 >= 100
/**
* @var int
*/
protected $green; // -100 >= 0 >= 100
/**
* @var int
*/
protected $blue; // -100 >= 0 >= 100
/**
* Colorize constructor.
* @param int $red The amount of red colors. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to add.
* @param int $green The amount of green colors. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to add.
* @param int $blue The amount of blue colors. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to add.
*/
public function __construct($red, $green, $blue)
{
$this->red = intval($red);
$this->green = intval($green);
$this->blue = intval($blue);
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
// normalize colorize levels
$red = $this->normalizeLevel($this->red);
$green = $this->normalizeLevel($this->green);
$blue = $this->normalizeLevel($this->blue);
$qrange = $image->getCore()->getQuantumRange();
$image->getCore()->levelImage(0, $red, $qrange['quantumRangeLong'], \Imagick::CHANNEL_RED);
$image->getCore()->levelImage(0, $green, $qrange['quantumRangeLong'], \Imagick::CHANNEL_GREEN);
$image->getCore()->levelImage(0, $blue, $qrange['quantumRangeLong'], \Imagick::CHANNEL_BLUE);
return $image;
}
private function normalizeLevel($level)
{
if ($level > 0) {
return $level/5;
} else {
return ($level+100)/100;
}
}
}
@@ -0,0 +1,38 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Change the contrast of an image. Contrast is the difference in luminance or colour that makes an object distinguishable.
*/
class Contrast implements FilterInterface{
/**
* @var int
*/
protected $amount; // -100 >= 0 >= 100
/**
* Contrast constructor.
* @param int $amount The amount of contrast to apply. >= -100 and <= -1 to reduce. 0 for no change. >= 1 and <= 100 to increase.
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$image->getCore()->sigmoidalContrastImage($this->amount > 0, $this->amount / 4, 0);
return $image;
}
}
@@ -0,0 +1,168 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Dither image. Dithering will turn the image black and white and add noise.
*/
class Dither implements FilterInterface{
/**
* @var string Dithering algorithm to use.
*/
private $type;
/**
* Dither an image.
*
* @param string $type Dithering algorithm to use. Options: diffusion, ordered. Defaults to diffusion.
*/
public function __construct( $type = 'diffusion' )
{
$this->type = $type;
}
/**
* Apply filter.
*
* @param Image $image
*
* @return Image
* @throws \Exception
*/
public function apply( $image ) {
if ( $this->type === 'ordered' ) {
return $this->ordered( $image );
} else if ( $this->type === 'diffusion' ) {
return $this->diffusion( $image );
}
throw new \Exception( sprintf( 'Invalid dither type "%s".', $this->type ) );
}
/**
* Dither using error diffusion.
*
* @param Image $image
*
* @return Image
*/
private function diffusion( $image ){
$pixels = array();
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
// Loop using image1
$pixelIterator = $image->getCore()->getPixelIterator();
foreach ($pixelIterator as $y => $rows) { /* Loop through pixel rows */
foreach ( $rows as $x => $px ) { /* Loop through the pixels in the row (columns) */
/**
* @var $px \ImagickPixel */
$rgba = $px->getColor();
$gray = round($rgba['r'] * 0.3 + $rgba['g'] * 0.59 + $rgba['b'] * 0.11);
if(isset($pixels[$x][$y])){ // Add errors to color if there are
$gray += $pixels[$x][$y];
}
if ( $gray <= 127 ) { // Determine if black or white. Also has the benefit of clipping excess val due to adding the error
$blackOrWhite = 0;
} else {
$blackOrWhite = 255;
}
$oldPixel = $gray;
$newPixel = $blackOrWhite;
// Current pixel
$px->setColor("rgb($newPixel,$newPixel,$newPixel)");
$qError = $oldPixel - $newPixel; // Quantization error
// Propagate error on neighbor pixels
if ( $x + 1 < $width ) {
$pixels[$x+1][$y] = (isset($pixels[$x+1][$y]) ? $pixels[$x+1][$y] : 0) + ($qError * (7 / 16));
}
if ( $x - 1 > 0 and $y + 1 < $height ) {
$pixels[$x-1][$y+1] = (isset($pixels[$x-1][$y+1]) ? $pixels[$x-1][$y+1] : 0) + ($qError * (3 / 16));
}
if ( $y + 1 < $height ) {
$pixels[$x][$y+1] = (isset($pixels[$x][$y+1]) ? $pixels[$x][$y+1] : 0) + ($qError * (5 / 16));
}
if ( $x + 1 < $width and $y + 1 < $height ) {
$pixels[$x+1][$y+1] = (isset($pixels[$x+1][$y+1]) ? $pixels[$x+1][$y+1] : 0) + ($qError * (1 / 16));
}
}
$pixelIterator->syncIterator(); /* Sync the iterator, this is important to do on each iteration */
}
$type = $image->getType();
$file = $image->getImageFile();
$image = $image->getCore();
return new Image( $image, $file, $width, $height, $type ); // Create new image with updated core
}
/**
* Dither by applying a threshold map.
*
* @param Image $image
*
* @return Image
*/
private function ordered( $image ) {
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
$thresholdMap = array(
array( 15, 135, 45, 165 ),
array( 195, 75, 225, 105 ),
array( 60, 180, 30, 150 ),
array( 240, 120, 210, 90 )
);
// Loop using image1
$pixelIterator = $image->getCore()->getPixelIterator();
foreach ($pixelIterator as $y => $rows) { /* Loop through pixel rows */
foreach ( $rows as $x => $px ) { /* Loop through the pixels in the row (columns) */
/**
* @var $px \ImagickPixel */
$rgba = $px->getColor();
$gray = round($rgba['r'] * 0.3 + $rgba['g'] * 0.59 + $rgba['b'] * 0.11);
$threshold = $thresholdMap[ $x % 4 ][ $y % 4 ];
$oldPixel = ( $gray + $threshold ) / 2;
if ( $oldPixel <= 127 ) { // Determine if black or white. Also has the benefit of clipping excess value
$newPixel = 0;
} else {
$newPixel = 255;
}
// Current pixel
$px->setColor("rgb($newPixel,$newPixel,$newPixel)");
}
$pixelIterator->syncIterator(); /* Sync the iterator, this is important to do on each iteration */
}
$type = $image->getType();
$file = $image->getImageFile();
$image = $image->getCore();
return new Image( $image, $file, $width, $height, $type ); // Create new image with updated core
}
}
@@ -0,0 +1,38 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Performs a gamma correction on an image.
*/
class Gamma implements FilterInterface{
/**
* @var float
*/
protected $amount; // >= 1.0
/**
* Gamma constructor.
* @param float $amount The amount of gamma correction to apply. >= 1.0
*/
public function __construct($amount)
{
$this->amount = (float) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$image->getCore()->gammaImage($this->amount);
return $image;
}
}
@@ -0,0 +1,23 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Turn image into grayscale.
*/
class Grayscale implements FilterInterface{
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$image->getCore()->modulateImage(100, 0, 100);
return $image;
}
}
@@ -0,0 +1,24 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Invert the image colors.
*/
class Invert implements FilterInterface{
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$image->getCore()->negateImage(false);
return $image;
}
}
@@ -0,0 +1,42 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Pixelate an image.
*/
class Pixelate implements FilterInterface{
/**
* @var int $amount Pixelate size from >= 1
*/
protected $amount;
/**
* Pixelate constructor.
* @param int $amount The size of pixelation. >= 1
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$size = $this->amount;
$width = $image->getWidth();
$height = $image->getHeight();
$image->getCore()->scaleImage(max(1, ($width / $size)), max(1, ($height / $size)));
$image->getCore()->scaleImage($width, $height);
return $image;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Sharpen an image.
*/
class Sharpen implements FilterInterface{
/**
* @var int $amount
*/
protected $amount;
/**
* Sharpen constructor.
* @param int $amount Amount of sharpening from >= 1 to <= 100
*/
public function __construct($amount)
{
$this->amount = (int) $amount;
}
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$image->getCore()->unsharpMaskImage(1, 1, $this->amount / 6.25, 0);
return $image;
}
}
@@ -0,0 +1,142 @@
<?php
namespace Grafika\Imagick\Filter;
use Grafika\FilterInterface;
use Grafika\Imagick\Image;
/**
* Sobel filter is an edge detection filter.
* @link https://en.wikipedia.org/wiki/Sobel_operator
*/
class Sobel implements FilterInterface{
/**
* @param Image $image
*
* @return Image
*/
public function apply( $image ) {
$pixels = array();
$finalPx = array();
// Localize vars
$width = $image->getWidth();
$height = $image->getHeight();
// Loop
$pixelIterator = $image->getCore()->getPixelIterator();
foreach ($pixelIterator as $y => $rows) { /* Loop through pixel rows */
foreach ( $rows as $x => $px ) { /* Loop through the pixels in the row (columns) */
// row 0
if ($x > 0 and $y > 0) {
$matrix[0][0] = $this->getColor($px, $pixels, $x - 1, $y - 1);
} else {
$matrix[0][0] = $this->getColor($px, $pixels, $x, $y);
}
if ($y > 0) {
$matrix[1][0] = $this->getColor($px, $pixels, $x, $y - 1);
} else {
$matrix[1][0] = $this->getColor($px, $pixels, $x, $y);
}
if ($x + 1 < $width and $y > 0) {
$matrix[2][0] = $this->getColor($px, $pixels, $x + 1, $y - 1);
} else {
$matrix[2][0] = $this->getColor($px, $pixels, $x, $y);
}
// row 1
if ($x > 0) {
$matrix[0][1] = $this->getColor($px, $pixels, $x - 1, $y);
} else {
$matrix[0][1] = $this->getColor($px, $pixels, $x, $y);
}
if ($x + 1 < $width) {
$matrix[2][1] = $this->getColor($px, $pixels, $x + 1, $y);
} else {
$matrix[2][1] = $this->getColor($px, $pixels, $x, $y);
}
// row 1
if ($x > 0 and $y + 1 < $height) {
$matrix[0][2] = $this->getColor($px, $pixels, $x - 1, $y + 1);
} else {
$matrix[0][2] = $this->getColor($px, $pixels, $x, $y);
}
if ($y + 1 < $height) {
$matrix[1][2] = $this->getColor($px, $pixels, $x, $y + 1);
} else {
$matrix[1][2] = $this->getColor($px, $pixels, $x, $y);
}
if ($x + 1 < $width and $y + 1 < $height) {
$matrix[2][2] = $this->getColor($px, $pixels, $x + 1, $y + 1);
} else {
$matrix[2][2] = $this->getColor($px, $pixels, $x, $y);
}
$edge = $this->convolve($matrix);
$edge = intval($edge / 2);
if ($edge > 255) {
$edge = 255;
}
/**
* @var \ImagickPixel $px Current pixel.
*/
$finalPx[] = $edge; // R
$finalPx[] = $edge; // G
$finalPx[] = $edge; // B
}
$pixelIterator->syncIterator(); /* Sync the iterator, this is important to do on each iteration */
}
$new = new \Imagick();
$new->newImage($width, $height, new \ImagickPixel('black'));
/* Import the pixels into image.
width * height * strlen("RGB") must match count($pixels) */
$new->importImagePixels(0, 0, $width, $height, "RGB", \Imagick::PIXEL_CHAR, $finalPx);
$type = $image->getType();
$file = $image->getImageFile();
return new Image( $new, $file, $width, $height, $type ); // Create new image with updated core
}
private function convolve($matrix)
{
$gx = $matrix[0][0] + ($matrix[2][0] * -1) +
($matrix[0][1] * 2) + ($matrix[2][1] * -2) +
$matrix[0][2] + ($matrix[2][2] * -1);
$gy = $matrix[0][0] + ($matrix[1][0] * 2) + $matrix[2][0] +
($matrix[0][2] * -1) + ($matrix[1][2] * -2) + ($matrix[2][2] * -1);
return sqrt(($gx * $gx) + ($gy * $gy));
}
/**
* @param \ImagickPixel $px
* @param array $pixels
* @param int $x
* @param int $y
*
* @return float
*/
private function getColor($px, &$pixels, $x, $y)
{
if (isset($pixels[$x][$y])) {
return $pixels[$x][$y];
}
$rgba = $px->getColor();
return $pixels[$x][$y] = round($rgba['r'] * 0.3 + $rgba['g'] * 0.59 + $rgba['b'] * 0.11); // gray
}
}
@@ -0,0 +1,269 @@
<?php
namespace Grafika\Imagick;
use Grafika\ImageInterface;
use Grafika\ImageType;
/**
* Image class for Imagick.
* @package Grafika\Gd
*/
final class Image implements ImageInterface {
/**
* @var \Imagick Imagick instance
*/
private $imagick;
/**
* @var string File path to image
*/
private $imageFile;
/**
* @var int Image width in pixels
*/
private $width;
/**
* @var int Image height in pixels
*/
private $height;
/**
* @var string Image type. Return value of Imagick::queryFormats(). See http://phpimagick.com/Imagick/queryFormats
* Sample values: JPEG, PNG, GIF, WBMP
*/
private $type;
/**
* @var bool True if image is an animated GIF.
*/
private $animated;
/**
* Image constructor.
*
* @param \Imagick $imagick
* @param string $imageFile
* @param int $width
* @param int $height
* @param string $type
* @param bool $animated
*/
public function __construct( \Imagick $imagick, $imageFile, $width, $height, $type, $animated = false ) {
$this->imagick = $imagick;
$this->imageFile = $imageFile;
$this->width = $width;
$this->height = $height;
$this->type = $type;
$this->animated = $animated;
}
public function __clone()
{
$copy = clone $this->imagick;
$this->imagick = $copy;
}
/**
* Output a binary raw dump of an image in a specified format.
*
* @param string|ImageType $type Image format of the dump.
*
* @throws \Exception When unsupported type.
*/
public function blob( $type = 'PNG' ) {
$this->imagick->setImageFormat($type);
echo $this->imagick->getImageBlob();
}
/**
* @param $imageFile
*
* @return Image
* @throws \Exception
*/
public static function createFromFile( $imageFile ){
$imageFile = realpath( $imageFile );
if ( ! file_exists( $imageFile ) ) {
throw new \Exception( sprintf('Could not open image file "%s"', $imageFile) );
}
$imagick = new \Imagick( realpath($imageFile) );
$animated = false;
if ($imagick->getImageIterations() > 0) {
$animated = true;
}
return new self(
$imagick,
$imageFile,
$imagick->getImageWidth(),
$imagick->getImageHeight(),
$imagick->getImageFormat(),
$animated
);
}
/**
* Create an Image from an instance of Imagick.
*
* @param \Imagick $imagick Instance of Imagick.
*
* @return Image
*/
public static function createFromCore( $imagick ) {
return new self( $imagick, '', $imagick->getImageWidth(), $imagick->getImageHeight(), $imagick->getImageFormat() );
}
/**
* Create a blank image.
*
* @param int $width Width in pixels.
* @param int $height Height in pixels.
*
* @return self
*/
public static function createBlank($width = 1, $height = 1){
$imagick = new \Imagick();
$imagick->newImage($width, $height, new \ImagickPixel('black'));
$imagick->setImageFormat('png'); // Default to PNG.
return new self( $imagick, '', $imagick->getImageWidth(), $imagick->getImageHeight(), $imagick->getImageFormat());
}
/**
* Get Imagick instance
*
* @return \Imagick
*/
public function getCore() {
return $this->imagick;
}
/**
* Get image file path.
*
* @return string File path to image.
*/
public function getImageFile() {
return $this->imageFile;
}
/**
* Get image width in pixels.
*
* @return int
*/
public function getWidth() {
return $this->width;
}
/**
* Get image height in pixels.
*
* @return int
*/
public function getHeight() {
return $this->height;
}
/**
* Get image type.
*
* @return string
*/
public function getType() {
return $this->type;
}
/**
* Get histogram from an entire image or its sub-region.
*
* @param array|null $slice Array of slice information. array( array( 0,0), array(100,50)) means x,y is 0,0 and width,height is 100,50
*
* @return array Returns array containing RGBA bins array('r'=>array(), 'g'=>array(), 'b'=>array(), 'a'=>array())
*/
public function histogram($slice = null)
{
if(null === $slice){
$sliceX = 0;
$sliceY = 0;
$sliceW = $this->getWidth();
$sliceH = $this->getHeight();
} else {
$sliceX = $slice[0][0];
$sliceY = $slice[0][1];
$sliceW = $slice[1][0];
$sliceH = $slice[1][1];
}
$rBin = array();
$gBin = array();
$bBin = array();
$aBin = array();
// Loop using image
$pixelIterator = $this->getCore()->getPixelIterator();
foreach ($pixelIterator as $y => $rows) { /* Loop through pixel rows */
if($y >= $sliceY and $y < $sliceY+$sliceH) {
foreach ($rows as $x => $px) { /* Loop through the pixels in the row (columns) */
if($x >= $sliceX and $x < $sliceX+$sliceW) {
/**
* @var $px \ImagickPixel */
$pixel = $px->getColor();
$r = $pixel['r'];
$g = $pixel['g'];
$b = $pixel['b'];
$a = $pixel['a'];
if ( ! isset($rBin[$r])) {
$rBin[$r] = 1;
} else {
$rBin[$r]++;
}
if ( ! isset($gBin[$g])) {
$gBin[$g] = 1;
} else {
$gBin[$g]++;
}
if ( ! isset($bBin[$b])) {
$bBin[$b] = 1;
} else {
$bBin[$b]++;
}
if ( ! isset($aBin[$a])) {
$aBin[$a] = 1;
} else {
$aBin[$a]++;
}
}
}
}
}
return array(
'r' => $rBin,
'g' => $gBin,
'b' => $bBin,
'a' => $aBin
);
}
/**
* Returns animated flag.
*
* @return bool True if animated GIF.
*/
public function isAnimated() {
return $this->animated;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Grafika\Imagick\ImageHash;
use Grafika\Imagick\Editor;
use Grafika\Imagick\Image;
/**
* AverageHash
*
* Algorithm:
* Reduce size. Remove high frequencies and detail by shrinking to 8x8 so that there are 64 total pixels.
* Reduce color. The tiny 8x8 picture is converted to a grayscale.
* Average the colors. Compute the mean value of the 64 colors.
* Compute the bits. Each bit is simply set based on whether the color value is above or below the mean.
* Construct the hash. Set the 64 bits into a 64-bit integer. The order does not matter, just as long as you are consistent.
*
* http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
*
* @package Grafika\Imagick\ImageHash
*/
class AverageHash
{
/**
* Generate and get the average hash of the image.
*
* @param Image $image
*
* @return string
*/
public function hash(Image $image)
{
return ''; // TODO: Implementation
}
}
@@ -0,0 +1,67 @@
<?php
namespace Grafika\Imagick\ImageHash;
use Grafika\Imagick\Editor;
use Grafika\Imagick\Image;
/**
* DifferenceHash
*
* Algorithm:
* Reduce size. The fastest way to remove high frequencies and detail is to shrink the image. In this case, shrink it to 9x8 so that there are 72 total pixels.
* Reduce color. Convert the image to a grayscale picture. This changes the hash from 72 pixels to a total of 72 colors.
* Compute the difference. The algorithm works on the difference between adjacent pixels. This identifies the relative gradient direction. In this case, the 9 pixels per row yields 8 differences between adjacent pixels. Eight rows of eight differences becomes 64 bits.
* Assign bits. Each bit is simply set based on whether the left pixel is brighter than the right pixel.
*
* http://www.hackerfactor.com/blog/index.php?/archives/529-Kind-of-Like-That.html
*
* @package Grafika\Imagick\ImageHash
*/
class DifferenceHash
{
/**
* Generate and get the difference hash of image.
*
* @param Image $image
*
* @param Editor $editor
*
* @return string
*/
public function hash($image, $editor)
{
$width = 9;
$height = 8;
$image = clone $image; // Make sure we are working on the clone if Image is passed
$editor->resizeExact($image, $width, $height); // Resize to exactly 9x8
$imagick = $image->getCore();
// Build hash
$hash = '';
for ($y = 0; $y < $height; $y++) {
// Get the pixel value for the leftmost pixel.
$rgba = $imagick->getImagePixelColor(0, $y)->getColor();
$left = floor(($rgba['r'] + $rgba['g'] + $rgba['b']) / 3);
for ($x = 1; $x < $width; $x++) {
// Get the pixel value for each pixel starting from position 1.
$rgba = $imagick->getImagePixelColor($x, $y)->getColor();
$right = floor(($rgba['r'] + $rgba['g'] + $rgba['b']) / 3);
// Each hash bit is set based on whether the left pixel is brighter than the right pixel.
if ($left > $right) {
$hash .= '1';
} else {
$hash .= '0';
}
// Prepare the next loop.
$left = $right;
}
}
$editor->free( $image );
return $hash;
}
}
@@ -0,0 +1,146 @@
<?php
namespace Grafika;
/**
* Hold and computes position of objects added to canvas.
*
* @package Grafika
*/
class Position {
/**
* Top left of the canvas.
*/
const TOP_LEFT = 'top-left';
/**
* Top center of the canvas.
*/
const TOP_CENTER = 'top-center';
/**
* Top right of the canvas.
*/
const TOP_RIGHT = 'top-right';
/**
* Center left of the canvas.
*/
const CENTER_LEFT = 'center-left';
/**
* Center of the canvas.
*/
const CENTER = 'center';
/**
* Center right of the canvas.
*/
const CENTER_RIGHT = 'center-right';
/**
* Center left of the canvas.
*/
const BOTTOM_LEFT = 'bottom-left';
/**
* Bottom center of the canvas.
*/
const BOTTOM_CENTER = 'bottom-center';
/**
* Bottom right of the canvas.
*/
const BOTTOM_RIGHT = 'bottom-right';
/**
* @var string Holds position in human-readable text.
*/
private $position;
/**
* @var int Number of pixels to the left of the origin
*/
private $offsetX;
/**
* @var int Number of pixels to the bottom of the origin.
*/
private $offsetY;
/**
* Position constructor.
*
* @param string $position Defaults to center.
* @param int $offsetX Defaults to 0.
* @param int $offsetY Defaults to 0.
*/
public function __construct($position='center', $offsetX=0, $offsetY=0) {
$this->position = $position;
$this->offsetX = $offsetX;
$this->offsetY = $offsetY;
}
/**
* Translate the textual position + offsets into x,y values.
*
* @param int $canvasWidth Width of canvas.
* @param int $canvasHeight Height of canvas.
* @param int $imageWidth Width of image/object added.
* @param int $imageHeight Height of image/object added.
*
* @return array Array of X and Y coordinates: array($x, $y).
* @throws \Exception When invalid position.
*/
public function getXY($canvasWidth, $canvasHeight, $imageWidth, $imageHeight){
if ( self::TOP_LEFT === $this->position) {
$x = 0;
$y = 0;
} else if ( self::TOP_CENTER === $this->position) {
$x = (int)round(($canvasWidth / 2) - ($imageWidth / 2));
$y = 0;
} else if ( self::TOP_RIGHT === $this->position) {
$x = $canvasWidth - $imageWidth;
$y = 0;
} else if ( self::CENTER_LEFT === $this->position) {
$x = 0;
$y = (int)round(($canvasHeight / 2) - ($imageHeight / 2));
} else if ( self::CENTER_RIGHT === $this->position) {
$x = $canvasWidth - $imageWidth;
$y = (int)round(($canvasHeight / 2) - ($imageHeight / 2));
} else if ( self::BOTTOM_LEFT === $this->position) {
$x = 0;
$y = $canvasHeight - $imageHeight;
} else if ( self::BOTTOM_CENTER === $this->position) {
$x = (int)round(($canvasWidth / 2) - ($imageWidth / 2));
$y = $canvasHeight - $imageHeight;
} else if ( self::BOTTOM_RIGHT === $this->position) {
$x = $canvasWidth - $imageWidth;
$y = $canvasHeight - $imageHeight;
} else if ( self::CENTER === $this->position) {
$x = (int)round(($canvasWidth / 2) - ($imageWidth / 2));
$y = (int)round(($canvasHeight / 2) - ($imageHeight / 2));
} else {
throw new \Exception( sprintf( 'Invalid position "%s".', $this->position ) );
}
return array(
$x + $this->offsetX,
$y + $this->offsetY
);
}
/**
* @return string
*/
public function getText() {
return $this->position;
}
/**
* @return int
*/
public function getOffsetY() {
return $this->offsetY;
}
/**
* @return int
*/
public function getOffsetX() {
return $this->offsetX;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
spl_autoload_register(function ($class) {
if ( 0 === strpos( $class, 'Grafika' ) ) { // Autoload our packages only
$base_dir = __DIR__ . '/';
$file = str_replace('\\', '/', $base_dir . $class . '.php'); // Change \ to /
require_once $file;
}
});