( ′∀`)σ≡σ☆))Д′)レ(゚∀゚;)ヘ=З=З=Зε≡(ノ´_ゝ`)ノ
<?php
/**
* GD2 Imaging (part of Lotos Framework)
*
* Copyright (c) 2005-2011 Artur Graniszewski (aargoth@boo.pl)
* All rights reserved.
*
* @category Library
* @package Lotos
* @subpackage Imaging
* @copyright Copyright (c) 2005-2011 Artur Graniszewski (aargoth@boo.pl)
* @license GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007
* @version 1.7.4
*/
/**
* Pixel shader class.
*/
class Shader
{
/**
* Shader drawing mode: use integer values of imagecolorat() only (fastest mode)
*/
const USE_INT = 1;
/**
* Shader drawing mode: calculate RGB values from integer returned by imagecolorat() (fast mode)
*/
const USE_RGB = 2;
/**
* Shader drawing mode: calculate HSV values from integer returned by imagecolorat() (slowest mode)
* This mode enables USE_RGB automatically.
*/
const USE_HSV = 4;
/**
* Shader drawing mode: Operate on alpha channel.
*/
const USE_ALPHA = 8;
/**
* Shader instructions (PHP code).
*
* @var string
*/
protected $shaderContent = '';
/**
* Shader drawing mode.
*
* @var int
*/
protected $shaderMode = self::USE_HSV;
/**
* Shaders' active area.
*
* @var Point[]
*/
protected $area = null;
/**
* Shader constructor
*
* @param mixed $shaderContent Shader instructions (PHP code).
* @return Shader
*/
public function __construct($shaderContent = '') {
$this->shaderContent = $shaderContent;
}
/**
* Returns the drawing mode (bitmask) of this shader.
*
* @param int $shaderMode Shader drawing mode: Shader::USE_INT, Shader::USE_RGB, Shader::USE_HSV (default), etc.
* @return Shader
*/
public function setMode($shaderMode) {
$this->shaderMode = $shaderMode;
return $this;
}
/**
* Returns the drawing mode (bitmask) of this shader.
*
* @return int
*/
public function getMode() {
return $this->shaderMode;
}
/**
* Sets the position of an active area (shader effects will be applied only to this area).
*
* @param Point $topLeftPosition Top left position of the area.
* @param Point $$bottomRightPosition Bottom right position of the area.
* @return Shader
*/
public function useArea(Point $topLeftPosition, Point $bottomRightPosition) {
if($topLeftPosition->x > $bottomRightPosition->x) {
$tmp = $topLeftPosition->x;
$topLeftPosition->x = $bottomRightPosition->x;
$bottomRightPosition->x = $tmp;
}
if($topLeftPosition->y > $bottomRightPosition->y) {
$tmp = $topLeftPosition->y;
$topLeftPosition->y = $bottomRightPosition->y;
$bottomRightPosition->y = $tmp;
}
$this->area = array($topLeftPosition, $bottomRightPosition);
return $this;
}
/**
* Returns the position of an active area (an array of two points: top left, and bottom right)
*
* @return Point[]
*/
public function getArea() {
return $this->area;
}
/**
* Returns shader instructions (PHP code).
*
* @return string
*/
public function getInstructions() {
return $this->shaderContent;
}
}
/**
* Stores point position.
*/
class Point
{
/**
* Point X coordinate.
*
* @var int
*/
public $x;
/**
* Point Y coordinate.
*
* @var int
*/
public $y;
/**
* Creates Point instance.
*
* @param int $x Point X coordinate.
* @param int $y Point Y coordinate.
* @return Point
*/
public function __construct($x, $y) {
$this->x = $x;
$this->y = $y;
}
/**
* Returns position of the pixel.
*
* @return int[] X and Y coordinates of the pixel.
*/
public function getPosition() {
return array($this->x, $this->y);
}
}
/**
* Describes image color.
*/
class Color
{
const RED = 1;
const GREEN = 2;
const BLUE = 4;
/**
* HSL color model.
*/
const HSL = 8;
/**
* HSV color model.
*/
const HSV = 16;
/**
* HSI color model.
*/
const HSI = 32;
/**
* RGB value of the color.
*
* @var int
*/
public $rgb;
/**
* Creates color instance.
*
* @param int $rgbOrRedValue Full RGB value or Red value (as int)
* @param mixed $greenValue Green value (if $rgbOrRedValue does not contain entire RGB value)
* @param mixed $blueValue Blue value (if $rgbOrRedValue does not contain entire RGB value)
* @return Color
*/
public function __construct($rgbOrRedValue, $greenValue = null, $blueValue = null) {
if($greenValue === null && $blueValue === null) {
$this->rgb = $rgbOrRedValue;
return;
}
$this->rgb = ($rgbOrRedValue << 16) + ($greenValue << 8) + $blueValue;
}
/**
* Calculates a value of a red component of the RGB value.
*
* Note: should not be used for performance reasons (reason PHP < 5.4 functions overhead).
* @return int
*/
public function getRed($rgb) {
return ($this->rgb >> 16) & 0xff;
}
/**
* Calculates a value of a green component of the RGB value.
*
* Note: should not be used for performance reasons (reason PHP < 5.4 functions overhead).
* @return int
*/
public function getGreen() {
return ($this->rgb >> 8) & 0xff;
}
/**
* Calculates a value of a blue component of the RGB value.
*
* Note: should not be used for performance reasons (reason PHP < 5.4 functions overhead).
* @return int
*/
public function getBlue() {
return $this->rgb & 0xff;
}
/**
* Returns color chroma.
*
* @return float
*/
public function getChroma() {
$r = ($this->rgb >> 16) & 0xff;
$g = ($this->rgb >> 8) & 0xff;
$r = $this->rgb & 0xff;
return (max($r, $g, $b) - min($r, $g, $b)) / 255;
}
/**
* Returns color hue.
*
* @return int Value in degrees (0 => 360).
*/
public function getHue() {
$r = (($rgb >> 16) & 0xff) / 255;
$g = (($rgb >> 8) & 0xff) / 255;
$b = ($rgb & 0xff) / 255;
$hue = rad2deg(atan2(1.7320508075688 /* = sqrt(3) */ * ($g - $b), 2 * $r - $g - $b));
return $hue >= 0 ? $hue : 360 + $hue;
}
/**
* Returns color saturation.
*
* @param int $colorMode Color mode for saturation (use Color::HSV, Color::HSI or Color::HSL as the value), default is Color::HSL
* @return float
*/
public function getSaturation($colorMode = self::HSL) {
$r = (($this->rgb >> 16) & 0xff) / 255;
$g = (($this->rgb >> 8) & 0xff) / 255;
$b = ($this->rgb & 0xff) / 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
if($max === 0) {
return 0;
}
if($colorMode === self::HSL) {
$diff = $max - $min;
//$luminance = ($max + $min) / 2;
if($diff < 0.5) {
return $diff / ($max + $min);
} else {
return $diff / (2 - $max - $min);
}
} else if($colorMode === self::HSV) {
return ($max - $min) / $max;
} else if($colorMode === self::HSI) {
if($max - $min === 0) {
return 0;
} else {
return 1 - $min / (($r + $g + $b) / 3);
}
}
throw new Exception('Unknown color mode');
}
/**
* Returns hexadecimal representation of the current color.
*
* @return string
*/
public function getHexValue() {
return str_pad(dechex($this->rgb), 6, '0', STR_PAD_LEFT);
}
/**
* Returns color luminance.
*
* @param int $mode Luminance mode: 0 = fastest, 1 = Digital CCIR601, 2 = Digital ITU-R, 3 = HSP (best quality), Color::HSL = HSL (default), Color::HSV = HSV
* @return float
*/
public function getLuminance($mode = self::HSL) {
$r = ($this->rgb >> 16) & 0xff;
$g = ($this->rgb >> 8) & 0xff;
$b = $this->rgb & 0xff;
switch ($mode) {
case 0:
// fastest, but less accurate.
return (($r + $r + $r + $b + $g + $g + $g + $g) >> 3) / 255;
break;
case 1:
// Digital CCIR601
return (int)(0.299 * $r + 0.587 * $g + 0.114 * $b) / 255;
break;
case 2:
// Ditigal ITU-R
return (int)(0.2126 * $r + 0.7152 * $g + 0.0722 * $b) / 255;
break;
case 3:
// HSP algorithm
return round(sqrt(0.299 * $r * $r + 0.587 * $g * $g + 0.114 * $b * $b)) / 255;
break;
case self::HSL:
// HSL algorithm
return (max($r, $g, $b) + min($r, $g, $b)) / (2 * 255);
break;
case self::HSV:
// HSV algorithm
return max($r, $g, $b) / 255;
break;
case self::HSI:
// HSI algorithm
return ($r + $g + $b) / (3 * 255);
break;
default:
throw new Exception('Unknown color mode');
break;
}
}
}
/**
* Describes area dimensions.
*/
class Dimensions
{
/**
* Width dimension.
*
* @var int
*/
public $width;
/**
* Height dimension.
*
* @var int
*/
public $height;
/**
* Stores area dimensions.
*
* @param int $width Width of the area.
* @param int $height Height of the area.
* @return Dimensions
*/
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
/**
* Returns an array containing width and height of this area.
*
* @return int[]
*/
public function getSize() {
return array($this->width, $this->height);
}
/**
* Multiplies width and height by a given values.
*
* @param int $x Value to multiply a width dimension.
* @param int $y Value to multiply a height dimension.
* @return Dimensions
*/
public function multiply($x, $y) {
$this->width *= $x;
$this->height *= $y;
return $this;
}
/**
* Adds values to width and height respectively.
*
* @param int $x Value to add to a width dimension.
* @param int $y Value to add to a height dimension.
* @return Dimensions
*/
public function add($x = 0, $y = 0) {
$this->width += $x;
$this->height += $y;
return $this;
}
/**
* Returns width of the diagonal line.
*
* @return double
*/
public function getDiagonalWidth() {
return sqrt($this->width * $this->width + $this->height * $this->height);
}
/**
* Returns the size of area.
*
* @return int
*/
public function getAreaSize() {
return $this->width * $this->height;
}
}
/**
* Describes color RGB channels.
*/
class Channel
{
const RED = 1;
const GREEN = 2;
const BLUE = 4;
const RGB = 7;
}
/**
* Describes vector.
*/
class Vector
{
/**
* An array of components of this vector.
*
* @var double[]
*/
protected $components = array();
/**
* A dimension of this vector.
*
* @var int
*/
protected $dimension;
/**
* An identifier of this vector.
*
* @var mixed
*/
protected $identifier = -1;
/**
* A length of this vector.
*
* @var int
*/
protected $length = null;
/**
* Creates new vector.
*
* @param int $dimension Dimension of this vector.
* @param mixed $identifier Identifier of this vector.
* @param double[] $components Components of this vector.
* @return Vector
*/
public function __construct($dimension, $identifier = -1, $components = null) {
$this->dimension = $dimension;
$this->identifier = $identifier;
$this->components = is_array($components) ? $components : array_fill(0, $dimension, 0);
}
/**
* Adds noise to this vector.
*
* @param int $level Noise level.
* @return Vector
*/
public function addNoise($level) {
$components = array();
foreach($this->components as $component) {
$components[] = $component + rand() * 2 * $level - $level;
}
$this->components = $components;
return $this;
}
/**
* Returns the dimension of this vector.
*
* @return int
*/
public function getDimensions() {
return $this->dimension;
}
/**
* Returns the identifier of this vector.
*
* @return mixed
*/
public function getIdentifier() {
return $this->identifier;
}
/**
* Sets the identifier of this vector.
*
* @param string $name
* @return void
*/
public function setIdentifier($name) {
$this->identifier = $name;
}
/**
* Returns components of this vector.
*
* @return double[]
*/
public function toArray() {
return $this->components;
}
/**
* Returns a component of this vector.
*
* @param int $i Index of the component.
* @return double Value of the selected component.
*/
public function getValue($i) {
if($i > $this->dimension) {
throw new Exception();
} else {
return $this->components[$i];
}
}
/**
* Adds two vectors.
*
* @param Vector $v A vector to add.
* @return Vector
*/
public function add(Vector $v) {
if($this->dimension != $v->getDimensions()) {
throw new Exception("Both vectors must have the same size");
}
$components = array();
$otherComponents = $v->toArray();
foreach($this->components as $i => $component) {
$components[] = $component + $otherComponents[$i];
}
$this->components = $components;
return $this;
}
/**
* Multiplies two vectors or by a given scalar value.
*
* @param mixed $v A vector or scalar value to multiply.
* @return Vector
*/
public function multiply($v) {
$components = array();
if(is_object($v)) {
if(!($v instanceof Vector)) {
throw new Exception('Unspupported data structure');
}
if($this->dimension != $v->getDimensions()) {
throw new Exception("Both vectors must have the same size");
}
$otherComponents = $v->toArray();
foreach($this->components as $i => $component) {
$components[] = $component * $otherComponents[$i];
}
} else {
foreach($this->components as $i => $component) {
$components[] = $component * $v;
}
}
$this->components = $components;
return $this;
}
/**
* Negates this vector.
*
* @return Vector
*/
public function negate() {
$components = array();
foreach($this->components as $index => $component) {
$components[$index] = -$component;
}
$this->components = $components;
return $this;
}
/**
* Adds two vectors.
*
* @param Vector $v A vector to add.
* @return Vector
*/
public function substract(Vector $v) {
if($this->dimension != $v->getDimensions()) {
throw new Exception("Both vectors must have the same size");
}
$components = array();
$otherComponents = $v->toArray();
foreach($this->components as $i => $component) {
$components[] = $component - $otherComponents[$i];
}
$this->components = $components;
return $this;
}
/**
* Returns substracted length
*
* @param Vector $v A vector to substract
* @return float
*/
public function getSubstractedLength(Vector $v) {
if($this->dimension != $v->getDimensions()) {
throw new Exception("Both vectors must have the same size");
}
$j = 0;
$otherComponents = $v->toArray();
foreach($this->components as $i => $component) {
$value = $component - $otherComponents[$i];
$j += $value * $value;
}
return sqrt($j);
}
/**
* Compares two vectors.
*
* @param Vector $v
* @return bool True if vectors are equal, false otherwise.
*/
public function equals(Vector $v) {
if($this->dimension != $v->getDimensions()) {
throw new Exception("Both vectors must have the same size");
}
$otherComponents = $v->toArray();
$ret = true;
for($i = 0; $ret && $i < $this->dimension; ++$i) {
$ret = ($this->components[$i] == $otherComponents[$i]);
}
return $ret;
}
/**
* Performs Vector normalization.
*
* @return Vector
*/
public function normalize() {
$j = $this->getLength();
if($j === 0) {
throw new Exception('Cannot normalize zero length vector');
}
$j *= $j;
$components = array();
foreach($this->components as $component) {
$components[] = sqrt(($component * $component) / $j);
}
$this->components = $components;
return $this;
}
/*
* Calculates pythagorean length of a Vector.
*
* @return int
*/
public function getLength() {
if($this->length !== null) {
return $this->length;
}
$j = 0;
foreach($this->components as $component) {
$j += $component * $component;
}
return ($this->length = sqrt($j));
}
/*
* Calculate sum of Vector components.
*
* @return int
*/
public function sum() {
return array_sum($this->components);
}
/**
* Returns copy of this vector.
*
* @return Vector
*/
public function getCopy() {
return new Vector($this->dimension, $this->identifier, $this->components);
}
}
/**
* Performs image quantization.
*/
class Quantizator
{
/**
* An array of glyphs/vectors to compare with.
*
* @var Vector[]
*/
protected $glyphs = array();
/**
* Adds vector to the vectors database.
*
* @param Vector $v Vector to add.
* @param mixed $identifier Vector identifier.
* @return Quantizator
*/
public function addGlyph(Vector $v, $identifier = null) {
$v = $v->getCopy();
$v->setIdentifier($identifier);
$this->glyphs[] = $v;
return $this;
}
/**
* Finds nearest euklid.
*
* @param Vector $v Vector to compare.
* @param int $noise Noise to add (default: 0 for no noise)
* @return mixed[] array containing ID of the similar vector, and it's distance to the compared vector.
*/
public function findNearestEuklid(Vector $v, $noise = 0) {
$minDimension = 1000000;
foreach($this->glyphs as $index => $w) {
$w = $w->getCopy();
if($noise) {
$w = $w->addNoise($noise);
}
$dimension = $w->getSubstractedLength($v);
if($dimension < $minDimension) {
$ret = $w;
$minDimension = $dimension;
}
}
if(!$ret) {
$ret = $w;
}
return array($ret->getIdentifier(), $minDimension);
}
}
/**
* Blender class.
*/
class Blender
{
/**
* Blending mode: addition.
*/
const USE_ADDITION = 1;
/**
* Blending mode: divide.
*/
const USE_DIVIDE = 2;
/**
* Blending mode: subtract.
*/
const USE_SUBTRACT = 4;
/**
* Blending mode: darken.
*/
const USE_DARKEN = 8;
/**
* Blending mode: lighten.
*/
const USE_LIGHTEN = 16;
/**
* Blending mode: dissolve.
*/
const USE_DISSOLVE = 32;
/**
* Blending mode: difference.
*/
const USE_DIFFERENCE = 64;
/**
* Blending mode: multiply.
*/
const USE_MULTIPLY = 128;
/**
* Blending mode: opacity.
*/
const USE_OPACITY = 256;
}
/**
* Allows advanced image processing.
*/
class Image
{
/**
* A GD2 image resource.
*
* @var resource
*/
protected $background;
/**
* A width of this image.
*
* @var int
*/
protected $width;
/**
* A height of this image.
*
* @var int
*/
protected $height;
/**
* Viewport's position in the parent image.
*
* @var mixed
*/
protected $position;
/**
* Viewport's parent image [not implemented]
*
* @var Image
*/
protected $parentImage;
/**
* Not implemented
*
* @var mixed
*/
protected $useBooster;
/**
* Creates new Image.
*
* @param string $fileName Image file name.
* @param resource $background Image data.
* @return Image
*/
public function __construct($fileName, $background = null, Point $position = null, Dimensions $size = null) {
if(!$background && $fileName) {
$background = imagecreatefromstring(file_get_contents($fileName));
}
if($background && !$position) {
$this->image = imagecreatetruecolor(imagesx($background), imagesy($background));
imagecopy($this->image, $background, 0, 0, 0, 0, imagesx($background), imagesy($background));
} else {
$this->image = imagecreatetruecolor($size->width, $size->height);
imagecopy($this->image, $background, 0, 0, $position->x, $position->y, $size->width, $size->height);
$this->position = $position;
}
$this->getSize();
}
/**
* Not implemented.
*
* @param mixed $yes
*/
public function useBooster($yes = false) {
$this->useBooster = (bool) $yes;
}
/**
* Returns next power of two representation of the given number.
*
* @param int $number Number to convert.
* @return int Power of two representation.
*/
protected function getNextPowerOfTwo($number) {
$ret = 1;
while($ret < $number) {
$ret <<= 1;
}
return $ret;
}
/**
* Gets image width.
*
* @return int
*/
public function getWidth() {
return $this->width;
}
/**
* Gets image height.
*
* @return int
*/
public function getHeight() {
return $this->height;
}
/**
* Sets image gamma.
*
* @return Image
*/
public function setGamma($level) {
imagegammacorrect($this->image, 1, $level);
return $this;
}
/**
* Resizes this image.
*
* @param mixed $widthOrDimensions New width (or Dimensions) of the this image.
* @param int $height New height of this image (used only if $widthOrDimensions is not an instance of Dimensions class)
* @param bool Use resampling? Default: true (slower)
* @return Image
*/
public function resize($widthOrDimensions, $height = null, $useResampling = true) {
if(is_object($widthOrDimensions) && $widthOrDimensions instanceof Dimensions) {
$y = $widthOrDimensions->height;
$x = $widthOrDimensions->width;
} else {
$x = $widthOrDimensions;
$y = $height;
}
$newImage = imagecreatetruecolor($x, $y);
if($useResampling) {
imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $x, $y, $this->width, $this->height);
} else {
imagecopyresized($newImage, $this->image, 0, 0, 0, 0, $x, $y, $this->width, $this->height);
}
$this->image = $newImage;
$this->getSize();
return $this;
}
/**
* Resizes this image and keeps the image aspect.
*
* @param mixed $widthOrDimensions New width (or Dimensions) of the this image.
* @param int $height New height of this image (used only if $widthOrDimensions is not an instance of Dimensions class)
* @param bool Use resampling? Default: true (slower)
* @return Image
*/
public function resizeAndKeepAspect($widthOrDimensions, $height = null, $useResampling = true) {
if(is_object($widthOrDimensions) && $widthOrDimensions instanceof Dimensions) {
$y = $widthOrDimensions->height;
$x = $widthOrDimensions->width;
} else {
$x = $widthOrDimensions;
$y = $height;
}
$originalAspect = $this->width / $this->height;
$newAspect = $x / $y;
if($originalAspect !== $newAspect) {
if($originalAspect > $newAspect) {
$ratio = $x / $this->width;
$y = $ratio * $this->height;
} else {
$ratio = $y / $this->height;
$x = $ratio * $this->width;
}
}
return $this->resize($x, $y, $useResampling);
}
/**
* Scales this image.
*
* @return Image
*/
public function rescale($x, $y) {
if(is_object($x) && $x instanceof Dimensions) {
$y = $x->height;
$x = $x->width;
}
$newImage = imagecreatetruecolor($this->width * $x, $this->height * $y);
$x = imagesx($newImage);
$y = imagesy($newImage);
imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $x, $y, $this->width, $this->height);
$this->image = $newImage;
$this->getSize();
return $this;
}
/**
* Returns a RGB value of the pixel at the X,Y coordinates.
*
* @param int $x X-coordinate of the pixel to check.
* @param int $y Y-coordinate of the pixel to check.
* @return int RGB value of the pixel.
*/
public function getPixelRgb($x, $y) {
return imagecolorat($this->image, $x, $y);
}
/**
* Returns a color of the pixel at the X,Y coordinates.
*
* @param int $x X-coordinate of the pixel to check.
* @param int $y Y-coordinate of the pixel to check.
* @return Color Object describing the color of the pixel.
*/
public function getPixelColor($x, $y) {
return new Color(imagecolorat($this->image, $x, $y));
}
/**
* Returns a RGB value of the pixel at the coordinates described in the Point object.
*
* @param Point $position Position of the pixel to check.
* @return int RGB value of the pixel.
*/
public function getPointRgb(Point $position) {
return imagecolorat($this->image, $position->x, $position->y);
}
/**
* Returns a color of the pixel at the coordinates described in the Point object.
*
* @param Point $position Position of the pixel to check.
* @return Color Object describing the color of the pixel.
*/
public function getPointColor(Point $position) {
return new Color(imagecolorat($this->image, $position->x, $position->y));
}
/**
* Sets a color of the pixel at the coordinates described in the Point object.
*
* @param Point $position Position of the pixel to set.
* @return Image
*/
public function setPointColor(Position $position, Color $color) {
imagesetpixel($this->image, $position->x, $position->y, $color->rgb);
return $this;
}
/**
* Sets a RGB value of the pixel at the coordinates described in the Point object.
*
* @param Point $position Position of the pixel to set.
* @return Image
*/
public function setPointRgb(Position $position, $rgb) {
imagesetpixel($this->image, $position->x, $position->y, $rgb);
return $this;
}
/**
* Sets a color of the pixel at the X,Y coordinates.
*
* @param int $x X-coordinate of the pixel to set.
* @param int $y Y-coordinate of the pixel to set.
* @return Image
*/
public function setPixelColor($x, $y, Color $color) {
imagesetpixel($this->image, $x, $y, $color->rgb);
return $this;
}
/**
* Sets a RGB value of the pixel at the X,Y coordinates.
*
* @param int $x X-coordinate of the pixel to set.
* @param int $y Y-coordinate of the pixel to set.
* @return Image
*/
public function setPixelRgb($x, $y, $rgb) {
imagesetpixel($this->image, $x, $y, $rgb);
return $this;
}
/**
* Returns copy of this image.
*
* @return Image
*/
public function getCopy() {
return new Image(null, $this->image, null, $this->getDimensions());
}
/**
* Converts this image to grayscale.
*
* @return Image
*/
public function toGrayScale() {
imagefilter($this->image, IMG_FILTER_GRAYSCALE);
return $this;
}
/**
* Returns a sub image of this image.
*
* @param Point $position
* @param Dimensions $size
* @return Image
*/
public function getSubImage(Point $position, Dimensions $size) {
return new Image(null, $this->image, $position, $size);
}
/**
* Creates a vector from a part of an image.
*
* @param Point $position X,Y-coordinates of the left upper-most point of the image.
* @param Dimensions $size Width and height of the image.
* @param bool $round Round the values?
* @param int $colorMask Color bitmask.
* @return Vector
*/
public function getVector(Point $position = null, Dimensions $size = null, $round = false, $colorMask = 0) {
if($position === null) {
$position = new Point(0, 0);
}
if($size === null) {
$size = $this->getDimensions();
}
$x = $position->x;
$y = $position->y;
$width = $size->width;
$height = $size->height;
$components = array();
$d = 0;
$maxX = $x + $width;
$maxY = $y + $height;
for($i = $y; $i < $maxY && $i < $this->height; ++$i) {
for($j = $x; $j < $maxX && $j < $this->width; ++$j) {
$rgb = imagecolorat($this->image, $j, $i);
if($colorMask === 0) {
$pixel = ($rgb >> 16) & 0xff;
$pixel += ($rgb >> 8) & 0xff;
$pixel += $rgb & 0xff;
$d = $pixel / 100 ;
}
else {
$count = $d = 0;
if(($colorMask & Channel::RED) > 0) {
$d += ($rgb >> 16) & 0xff;
++$count;
}
if(($colorMask & Channel::GREEN) > 0) {
$d += ($rgb >> 8) & 0xff;
++$count;
}
if(($colorMask & Channel::BLUE) > 0) {
$d += $rgb & 0xff;
++$count;
}
$d /= (256 * $count);
}
if($round === false) {
$components[] = 1 - $d;
} else {
$components[] = 1 - round($d);
}
}
}
return new Vector(count($components), -1, $components);
}
/**
* Returns viewport's position.
*
* @return Position
*/
public function getPosition() {
return $this->position;
}
/**
* Rotates image by a given angle.
*
* @param int $angle Angle in degrees (not radians).
* @param int $backgroundColor Backround color used to fill empty spaces after rotation (or -1 for autodetection).
* @return Image
*/
public function rotate($angle, $backgroundColor = -1) {
if($backgroundColor === -1) {
$backgroundColor = hexdec($this->getBackgroundColor()->getHexValue());
}
$this->image = imagerotate($this->image, $angle, (int) $backgroundColor);
$this->getSize();
return $this;
}
/**
* Finds different shapes in the image.
*
* @param int $sortMode
* @return Image[]
*/
public function findObjects($sortMode = 1) {
$background = $this->getCopy();
$background->toBinary();
$gd = $background->getGd2Handle();
$objects = array();
do {
$object = imagecreatetruecolor($this->width, $this->height);
imagefill($object, 0, 0, 0xffffff);
$stop = false;
for($y = 0; $y <$this->height && !$stop; ++$y) {
for($x = 0; $x < $this->width && !$stop; ++$x) {
if(imagecolorat($gd, $x, $y) > 0) {
imagesetpixel($gd, $x, $y, 0);
imagesetpixel($object, $x, $y, 0xff0000);
$minX = $x;
$minY = $y;
$maxX = $x;
$maxY = $y;
$stop = true;
}
}
}
if(!$stop) {
continue;
}
do {
$colors = 0;
for($x = ($minX > 0 ? $minX : 0); $x < ($maxX + 1 < $this->width ? $maxX + 1 : $this->width); ++$x) {
for($y = ($minY > 0 ? $minY : 0); $y < ($maxY + 1 < $this->height ? $maxY + 1 : $this->height); ++$y) {
if(imagecolorat($object, $x, $y) === 0xff0000) {
++$colors;
$found = false;
for($i = ($x - 1 < 0 ? 0 : $x - 1), $maxI = ($x + 2 < $this->width ? $x + 2 : $this->width); $i < $maxI; ++$i) {
for($j = ($y - 1 < 0 ? 0 : $y - 1), $maxJ = ($y + 2 < $this->height ? $y + 2: $this->height); $j < $maxJ; ++$j) {
if(imagecolorat($gd, $i, $j) > 0) {
imagesetpixel($gd, $i, $j, 0);
imagesetpixel($object, $i, $j, 0xff0000);
if($i < $minX) {
$minX = $i;
$x = ($minX - 1 > 0 ? $minX - 1 : 0);
} else if($i > $maxX) {
$maxX = $i;
}
if($j < $minY) {
$minY = $j;
$y = $x = ($minY - 1 > 0 ? $minY - 1 : 0);
} else if($j > $maxY) {
$maxY = $j;
}
$found = true;
}
}
}
if(!$found) {
imagesetpixel($object, $x, $y, 0x00ff00);
}
}
}
}
} while($colors > 0);
$background = new Image(null, $object, new Point($minX, $minY), new Dimensions(1 + $maxX - $minX, 1 + $maxY - $minY));
$background = $background->toBinary();
if($sortMode === 1) {
$objects[$minX * 100000 + $minY] = $background;
} else {
$objects[$minY * 100000 + $minX] = $background;
}
} while($stop);
ksort($objects, SORT_NUMERIC);
$results = array();
foreach($objects as $object) {
$results[] = $object;
}
return $results;
}
/**
* Returns image dimensions.
*
* @return Dimensions
*/
public function getDimensions() {
$this->getSize();
return new Dimensions($this->width, $this->height);
}
/**
* Crops current image.
*
* @param Point $position Position of the upper left point of the area to crop.
* @param Dimensions $dimensions Size of the area to crop.
* @return Image
*/
public function crop(Point $position, Dimensions $dimensions) {
$newImage = imagecreate($dimensions->width, $dimensions->height);
imagecopy($newImage, $this->image, 0, 0, $position->x, $position->y, $dimensions->width, $dimensions->height);
$this->image = $newImage;
$this->getSize();
$this->position = $position;
return $this;
}
/**
* Experimental functionality, do not use!.
*
* @param Image $otherImage
* @param mixed $minAngle
* @param mixed $maxAngle
* @param mixed $backgroundColor
* @param mixed $minDimension
*/
private function tiltAndCompare(Image $otherImage, $minAngle = -20, $maxAngle = 20, $backgroundColor = 0xffffff, &$minDimension) {
if($minAngle > $maxAngle) {
$tmp = $maxAngle;
$maxAngle = $minAngle;
$minAngle = $tmp;
}
$quantization = new Quantization();
$vector = $otherImage->getVector(new Point(0, 0), $otherImage->getDimensions(), false, 0);
//$vector->normalize();
$quantization->addGlyph($vector, 1);
$otherImageDimensions = $otherImage->getDimensions();
$foundObject = $this;
$minDimension = 1000000000000;
for($angle = $minAngle; $angle <= $maxAngle; $angle += 1) {
$copy = $this->getCopy();
$copy->rotate($angle, $backgroundColor);
// find object in object - this is a smart autocropping of binary images.
//$objects = $copy->findObjects();
$objects[0] = $copy;
// we want only the first detected object (only one should be detected anyway!;)
list($width, $height) = $objects[0]->getSize();
// object must be resized before quantization in order to match the other image dimensions.
$objects[0]->resize($otherImageDimensions, null, false)->toBinary();
$vector = $objects[0]->getVector(new Point(0, 0), $otherImageDimensions, false, 0);
//$vector->normalize();
$result = $quantization->findNearestEuklid($vector);
if($result[1] < $minDimension) {
$minDimension = $result[1];
$foundObject = $objects[0];
}
}
return $foundObject;
}
/**
* Reverses all colors of the image.
*
* @return Image
*/
public function toNegative() {
imagefilter($this->image, IMG_FILTER_NEGATE);
return $this;
}
/**
* Returns a number of bytes used to store a single row of the image binary buffer.
*
* For example:
* 128 pixels = 16 bytes,
* 127 pixels = 16 bytes (127 bits of image + 1 bits of padding),
* 129 pixels = 17 bytes (129 bits of image + 7 bits of padding)
*
* @return int Number of bytes per image row.
*/
protected function getByteWidth() {
return (int)(($this->width + 7) / 8);
}
/**
* Displays the image.
*
* @param bool $die Die after displaying an image? Default: false
* @return Image
*/
public function show($die = false) {
header('Content-Type: image/jpeg');
imagejpeg($this->image,null,30);
if($die) {
die();
}
return $this;
}
/**
* Saves this image to the disk.
*
* @param string $fileName File name.
* @return Image
*/
public function saveAsPng($fileName) {
imagepng($this->image, $fileName);
return $this;
}
/**
* Saves this image to the disk.
*
* @param string $fileName File name.
* @return Image
*/
public function saveAsGif($fileName) {
imagegif($this->image, $fileName);
return $this;
}
/**
* Saves this image to the disk.
*
* @param string $fileName File name.
* @return Image
*/
public function saveAsJpeg($fileName) {
imagejpeg($this->image, $fileName,100);
return $this;
}
/**
* Deskews the image.
*
* @param int $backgroundColor Color of the background to use, or -1 (default) to use auto detection.
* @param bool $precisionLevel Set true or 1 to enable turbo mode (may be less accurate) or use 2-4 values to increase the deskew precision.
* @param bool $debugMode Set true to enable the debug mode.
* @return Image
*/
public function deskew($backgroundColor = -1, $precisionLevel = true, $debugMode = false) {
// detect the background color
if($backgroundColor < 0) {
$backgroundColor = hexdec($this->getBackgroundColor(1, $precisionLevel === true)->getHexValue());
}
// calculate a Hough matrix and get the skew angle
if($precisionLevel === false || $precisionLevel === 2) {
$precisionLevel = 256;
} else if($precisionLevel === 3) {
$precisionLevel = 512;
} else if($precisionLevel === 4) {
$precisionLevel = 1024;
} else {
$precisionLevel = 128;
}
$angles = $this->getSkewAngle($debugMode ? 1 : 8, $precisionLevel, $debugMode);
$this->rotate($angles[0]['degrees'], $backgroundColor);
return $this;
}
/**
* Calculates the size of this image.
*
* @return int[] An array containing a width and height of this image.
*/
public function getSize() {
list($this->width, $this->height) = $result = array(imagesx($this->image), imagesy($this->image));
return $result;
}
/**
* Detects the skew angle of this image using the Hough normal transform.
*
* @param int $numberOfSamples Number of different angles to detect (sorted by a number of votes).
* @param int $matrixSize Size of the pixel matrix used for detection, bigger matrix = slower, more accurate detection.
* @param bool $debugMode Draw detected Hough lines on this image? Works only in $returnMode = 1, default: false
* @param int $returnMode Return mode: 1 = array of detected angles (default), 2 = rendered transform results (Image instance), 3 = array of transform results
* @return resource
*/
public function getSkewAngle($numberOfSamples = 1, $matrixSize = 128, $debugMode = false, $returnMode = 1) {
// make a copy of the original image
if(!$debugMode) {
$copy = imagecreatetruecolor($this->width, $this->height);
imagecopy($copy, $this->image, 0, 0, 0, 0, $this->width, $this->height);
}
// create a cache of sinus and cosinus values.
static $sin = array();
static $cos = array();
// initialize Hough matrix
$matrix = array();
// initialize angle deviation
$maxDeviation = 2;
// initialize byte offset in the image frame buffer
$offset = 0;
// initialize bits offset in the image frame buffer
$bits = 0;
// get binary buffer of this image
$bin = $this->createImageMatrix($matrixSize, $ratio, $debugMode);
// get size of binary buffer
$length = strlen($bin);
// calculate padding of this image
$padding = $this->getNextPowerOfTwo($this->width);
// get the centre of this image.
$sizeX2 = (int)($this->width / 2);
$sizeY2 = (int)($this->height / 2);
if(!$debugMode) {
$this->image = $copy;
}
// calculate the maximal distance from the centre of this image.
$rMax = (int) sqrt($sizeX2 * $sizeX2 + $sizeY2 * $sizeY2);
$rMin = -$rMax;
// create an empty row used to fill (initialize) all rows in Hough matrix.
$emptyRow = array_fill(0, $rMax + 1, 0);
// initialize (fill) Hough matrix with empty values and precalculate sinus/cosinus values if necessary.
// precalculate sinus/cosinus only when necessary
if(!isset($sin[0])) {
for($i = 0; $i < 180; ++$i) {
// loop is unwinded for performance reasons (it is 30% faster, than $i < 360)
$matrix[$i] = $emptyRow;
$matrix[180 + $i] = $emptyRow;
// calculate a sinus value in the first half of the circle
$sin[$i] = sin(deg2rad($i));
// calculate a cosinus value using the fast conversion from sinus
if($i >= 90) {
$cos[$i - 90] = $sin[$i];
} else {
$cos[$i + 90] = -$sin[$i];
}
}
}
// create Hough matrix
do {
// get the 8bit representation of the current byte in framebuffer.
$byte = ord($bin[$offset]);
++$offset;
$bits += 8;
if($byte === 0) {
continue;
}
// ignore null bytes in the framebuffer (there are no pixels inside of them)
// this allows us to quickly skip 8 iterations every time the null byte was detected.
// calculate current x, y position in the image frame buffer.
$y = (int)($bits / $padding);
$x = $bits - $padding * $y;
// precalculate some variables used in the next loop.
$y2 = $sizeY2 - $y;
$x1 = $x;
// check every bit in the current byte of framebuffer.
// every byte in framebuffer stores the information about 8 neighbouring pixels in the image.
// bit test: 0 - background, 1 = pixel is set.
do {
// use the bit mask to check if the selected bit is set.
if($mask = ($byte & (0x80 >> ($x - $x1)))) {
// bit is set, remove it from the byte, so this loop can end as soon as no more bits are present.
$byte ^= $mask;
// precalculate some variables used in the next loop.
$x2 = $sizeX2 - $x;
$i = -1;
// count votes for every angle (0->360') generated at this point (x2,y2).
do {
// loop is unwinded for performance reasons (it is 60% faster, than $i < 360)
// we are making four different tests instead of one during every loop iteration
// what's more, we are using only 50% less index lookups in the sin/cos arrays.
$r = $x2 * $cos[++$i] + $y2 * $sin[$i];
if($r >= $rMin && $r <= 0) {
++$matrix[$i][(int)($r - $rMin)];
} else if($r <= $rMax && $r >= 0) {
++$matrix[180 + $i][(int)($rMax - $r)];
}
$r = $x2 * $cos[++$i] + $y2 * $sin[$i];
if($r >= $rMin && $r <= 0) {
++$matrix[$i][(int)($r - $rMin)];
} else if($r <= $rMax && $r >= 0) {
++$matrix[180 + $i][(int)($rMax - $r)];
}
} while($i < 179);
}
++$x;
} while($byte);
} while ($offset < $length);
// mode #3: return Hough matrix as an array
if($returnMode === 3) {
return $matrix;
}
// mode #2: return Hough matrix as a rendered image.
if($returnMode === 2) {
$background = imagecreatetruecolor($rangeOfTheta, $rMax + 1);
for($i = 0; $i < $rangeOfTheta; ++$i) {
for($r = 0; $r < $rMax; ++$r) {
imagesetpixel($background, $i, $ratio * $r, $matrix[$i][$r]);
}
}
return new Image(false, $background);
}
// mode #1 (default): return an array of angles and their distances.
if($returnMode === 1) {
$results = array();
// get size of the matrix.
$width = count($matrix);
$height = count($matrix[0]);
for($l = 0; $l < $width; ++$l) {
$matrix[$l] = array_filter($matrix[$l]);
}
// calculate angles
for($i = 0; $i < $numberOfSamples; ++$i) {
$val = -1;
// find the brightest point on the Hough matrix.
for($l = 0; $l < $width; ++$l) {
foreach($matrix[$l] as $m => $level) {
if($val < $level) {
$val = $level;
$x = $l;
$y = $m;
}
}
}
if($val === -1) {
// there is nothing left (rather strange case).
break;
}
// calculate distance.
$r = $rMax - $y;
// add this sample to the results array
$results[$i] = array('degrees' => $x, 'radians' => deg2rad($x), 'distance' => $r);
// draw Hough lines if necessary
if($debugMode) {
$this->drawHoughLine($results[$i]['distance'], $results[$i]['degrees']);
}
if($numberOfSamples > 1) {
// clear the matrix around and the brightest point.
// so they wont be detected the during next iteration
$maxK = min($x + 10, $width);
$maxJ = min($y + 10, $height);
$minJ = max($y - 10, 0);
$minK = max($x - 10, 0);
for ($k = $minK; $k < $maxK; ++$k){
for($j = $minJ; $j < $maxJ; ++$j){
unset($matrix[$k][$j]);
}
}
}
}
return $results;
}
}
/**
* Detects the skew angle of this image using the Hough normal transform.
*
* @param int $numberOfSamples Number of different angles to detect (sorted by a number of votes).
* @param int $matrixSize Size of the pixel matrix used for detection, bigger matrix = slower, more accurate detection.
* @param bool $debugMode Draw detected Hough lines on this image? Works only in $returnMode = 1, default: false
* @param int $returnMode Return mode: 1 = array of detected angles (default), 2 = rendered transform results (Image instance), 3 = array of transform results
* @return resource
*/
public function getSkewAngle2($numberOfSamples = 1, $matrixSize = 128, $debugMode = false, $returnMode = 1) {
// make a copy of the original image
if(!$debugMode) {
$copy = imagecreatetruecolor($this->width, $this->height);
imagecopy($copy, $this->image, 0, 0, 0, 0, $this->width, $this->height);
}
// create a cache of sinus and cosinus values.
static $sin = array();
static $cos = array();
// initialize Hough matrix
$matrix = array();
// initialize angle deviation
$maxDeviation = 2;
// initialize byte offset in the image frame buffer
$offset = -1;
// initialize bits offset in the image frame buffer
$bits = -8;
// get binary buffer of this image
$bin = $this->createImageMatrix($matrixSize, $ratio, $debugMode);
// get size of binary buffer
$length = strlen($bin) - 1;
// calculate padding of this image
$padding = $this->getNextPowerOfTwo($this->width);
// get the centre of this image.
$sizeX2 = (int)($this->width / 2);
$sizeY2 = (int)($this->height / 2);
if(!$debugMode) {
$this->image = $copy;
}
// calculate the maximal distance from the centre of this image.
$rMax = (int) sqrt($sizeX2 * $sizeX2 + $sizeY2 * $sizeY2);
$rMin = -$rMax;
// create an empty row used to fill (initialize) all rows in Hough matrix.
$emptyRow = array_fill(0, $rMax + 1, 0);
// initialize (fill) Hough matrix with empty values and precalculate sinus/cosinus values if necessary.
// precalculate sinus/cosinus only when necessary
if(!isset($sin[0])) {
for($i = 0; $i < 180; ++$i) {
// loop is unwinded for performance reasons (it is 30% faster, than $i < 360)
$matrix[$i] = $emptyRow;
$matrix[180 + $i] = $emptyRow;
// calculate a sinus value in the first half of the circle
$sin[$i] = sin(deg2rad($i));
// calculate a cosinus value using the fast conversion from sinus
if($i >= 90) {
$cos[$i - 90] = $sin[$i];
} else {
$cos[$i + 90] = -$sin[$i];
}
}
}
// create Hough matrix
do {
$bits += 8;
// ignore null bytes in the framebuffer (there are no pixels inside of them)
// this allows us to quickly skip 8 iterations every time the null byte was detected.
if($bin[++$offset] === "\0") {
continue;
}
// calculate current x, y position in the image frame buffer.
$y = (int)($bits / $padding);
$x = $bits - $padding * $y;
// precalculate some variables used in the next loop.
$y2 = $sizeY2 - $y;
$x1 = $x;
// get the 8bit representation of the current byte in framebuffer.
$byte = ord($bin[$offset]);
// check every bit in the current byte of framebuffer.
// every byte in framebuffer stores the information about 8 neighbouring pixels in the image.
// bit test: 0 - background, 1 = pixel is set.
$x2 = $sizeX2;
do {
// use the bit mask to check if the selected bit is set.
if($mask = ($byte & 0x80 >> $x - $x1)) {
// bit is set, remove it from the byte, so this loop can end as soon as no more bits are present.
$byte ^= $mask;
// precalculate some variables used in the next loop.
$x2 -= $x;
$i = -1;
// count votes for every angle (0->360') generated at this point (x2, y2).
do {
// loop is unwinded for performance reasons (it is 60% faster, than $i < 360)
// we are making four different tests instead of one during every loop iteration
// what's more, we are using only 50% less index lookups in the sin/cos arrays.
$r = $x2 * $cos[++$i] + $y2 * $sin[$i];
if($r >= $rMin && $r <= 0) {
++$matrix[$i][(int)($r - $rMin)];
} else if($r <= $rMax && $r >= 0){
++$matrix[180 + $i][(int)($rMax - $r)];
}
$r = $x2 * $cos[++$i] + $y2 * $sin[$i];
if($r >= $rMin && $r <= 0) {
++$matrix[$i][(int)($r - $rMin)];
} else if($r <= $rMax && $r >= 0){
++$matrix[180 + $i][(int)($rMax - $r)];
}
} while($i < 179);
}
++$x;
} while($byte);
} while ($offset < $length);
// mode #3: return Hough matrix as an array
if($returnMode === 3) {
return $matrix;
}
// mode #2: return Hough matrix as a rendered image.
if($returnMode === 2) {
$background = imagecreatetruecolor($rangeOfTheta, $rMax + 1);
for($i = 0; $i < $rangeOfTheta; ++$i) {
for($r = 0; $r < $rMax; ++$r) {
imagesetpixel($background, $i, $ratio * $r, $matrix[$i][$r]);
}
}
return new Image(false, $background);
}
// mode #1 (default): return an array of angles and their distances.
if($returnMode === 1) {
$results = array();
// get size of the matrix.
$width = count($matrix);
$height = count($matrix[0]);
// calculate angles
for($i = 0; $i < $numberOfSamples; ++$i) {
$val = -1;
// find the brightest point on the Hough matrix.
for($l = 0; $l < $width; ++$l) {
for($m = 0; $m < $height; ++$m) {
if($val < $matrix[$l][$m]) {
$val = $matrix[$l][$m];
$x = $l;
$y = $m;
}
}
}
if($val === -1) {
// there is nothing left (rather strange case).
break;
}
// calculate distance.
$r = -($y - $rMax);
// add this sample to the results array
$results[$i] = array('degrees' => $x, 'radians' => deg2rad($x), 'distance' => $r);
// draw Hough lines if necessary
if($debugMode) {
$this->drawHoughLine($results[$i]['distance'], $results[$i]['degrees']);
}
if($numberOfSamples > 1) {
// clear the matrix around and the brightest point.
// so they wont be detected the during next iteration
$maxK = min($x + 10, $width - 1);
$maxJ = min($y + 10, $height - 1);
for ($k = max($x - 10, 0); $k <= $maxK; ++$k){
for($j = max($y - 10, 0); $j <= $maxJ; ++$j){
$matrix[$k][$j] = 0;
}
}
}
}
return $results;
}
}
/**
* Draws the Hough line.
*
* @param int $distance Distance from the origin.
* @param int $theta Angle in degrees.
* @return Image
*/
protected function drawHoughLine($distance, $theta, $color = 111111) {
if($theta === 0 || $theta === 360) {
return $this;
}
$theta = deg2rad($theta);
$sizeX2 = $this->width / 2;
$sizeY2 = $this->height / 2;
for($x = 0; $x< $this->width; ++$x) {
$y1 = (int)($distance / sin($theta) - ($x - $sizeX2) * (cos($theta) / sin($theta)) + $sizeY2);
$x1 = (int)($distance / cos($theta) - ($x - $sizeX2) * tan($theta) + $sizeY2);
if($x > 0 && $x < $this->width && $y1 > 0 && $y1 < $this->height) {
imageline($this->image, $x, $y1, $x, $y1, $color);
}
if($x1 > 0 && $x1 < $this->width) {
imageline($this->image, $x1, $x, $x1, $x, $color);
}
}
return $this;
}
/**
* Returns binary frame buffer of the current image.
*
* In case of color/grayscale images, use Image::toBinary() first.
*
* @param resource $background Image to binarize.
* @return string String of chars.
*/
protected function getBinaryBuffer() {
// catch the GD2 output
ob_start();
// convert a (probably) color/greyscale image to the monochrome counterpart.
imagewbmp($this->image);
// catch contents of the WBMP file.
$wbmp = ob_get_clean();
// ignore 2 bytes of WBMP file header
$i = 2;
// ignore 2 VLQ's (variable-length quantities) describing WBMP resolution.
for($j = 0; $j < 2; ++$j) {
do {
$test = ord($wbmp[$i]) & 0x80;
++$i;
} while($test);
}
// return binary buffer as a string of chars (bytes)
return substr($wbmp, $i);
}
/**
* Detects if the binary buffer of this image is empty after conversion to two-colors mode.
*
* Empty buffer may mean that the conversion failed.
*
* @param double $threshold The threshold.
* @return bool True if empty, false otherwise.
*/
public function isBinaryBufferEmpty($threshold = 0.01) {
$bin = $this->getBinaryBuffer();
// delete all empty bytes
if(strlen(trim($bin, chr(255))) / strlen($bin) < $threshold || strlen(trim($bin, chr(0))) / strlen($bin) < $threshold) {
// something is wrong, image is filled (almost) solid color.
return true;
}
return false;
}
/**
* Returns GD2 resource handle of this image.
*
* @return resource
*/
public function getGd2Handle() {
return $this->image;
}
/**
* Reduces color palette of this image to two colors (1 = pixel set, 0 = background).
*
* @param bool $useEdgeDetection Use edge detection before converting to two colors?
* @param bool $useDithering Use dithering? Default: false
* @return Image
*/
public function toBinary($useEdgeDetection = false, $useDithering = false) {
// convert to 2 colors
if($useEdgeDetection) {
$background = imagecreate($this->width, $this->height);
imagecopy($background, $this->image, 0, 0, 0, 0, $this->width, $this->height);
imagefilter($this->image, IMG_FILTER_EDGEDETECT);
if($this->toBinary(false)->isBinaryBufferEmpty()) {
// conversion failed, we lost too much data, further results would be inconclusive
// try again, without edge detection
$this->image = $background;
$this->toBinary(false);
return $this;
}
}
imagetruecolortopalette($this->image, $useDithering, 2);
imagecolorset($this->image, 0, 0, 0, 0);
imagecolorset($this->image, 1, 255, 255, 255);
return $this;
}
/**
* Generate histogram data.
*
* @param int $channels Channels to draw, possibilities: Channel::RGB (default), Channel::RED, Channel::GREEN, Channel::BLUE
* @param int $colorMode Color mode for saturation (use Color::HSV, Color::HSI or Color::HSL as the value), default is Color::HSL
* @return int[] Histogram data
*/
public function getHistogramData($channels, $colorMode = Color::HSL) {
$colors = array_fill(0, 256, 0);
for($y = 0; $y < $this->height; ++$y) {
for($x = 0; $x < $this->width; ++$x) {
$rgb = imagecolorat($this->image, $x, $y);
$r = ($rgb >> 16) & 0xff;
$g = ($rgb >> 8) & 0xff;
$b = $rgb & 0xff;
switch ($channels) {
case Channel::RGB:
switch($colorMode) {
case Color::HSL:
// HSL algorithm
$luminescence = (max($r, $g, $b) + min($r, $g, $b)) / 2;
break;
case Color::HSV:
// HSV algorithm
$luminescence = max($r, $g, $b);
break;
case Color::HSI:
// HSI algorithm
$luminescence = ($r + $g + $b) / 3;
break;
default:
// fastest
$luminescence = ($r + $r + $r + $b + $g + $g + $g + $g) >> 3;
break;
}
break;
case Channel::RED:
$luminescence = $r;
break;
case Channel::GREEN:
$luminescence = $g;
break;
case Channel::BLUE:
$luminescence = $b;
break;
}
++$colors[$luminescence];
}
}
return $colors;
}
/**
* Creates an image histogram.
*
* @param int $channels Channels to draw, possibilities: Channel::RGB (default), Channel::RED, Channel::GREEN, Channel::BLUE
* @param int $color Foreground color.
* @param int $backgroundColor Background color.
* @param bool $turboMode Use turbo mode (less accurate).
* @return Image A histogram image
*/
public function getHistogram($channels = Channel::RGB, $color = 0, $backgroundColor = 0xffffff, $turboMode = false) {
$colors = $colors2 = $this->getHistogramData($channels, $turboMode ? null : Color::HSL);
sort($colors2, SORT_NUMERIC);
$min = $colors2[0];
$max = $colors2[255];
$diff = $max - $min;
$ratio = 255 / $colors2[255];
$histogram = imagecreatetruecolor(256, 128);
imagefill($histogram, 0, 0, $backgroundColor);
foreach($colors as $gray => $value) {
imageline($histogram, $gray, 255, $gray, 128 - ($colors[$gray] - $min) * 128 / $diff, $color);
}
return new Image(null, $histogram);
}
/**
* Changes the hue of an image.
*
* @param int $angle The hue value in degrees (0 - 360')
* @return Image
*/
public function setHue($angle) {
return $this->changeHsl($angle % 360, 1, 1);
}
/**
* Changes (multiplies) the saturation of the image by a given factor.
*
* @param float $factor The saturation factor (1 = no change, 0.5 = 50% of original saturation, 2 = 200% saturation, etc.)
* @return Image
*/
public function setSaturation($factor) {
return $this->changeHsl(0, $factor, 1);
}
/**
* Changes (multiplies) the luminosity of the image by a given factor.
*
* @param float $factor The luminosity factor (1 = no change, 0.5 = 50% of original luminosity, 2 = 200% luminosity, etc.)
* @return Image
*/
public function setLuminance($factor) {
return $this->changeHsl(0, 1, $factor);
}
/**
* Blends two images together.
*
* @param Image $otherImage Other image to blend.
* @param Point $position X, Y coordinates of the destination point.
* @param int $blendingMode Blending mode, for example: Blender::USE_ADDITION [is used as default]
* @param float $opacity Opacity factor [0...1], used only in the Blender::USE_TRANSPARENCY blending mode
* @return Image
*/
public function useBlender(Image $otherImage, Point $position, $blendingMode = Blender::USE_ADDITION, $opacity = null) {
$background = $otherImage->getGd2Handle();
$mode = Shader::USE_RGB;
switch($blendingMode) {
case Blender::USE_ADDITION:
$ops = '$r += $r2; $g += $g2; $b += $b2;';
break;
case Blender::USE_SUBTRACT:
$ops = '$r -= $r2; $g -= $g2; $b -= $b2;
$r = $r < 0 ? 0 : $r;
$g = $g < 0 ? 0 : $g;
$b = $b < 0 ? 0 : $b;
';
break;
case Blender::USE_DIVIDE:
$ops = '
$r = $r2 / ($r + 0.001);
$g = $g2 / ($g + 0.001);
$b = $b2 / ($b + 0.001);
';
break;
case Blender::USE_LIGHTEN:
$ops = '$luminance2 = ($g2 >= $b2 ? ($r2 >= $g2 ? $r2 : $g2) : ($r2 >= $b2 ? $r2 : $b2));
$luminance = ($g >= $b ? ($r >= $g ? $r : $g) : ($r >= $b ? $r : $b));
if($luminance < $luminance2) {
$r = $r2; $g = $g2; $b = $b2;
}
';
break;
case Blender::USE_DARKEN:
$ops = '$luminance2 = ($g2 >= $b2 ? ($r2 >= $g2 ? $r2 : $g2) : ($r2 >= $b2 ? $r2 : $b2));
$luminance = ($g >= $b ? ($r >= $g ? $r : $g) : ($r >= $b ? $r : $b));
if($luminance > $luminance2) {
$r = $r2; $g = $g2; $b = $b2;
}
';
break;
case Blender::USE_DIFFERENCE:
$ops = '$r -= $r2; $g -= $g2; $b -= $b2;
$r = $r < 0 ? -$r : $r;
$g = $g < 0 ? -$g : $g;
$b = $b < 0 ? -$b : $b;
';
break;
case Blender::USE_MULTIPLY:
$ops = '$r *= 255; $g *=255; $b *= 255;
$r2 *= 255; $g2 *=255; $b2 *= 255;
$r = ($r * $r2) / 65025;
$g = ($g * $g2) / 65025;
$b = ($b * $b2) / 65025;
';
break;
case Blender::USE_OPACITY:
$alpha = 1 - $opacity;
$mode = Shader::USE_INT | Shader::USE_ALPHA;
$ops = '$r = $r2; $g = $g2; $b = $b2; $a = '.$alpha.';';
break;
default:
throw new Exception('Unsupported blending mode');
break;
}
$shader = new Shader('
$rgb2 = imagecolorat($args[0], $x - $args[1], $y - $args[2]);
$r2 = (($rgb2 >> 16) & 0xff) / 255;
$g2 = (($rgb2 >> 8) & 0xff) / 255;
$b2 = ($rgb2 & 0xff) / 255;
'.$ops);
$shader->setMode($mode);
$shader->useArea($position, new Point($otherImage->getWidth() + $position->x, $otherImage->getHeight() + $position->y));
$this->useShader($shader, array($background, $position->x, $position->y));
return $this;
}
/**
* Creates and runs a new shader (HSV mode).
*
* Shader is a set of PHP operations performed on each pixel of the image.
* Currently you can alter the luminance, saturation and hue of every single pixel of the image.
*
* In order to do so, you have a read-only access to these variables:
* $r [0...1], $g[0...1], $b[0...1]
* $x, $y,
* $width, $height, $background [gd resource]
*
* and full read/write access to:
* $luminance [0...1], $saturation [0...1], $hue [0...360],
*
* @param mixed $shaderContent Shader content.
* @param mixed[] $args Arguments array passed to the shader.
* @return Image
*/
public function useShader($shaderContent, $args = array()) {
$width = $this->width;// 'imagesx($background)';
$height = $this->height;// 'imagesy($background)';
$x = $y = 0;
$mode = Shader::USE_HSV;
if(is_object($shaderContent) && $shaderContent instanceof Shader) {
$area = $shaderContent->getArea();
if($area) {
$x = max($area[0]->x, 0);
$y = max($area[0]->y, 0);
$width = min($area[1]->x, $this->width);
$height = min($area[1]->y, $this->height);
}
$mode = $shaderContent->getMode();
$shaderContent = $shaderContent->getInstructions();
}
$shader = '
$hits = 0;
$width = '.$width.';
$height = '.$height.';
for($y = '.$y.'; $y < $height; ++$y) {
for($x = '.$x.'; $x < $width; ++$x) {
$rgb = imagecolorat($background, $x, $y);
$r = (($rgb >> 16) & 0xff) / 255;
$g = (($rgb >> 8) & 0xff) / 255;
$b = ($rgb & 0xff) / 255;
'.($mode & Shader::USE_HSV ?
// --- HSV MODE START
'
// equivalent of $min = min($r, $g, $b), etc (loop is 10% faster)
$min = ($g <= $b ? ($r <= $g ? $r : $g) : ($r <= $b ? $r : $b));
$luminance = ($g >= $b ? ($r >= $g ? $r : $g) : ($r >= $b ? $r : $b));
$diff = $luminance - $min;
if($luminance > $min) {
$saturation = $diff / $luminance;
$hue = ((($r === $min ? 3.0 : ($g === $min ? 5 : 1)) - ($r === $min ? $g - $b : ($g === $min ? $b - $r : $r - $g)) / $diff) * 60) / 360;
} else {
$saturation = 0.0;
$hue = 0.0;
}
'
:
null
// --- HSV MODE END
)
.
$shaderContent
.
($mode & Shader::USE_HSV ?
// --- HSV MODE START
'
if($saturation === 0.0) {
$r = $luminance;
$g = $luminance;
$b = $luminance;
} else {
$tH = $hue * 6.0;
$tI = (int)$tH;
$t1 = $luminance * (1.0 - $saturation);
$t2 = $luminance * (1.0 - $saturation * ($tH - $tI));
$t3 = $luminance * (1.0 - $saturation * (1.0 - ($tH - $tI)));
switch($tI) {
case 0:
$r = $luminance;
$g = $t3;
$b = $t1;
break;
case 1:
$r = $t2;
$g = $luminance;
$b = $t1;
break;
case 2:
$r = $t1;
$g = $luminance;
$b = $t3;
break;
case 3:
$r = $t1;
$g = $t2;
$b = $luminance;
break;
case 4:
$r = $t3;
$g = $t1;
$b = $luminance;
break;
default:
$r = $luminance;
$g = $t1;
$b = $t2;
break;
}
}
'
:
null
// --- HSV MODE END
).
($mode & Shader::USE_ALPHA ?
'
imagesetpixel($background, $x, $y, (($a * 127) << 24) | (($r < 1 ? ($r * 255) << 16 : 16711680)) | (($g < 1 ? ($g * 255) << 8 : 65280)) | ($b < 1 ? $b * 255 : 255));
'
:
'
imagesetpixel($background, $x, $y, (($r < 1 ? ($r * 255) << 16 : 16711680)) | (($g < 1 ? ($g * 255) << 8 : 65280)) | ($b < 1 ? $b * 255 : 255));
'
)
.'
}
}
';
$run = create_function('$background, $args', $shader);
if($mode & Shader::USE_ALPHA) {
imagealphablending($this->image, true);
}
$run($this->image, $args);
if($mode & Shader::USE_ALPHA) {
imagealphablending($this->image, false);
}
return $this;
}
/**
* Gaussian blur (CPU optimized, eats memory like hell)
*
* @param int $distance Blur distance.
* @return Image
*/
protected function useFastBlur($distance) {
$size = ($distance * 2 + 1) * ($distance * 2 + 1);
$img = array();
for ($x = 0; $x < $this->width; ++$x) {
$img[$x] = array();
for ($y = 0; $y < $this->height; ++$y) {
$img[$x][$y] = imagecolorat($this->image, $x, $y);
}
}
for ($x = 0; $x < $this->width; ++$x) {
$maxX = $this->width < $x + $distance ? $this->width : $x + $distance;
$k1 = $x < $distance ? 0 : $x - $distance;
for ($y = 0; $y < $this->height; ++$y) {
$maxY = $this->height < $y + $distance ? $this->height : $y + $distance;
$l1 = $y < $distance ? 0 : $y - $distance;
$r = $g = $b = 0;
for($k = $k1; $k < $maxX; ++$k) {
$columnK = $img[$k];
for ($l = $l1; $l < $maxY; ++$l) {
$color = $columnK[$l];
$r += ($color >> 16) & 0xff;
$g += ($color >> 8) & 0xff;
$b += $color & 0xff;
}
}
$size = ($maxX - $k1) * ($maxY - $l1);
$r /= $size;
$g /= $size;
$b /= $size;
imagesetpixel($this->image, $x, $y, $r << 16 | $g << 8 | $b);
}
}
return $this;
}
/**
* Gaussian blur (memory optimized, uses more CPU)
*
* @param int $distance Blur distance.
* @return Image
*/
public function useBlur($distance) {
if($this->useBooster || $this->width * $this->height < 100000) {
return $this->useFastBlur($distance);
}
$cache = array(array());
for ($x = 0; $x < $this->width; ++$x) {
$maxX = $this->width < $x + $distance ? $this->width : $x + $distance;
$k1 = $x < $distance ? 0 : $x - $distance;
for ($y = 0; $y < $this->height; ++$y) {
$maxY = $this->height < $y + $distance ? $this->height : $y + $distance;
$l1 = $y < $distance ? 0 : $y - $distance;
$r = $g = $b = 0;
for($k = $k1; $k < $maxX; ++$k) {
for ($l = $l1; $l < $maxY; ++$l) {
if(isset($cache[$k][$l])) {
$color = $cache[$k][$l];
} else {
$color = imagecolorat($this->image, $k, $l);// $columnK[$l];
$cache[$k][$l] = $color;
}
$r += ($color >> 16) & 0xff;
$g += ($color >> 8) & 0xff;
$b += $color & 0xff;
}
if($k % 32 === 0 && $l % 16 === 0) {
$cache = array(array());
}
}
$size = ($maxX - $k1) * ($maxY - $l1);
$r /= $size;
$g /= $size;
$b /= $size;
imagesetpixel($this->image, $x, $y, $r << 16 | $g << 8 | $b);
}
}
return $this;
}
/**
* Changes HSL values.
*
* @param mixed $value
* @return Image
*/
public function changeHsl($hueAngle = 0, $saturationFactor = 1, $luminanceFactor = 1) {
$hueAngle /= 360;
$this->useShader('
$hue += $args[0];
$saturation *= $args[1];
$luminance *= $args[2];
', array($hueAngle, $saturationFactor, $luminanceFactor));
return $this;
}
/**
* Creates simulated HDR image (still in beta version).
*
* @param float $threshold Luminance threshold (0...1), the higher, the darker image.
* @return Image
*/
public function toHdr($threshold = 0.80) {
if($threshold > 1 || $threshold < 0) {
throw new Exception('Invalid threshold, should be between 0 and 1');
}
$emboss = $this->getCopy();
$gd = $emboss->getGd2Handle();
imagefilter($gd, IMG_FILTER_EMBOSS);
imagefilter($gd, IMG_FILTER_GAUSSIAN_BLUR);
$emboss->toGrayScale();
//$this->image = $light;
$this->useShader('
$alpha = ((imagecolorat($args[0], $x, $y) & 0xff) / 255);
if($luminance < '.$threshold.') {
$dd = ($luminance/ '.$threshold.');
$luminance *= 0.9 * $dd;
$saturation *= 2 - $dd;
if($saturation > 1) {
$saturation = 1;
}
} else {
if($alpha < 0.9) {
$luminance *= (1 - $alpha / 8);
$saturation *= 1.3 * (1 - $alpha / 8);
} else {
$dd = 1 - (($luminance - '.$threshold.') / '.(1 - $threshold).');
$saturation *= 1.8 * $dd;
}
}
', array($gd));
//$this->image = $gd;
return $this;
}
/**
* Uses median filter to reduce image noise.
*
* @param int $maskWidth Width of the median mask (default: 3)
* @param int $maskHeight Height of the median mask (default: 3)
* @return Image This image.
*/
public function useMedian($maskWidth = 3, $maskHeight = 3) {
// initialize scanline buffer
$scanLines = array();
// precalculate some variables for better performance
$maskWidth2 = (int)($maskWidth / 2);
$maskHeight2 = (int)($maskHeight / 2);
$maskMiddle = (int)(($maskWidth * $maskHeight) / 2);
$maxY = $this->height - $maskHeight2;
$maxX = $this->width - $maskWidth2;
// scan every line of the image
for($y = $maskHeight2; $y < $maxY; ++$y) {
$medianY = $y + $maskHeight2;
$maxI = $y + $maskHeight2 + 1;
$minI = $y <= $maskHeight2 ? 0 : $y + 1;
// cache few image lines in advance, to speed up further access.
for($i = $minI; $i < $maxI; ++$i) {
for($j = 0; $j < $this->width; ++$j) {
$scanLines[$i][$j] = imagecolorat($this->image, $j, $i);
}
}
// unset old image lines from the cache.
unset($scanLines[$medianY - $maskHeight]);
for($x = $maskWidth2; $x < $maxX; ++$x) {
$medianX = $x + $maskWidth2;
$median = array();
for($n = 0; $n < $maskHeight; ++$n) {
for($m = 0; $m < $maskWidth; ++$m) {
$median[] = $scanLines[$medianY - $n][$medianX - $m];
}
}
sort($median, SORT_NUMERIC);
imagesetpixel($this->image, $x, $y, $median[$maskMiddle]);
}
}
return $this;
}
/**
* (Fast) Detects the background color of this image.
*
* @param int $numberOfSampes Number of detected colors (sorted by the propability).
* @return mixed RGB representation of the color, array of samples: array(rgb => propability), or an instance of Color class if one sample selected.
*/
public function getBackgroundColor($numberOfSamples = 1) {
$colors = array();
if($this->useBooster && ($this->width > 256 || $this->height > 256)) {
// create a thumbnail for the computations
if($this->width > $this->height) {
$ratio = 256 / $this->width;
$width = 256;
$height = (int)($ratio * $this->width);
} else {
$ratio = 256 / $this->height;
$height = 256;
$width = (int)($ratio * $this->width);
}
$background = imagecreatetruecolor($width, $height);
imagecopyresized($background, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
} else {
$background = $this->image;
$width = $this->width;
$height = $this->height;
}
// this is a quick test only, may be inaccurate
// two image samples per one iteration (better performance)
for($x = 0; $x < $width; ++$x) {
$rgb = imagecolorat($background, $x, 0);
if(isset($colors[$rgb])) {
++$colors[$rgb];
} else {
$colors[$rgb] = 1;
}
$rgb = imagecolorat($background, $x, $height - 1);
if(isset($colors[$rgb])) {
++$colors[$rgb];
} else {
$colors[$rgb] = 1;
}
}
// two image samples per one iteration (better performance)
for($y = 0; $y < $height; ++$y) {
$rgb = imagecolorat($background, 0, $y);
if(isset($colors[$rgb])) {
++$colors[$rgb];
} else {
$colors[$rgb] = 1;
}
$rgb = imagecolorat($background, $width - 1, $y);
if(isset($colors[$rgb])) {
++$colors[$rgb];
} else {
$colors[$rgb] = 1;
}
}
arsort($colors, SORT_NUMERIC);
if($numberOfSamples === 1) {
reset($colors);
return new Color(key($colors));
}
$colors = array_slice($colors, 0, 10);
foreach($colors as $key => $value) {
$colors[$key] = new Color($value);
}
return $colors;
}
/**
* Creates the image buffer used by the skew functions.
*
* @param int $size Size of the matrix.
* @param int $ratio Rescale ratio of this image (value is returned by this method)
* @return string Binary frame buffer
*/
protected function createImageMatrix($size = 0, &$ratio) {
// create two-color image
$this->toBinary(false);
// rescale if necessary (for performance reasons)
if($size > 0 && ($this->width > $size || $this->height > $size)) {
if($this->width > $this->height) {
$ratio = $size / $this->width;
} else {
$ratio = $size / $this->height;
}
$width = (int)($ratio * $this->width);
$height = (int)($ratio * $this->height);
$background = imagecreate($width, $height);
imagecopyresized($background, $this->image, 0, 0, 0, 0, $width, $height, $this->width, $this->height);
} else {
$ratio = 1;
$background = imagecreate($this->width, $this->height);
imagecopy($background, $this->image, 0, 0, 0, 0, $this->width, $this->height);
list($width, $height) = $this->getSize();
}
$this->image = $background;
// find the background color (fast approximation)
$bin = $this->getBinaryBuffer();
$colors = count_chars($bin);
arsort($colors, SORT_NUMERIC);
reset($colors); // HipHop for PHP compatibility.
$backgroundColor = key($colors);
// make colors negation if necessary, 0's should be used for background.
if($backgroundColor === 255) {
imagefilter($this->image, IMG_FILTER_NEGATE);
}
$background = imagecreatetruecolor($size, $size);
imagefill($background, 0, 0, 0);
imagecopy($background, $this->image, ($size - $width) / 2, ($size - $height) / 2, 0, 0, $width, $height);
$this->image = $background;
$bin = $this->getBinaryBuffer();
$this->getSize();
return $bin;
}
}