update niucloud vendor

This commit is contained in:
全栈小学生 2023-06-21 16:54:02 +08:00
parent 4539074303
commit 38010ed1b5
101 changed files with 8623 additions and 1846 deletions

View File

@ -10,9 +10,9 @@ return array(
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => $vendorDir . '/topthink/think-orm/stubs/load_stubs.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',

View File

@ -6,12 +6,12 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'yunwuxin\\cron\\' => array($vendorDir . '/yunwuxin/think-cron/src/cron'),
'think\\view\\driver\\' => array($vendorDir . '/topthink/think-view/src'),
'think\\trace\\' => array($vendorDir . '/topthink/think-trace/src'),
'think\\captcha\\' => array($vendorDir . '/topthink/think-captcha/src'),
'think\\app\\' => array($vendorDir . '/topthink/think-multi-app/src'),
'think\\' => array($vendorDir . '/topthink/framework/src/think', $vendorDir . '/topthink/think-helper/src', $vendorDir . '/topthink/think-image/src', $vendorDir . '/topthink/think-orm/src', $vendorDir . '/topthink/think-queue/src', $vendorDir . '/topthink/think-template/src'),
'schedule\\' => array($vendorDir . '/yzh52521/schedule/src'),
'dh2y\\qrcode\\' => array($vendorDir . '/dh2y/think-qrcode/src'),
'core\\' => array($baseDir . '/core'),
'clagiordano\\weblibs\\configmanager\\' => array($vendorDir . '/clagiordano/weblibs-configmanager/src'),
@ -65,6 +65,7 @@ return array(
'GuzzleHttp\\Command\\Guzzle\\' => array($vendorDir . '/guzzlehttp/guzzle-services/src'),
'GuzzleHttp\\Command\\' => array($vendorDir . '/guzzlehttp/command/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'Grafika\\' => array($vendorDir . '/kosinix/grafika/src/Grafika'),
'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
'Fastknife\\' => array($vendorDir . '/fastknife/ajcaptcha/src'),
'EasyWeChat\\' => array($vendorDir . '/overtrue/wechat/src'),

View File

@ -11,9 +11,9 @@ class ComposerStaticInitf082efa3600aae2b847c3e8b4e641a4e
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => __DIR__ . '/..' . '/topthink/think-orm/stubs/load_stubs.php',
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
@ -39,6 +39,10 @@ class ComposerStaticInitf082efa3600aae2b847c3e8b4e641a4e
);
public static $prefixLengthsPsr4 = array (
'y' =>
array (
'yunwuxin\\cron\\' => 14,
),
't' =>
array (
'think\\view\\driver\\' => 18,
@ -47,10 +51,6 @@ class ComposerStaticInitf082efa3600aae2b847c3e8b4e641a4e
'think\\app\\' => 10,
'think\\' => 6,
),
's' =>
array (
'schedule\\' => 9,
),
'd' =>
array (
'dh2y\\qrcode\\' => 12,
@ -151,6 +151,7 @@ class ComposerStaticInitf082efa3600aae2b847c3e8b4e641a4e
'GuzzleHttp\\Command\\Guzzle\\' => 26,
'GuzzleHttp\\Command\\' => 19,
'GuzzleHttp\\' => 11,
'Grafika\\' => 8,
),
'F' =>
array (
@ -180,6 +181,10 @@ class ComposerStaticInitf082efa3600aae2b847c3e8b4e641a4e
);
public static $prefixDirsPsr4 = array (
'yunwuxin\\cron\\' =>
array (
0 => __DIR__ . '/..' . '/yunwuxin/think-cron/src/cron',
),
'think\\view\\driver\\' =>
array (
0 => __DIR__ . '/..' . '/topthink/think-view/src',
@ -205,10 +210,6 @@ class ComposerStaticInitf082efa3600aae2b847c3e8b4e641a4e
4 => __DIR__ . '/..' . '/topthink/think-queue/src',
5 => __DIR__ . '/..' . '/topthink/think-template/src',
),
'schedule\\' =>
array (
0 => __DIR__ . '/..' . '/yzh52521/schedule/src',
),
'dh2y\\qrcode\\' =>
array (
0 => __DIR__ . '/..' . '/dh2y/think-qrcode/src',
@ -422,6 +423,10 @@ class ComposerStaticInitf082efa3600aae2b847c3e8b4e641a4e
array (
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
),
'Grafika\\' =>
array (
0 => __DIR__ . '/..' . '/kosinix/grafika/src/Grafika',
),
'Firebase\\JWT\\' =>
array (
0 => __DIR__ . '/..' . '/firebase/php-jwt/src',

View File

@ -1341,6 +1341,61 @@
],
"install-path": "../intervention/image"
},
{
"name": "kosinix/grafika",
"version": "dev-master",
"version_normalized": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/kosinix/grafika.git",
"reference": "211f61fc334b8b36616b23e8af7c5727971d96ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kosinix/grafika/zipball/211f61fc334b8b36616b23e8af7c5727971d96ee",
"reference": "211f61fc334b8b36616b23e8af7c5727971d96ee",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=5.3"
},
"time": "2017-06-20T03:13:49+00:00",
"default-branch": true,
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Grafika\\": "src/Grafika"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT",
"GPL-2.0+"
],
"authors": [
{
"name": "Nico Amarilla",
"homepage": "https://www.kosinix.com"
}
],
"description": "An image manipulation library for PHP.",
"homepage": "http://kosinix.github.io/grafika",
"keywords": [
"grafika"
],
"support": {
"issues": "https://github.com/kosinix/grafika/issues",
"source": "https://github.com/kosinix/grafika/tree/2.0.8"
},
"install-path": "../kosinix/grafika"
},
{
"name": "laravel/serializable-closure",
"version": "v1.2.2",
@ -5711,18 +5766,18 @@
"install-path": "../yansongda/supports"
},
{
"name": "yzh52521/schedule",
"version": "v1.0.0",
"version_normalized": "1.0.0.0",
"name": "yunwuxin/think-cron",
"version": "v3.0.5",
"version_normalized": "3.0.5.0",
"source": {
"type": "git",
"url": "https://github.com/yzh52521/schedule.git",
"reference": "4c8f537f0c08417e785f84b8b91bf16b083cb163"
"url": "https://github.com/yunwuxin/think-cron.git",
"reference": "a5e5c679b7f5daedab9fb4bb00b641b6c4a054ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/yzh52521/schedule/zipball/4c8f537f0c08417e785f84b8b91bf16b083cb163",
"reference": "4c8f537f0c08417e785f84b8b91bf16b083cb163",
"url": "https://api.github.com/repos/yunwuxin/think-cron/zipball/a5e5c679b7f5daedab9fb4bb00b641b6c4a054ca",
"reference": "a5e5c679b7f5daedab9fb4bb00b641b6c4a054ca",
"shasum": "",
"mirrors": [
{
@ -5732,35 +5787,48 @@
]
},
"require": {
"nesbot/carbon": "^2.0",
"php": ">=7.1"
"dragonmantank/cron-expression": "^3.0",
"nesbot/carbon": "^2.28",
"symfony/process": "^4.4|^5.0",
"topthink/framework": "^6.0"
},
"time": "2020-07-02T01:34:32+00:00",
"require-dev": {
"topthink/think-swoole": "^4.0"
},
"time": "2021-12-22T09:25:54+00:00",
"type": "library",
"extra": {
"think": {
"config": {
"cron": "src/config.php"
},
"services": [
"yunwuxin\\cron\\Service"
]
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"schedule\\": "src/"
"yunwuxin\\cron\\": "src/cron"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
"Apache-2.0"
],
"description": "task schedule,schedule,thinkphp schedule,任务调度",
"keywords": [
"schedule",
"task schedule",
"think-schedule",
"thinkphp",
"thinkphp5.1",
"thinkphp6"
"authors": [
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"description": "计划任务",
"support": {
"issues": "https://github.com/yzh52521/schedule/issues",
"source": "https://github.com/yzh52521/schedule/tree/v1.0.0"
"issues": "https://github.com/yunwuxin/think-cron/issues",
"source": "https://github.com/yunwuxin/think-cron/tree/v3.0.5"
},
"install-path": "../yzh52521/schedule"
"install-path": "../yunwuxin/think-cron"
}
],
"dev": true,

View File

@ -3,7 +3,7 @@
'name' => 'topthink/think',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'dc3bba8859823f8836483423b8b7bfdda3a18e7d',
'reference' => 'bdc0c0b0594f0eff6e5ca8d8018f125190f8ee95',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -163,6 +163,17 @@
'aliases' => array(),
'dev_requirement' => false,
),
'kosinix/grafika' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '211f61fc334b8b36616b23e8af7c5727971d96ee',
'type' => 'library',
'install_path' => __DIR__ . '/../kosinix/grafika',
'aliases' => array(
0 => '9999999-dev',
),
'dev_requirement' => false,
),
'laravel/serializable-closure' => array(
'pretty_version' => 'v1.2.2',
'version' => '1.2.2.0',
@ -652,7 +663,7 @@
'topthink/think' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'dc3bba8859823f8836483423b8b7bfdda3a18e7d',
'reference' => 'bdc0c0b0594f0eff6e5ca8d8018f125190f8ee95',
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -766,12 +777,12 @@
'aliases' => array(),
'dev_requirement' => false,
),
'yzh52521/schedule' => array(
'pretty_version' => 'v1.0.0',
'version' => '1.0.0.0',
'reference' => '4c8f537f0c08417e785f84b8b91bf16b083cb163',
'yunwuxin/think-cron' => array(
'pretty_version' => 'v3.0.5',
'version' => '3.0.5.0',
'reference' => 'a5e5c679b7f5daedab9fb4bb00b641b6c4a054ca',
'type' => 'library',
'install_path' => __DIR__ . '/../yzh52521/schedule',
'install_path' => __DIR__ . '/../yunwuxin/think-cron',
'aliases' => array(),
'dev_requirement' => false,
),

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.

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.

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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Grafika;
/**
* Interface DrawingObjectInterface
* @package Grafika
*/
interface DrawingObjectInterface {
/**
* @param ImageInterface $image
*
* @return ImageInterface
*/
public function draw( $image );
}

View File

@ -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 );
}

View File

@ -0,0 +1,17 @@
<?php
namespace Grafika;
/**
* Interface FilterInterface
* @package Grafika
*/
interface FilterInterface {
/**
* @param ImageInterface $image
*
* @return ImageInterface
*/
public function apply( $image );
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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()
);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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)));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.');
}
}
}

View File

@ -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();
}

View File

@ -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';
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

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;
}
});

View File

@ -1,9 +1,10 @@
<?php
// This file is automatically generated at:2023-05-24 15:05:09
// This file is automatically generated at:2023-06-13 17:34:02
declare (strict_types = 1);
return array (
0 => 'think\\captcha\\CaptchaService',
1 => 'think\\app\\Service',
2 => 'think\\queue\\Service',
3 => 'think\\trace\\Service',
4 => 'yunwuxin\\cron\\Service',
);

View File

@ -0,0 +1,3 @@
.idea/
composer.lock
vendor/

View File

@ -0,0 +1,66 @@
# think-cron 计划任务
## 安装方法
```
composer require yunwuxin/think-cron
```
## 使用方法
### 创建任务类
```
<?php
namespace app\task;
use yunwuxin\cron\Task;
class DemoTask extends Task
{
public function configure()
{
$this->daily(); //设置任务的周期,每天执行一次,更多的方法可以查看源代码,都有注释
}
/**
* 执行任务
* @return mixed
*/
protected function execute()
{
//...具体的任务执行
}
}
```
### 配置
> 配置文件位于 application/extra/cron.php
```
return [
'tasks' => [
\app\task\DemoTask::class, //任务的完整类名
]
];
```
### 任务监听
#### 两种方法:
> 方法一 (推荐)
起一个常驻进程可以配合supervisor使用
~~~
php think cron:schedule
~~~
> 方法二
在系统的计划任务里添加
~~~
* * * * * php /path/to/think cron:run >> /dev/null 2>&1
~~~

View File

@ -0,0 +1,42 @@
{
"name": "yunwuxin/think-cron",
"description": "计划任务",
"license": "Apache-2.0",
"authors": [
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"require": {
"topthink/framework": "^6.0",
"symfony/process": "^4.4|^5.0",
"nesbot/carbon": "^2.28",
"dragonmantank/cron-expression": "^3.0"
},
"autoload": {
"psr-4": {
"yunwuxin\\cron\\": "src/cron"
}
},
"extra": {
"think": {
"config": {
"cron": "src/config.php"
},
"services": [
"yunwuxin\\cron\\Service"
]
}
},
"require-dev": {
"topthink/think-swoole": "^4.0"
},
"config": {
"preferred-install": "dist",
"platform-check": false,
"platform": {
"ext-swoole": "4.6.0"
}
}
}

View File

@ -0,0 +1,5 @@
<?php
return [
'tasks' => []
];

View File

@ -1,18 +1,18 @@
<?php
namespace schedule\console;
namespace yunwuxin\cron;
use Carbon\Carbon;
trait ManagesFrequencies
{
/**
* The Cron expression representing the event's frequency.
* 设置任务执行周期
*
* @param string $expression
* @param string $expression
* @return $this
*/
public function cron($expression)
public function expression($expression)
{
$this->expression = $expression;
@ -20,10 +20,10 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run between start and end time.
* 设置区间时间
*
* @param string $startTime
* @param string $endTime
* @param string $startTime
* @param string $endTime
* @return $this
*/
public function between($startTime, $endTime)
@ -32,10 +32,10 @@ trait ManagesFrequencies
}
/**
* Schedule the event to not run between start and end time.
* 排除区间时间
*
* @param string $startTime
* @param string $endTime
* @param string $startTime
* @param string $endTime
* @return $this
*/
public function unlessBetween($startTime, $endTime)
@ -43,13 +43,6 @@ trait ManagesFrequencies
return $this->skip($this->inTimeInterval($startTime, $endTime));
}
/**
* Schedule the event to run between start and end time.
*
* @param string $startTime
* @param string $endTime
* @return \Closure
*/
private function inTimeInterval($startTime, $endTime)
{
return function () use ($startTime, $endTime) {
@ -62,57 +55,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run every minute.
*
* @return $this
*/
public function everyMinute()
{
return $this->spliceIntoPosition(1, '*');
}
/**
* Schedule the event to run every five minutes.
*
* @return $this
*/
public function everyFiveMinutes()
{
return $this->spliceIntoPosition(1, '*/5');
}
/**
* Schedule the event to run every ten minutes.
*
* @return $this
*/
public function everyTenMinutes()
{
return $this->spliceIntoPosition(1, '*/10');
}
/**
* Schedule the event to run every fifteen minutes.
*
* @return $this
*/
public function everyFifteenMinutes()
{
return $this->spliceIntoPosition(1, '*/15');
}
/**
* Schedule the event to run every thirty minutes.
*
* @return $this
*/
public function everyThirtyMinutes()
{
return $this->spliceIntoPosition(1, '0,30');
}
/**
* Schedule the event to run hourly.
* 按小时执行
*
* @return $this
*/
@ -122,9 +65,9 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run hourly at a given offset in the hour.
* 按小时延期执行
*
* @param int $offset
* @param int $offset
* @return $this
*/
public function hourlyAt($offset)
@ -133,20 +76,20 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run daily.
* 按天执行
*
* @return $this
*/
public function daily()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0);
->spliceIntoPosition(2, 0);
}
/**
* Schedule the command at a given time.
* 指定时间执行
*
* @param string $time
* @param string $time
* @return $this
*/
public function at($time)
@ -155,9 +98,9 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run daily at a given time (10:00, 19:30, etc).
* 指定时间执行
*
* @param string $time
* @param string $time
* @return $this
*/
public function dailyAt($time)
@ -165,26 +108,26 @@ trait ManagesFrequencies
$segments = explode(':', $time);
return $this->spliceIntoPosition(2, (int) $segments[0])
->spliceIntoPosition(1, count($segments) == 2 ? (int) $segments[1] : '0');
->spliceIntoPosition(1, count($segments) == 2 ? (int) $segments[1] : '0');
}
/**
* Schedule the event to run twice daily.
* 每天执行两次
*
* @param int $first
* @param int $second
* @param int $first
* @param int $second
* @return $this
*/
public function twiceDaily($first = 1, $second = 13)
{
$hours = $first.','.$second;
$hours = $first . ',' . $second;
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, $hours);
->spliceIntoPosition(2, $hours);
}
/**
* Schedule the event to run only on weekdays.
* 工作日执行
*
* @return $this
*/
@ -194,7 +137,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on weekends.
* 周末执行
*
* @return $this
*/
@ -204,7 +147,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on Mondays.
* 星期一执行
*
* @return $this
*/
@ -214,7 +157,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on Tuesdays.
* 星期二执行
*
* @return $this
*/
@ -224,7 +167,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on Wednesdays.
* 星期三执行
*
* @return $this
*/
@ -234,7 +177,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on Thursdays.
* 星期四执行
*
* @return $this
*/
@ -244,7 +187,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on Fridays.
* 星期五执行
*
* @return $this
*/
@ -254,7 +197,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on Saturdays.
* 星期六执行
*
* @return $this
*/
@ -264,7 +207,7 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run only on Sundays.
* 星期天执行
*
* @return $this
*/
@ -274,22 +217,22 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run weekly.
* 按周执行
*
* @return $this
*/
public function weekly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(5, 0);
->spliceIntoPosition(2, 0)
->spliceIntoPosition(5, 0);
}
/**
* Schedule the event to run weekly on a given day and time.
* 指定每周的时间执行
*
* @param int $day
* @param string $time
* @param int $day
* @param string $time
* @return $this
*/
public function weeklyOn($day, $time = '0:0')
@ -300,22 +243,22 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run monthly.
* 按月执行
*
* @return $this
*/
public function monthly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1);
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1);
}
/**
* Schedule the event to run monthly on a given day and time.
* 指定每月的执行时间
*
* @param int $day
* @param string $time
* @param int $day
* @param string $time
* @return $this
*/
public function monthlyOn($day = 1, $time = '0:0')
@ -326,15 +269,15 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run twice monthly.
* 每月执行两次
*
* @param int $first
* @param int $second
* @param int $first
* @param int $second
* @return $this
*/
public function twiceMonthly($first = 1, $second = 16)
{
$days = $first.','.$second;
$days = $first . ',' . $second;
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
@ -342,35 +285,75 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run quarterly.
* 按季度执行
*
* @return $this
*/
public function quarterly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, '1-12/3');
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, '*/3');
}
/**
* Schedule the event to run yearly.
* 按年执行
*
* @return $this
*/
public function yearly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, 1);
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, 1);
}
/**
* Set the days of the week the command should run on.
* 每分钟执行
*
* @param array|mixed $days
* @return $this
*/
public function everyMinute()
{
return $this->spliceIntoPosition(1, '*');
}
/**
* 每5分钟执行
*
* @return $this
*/
public function everyFiveMinutes()
{
return $this->spliceIntoPosition(1, '*/5');
}
/**
* 每10分钟执行
*
* @return $this
*/
public function everyTenMinutes()
{
return $this->spliceIntoPosition(1, '*/10');
}
/**
* 每30分钟执行
*
* @return $this
*/
public function everyThirtyMinutes()
{
return $this->spliceIntoPosition(1, '0,30');
}
/**
* 按周设置天执行
*
* @param array|mixed $days
* @return $this
*/
public function days($days)
@ -381,9 +364,9 @@ trait ManagesFrequencies
}
/**
* Set the timezone the date should be evaluated on.
* 设置时区
*
* @param \DateTimeZone|string $timezone
* @param string $timezone
* @return $this
*/
public function timezone($timezone)
@ -393,19 +376,12 @@ trait ManagesFrequencies
return $this;
}
/**
* Splice the given value into the given position of the expression.
*
* @param int $position
* @param string $value
* @return $this
*/
protected function spliceIntoPosition($position, $value)
{
$segments = explode(' ', $this->expression);
$segments[$position - 1] = $value;
return $this->cron(implode(' ', $segments));
return $this->expression(implode(' ', $segments));
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace yunwuxin\cron;
use app\service\core\schedule\CoreScheduleService;
use Carbon\Carbon;
use Exception;
use think\App;
use think\cache\Driver;
use yunwuxin\cron\event\TaskFailed;
use yunwuxin\cron\event\TaskProcessed;
use yunwuxin\cron\event\TaskSkipped;
class Scheduler
{
/** @var App */
protected $app;
/** @var Carbon */
protected $startedAt;
protected $tasks = [];
/** @var Driver */
protected $cache;
public function __construct(App $app)
{
$this->app = $app;
// $this->tasks = $app->config->get('cron.tasks', []);
$this->tasks = (new CoreScheduleService())->getList();
$this->cache = $app->cache->store();
}
public function run()
{
$this->startedAt = Carbon::now();
$file = root_path('runtime').'.schedule';
file_put_contents($file, time());
$taskClass = 'app\command\schedule\Schedule';
foreach ($this->tasks as $task_value) {
if (is_subclass_of($taskClass, Task::class)) {
/** @var Task $task */
$task = $this->app->invokeClass($taskClass, [$task_value]);
if ($task->isDue()) {
if (!$task->filtersPass()) {
continue;
}
if ($task->onOneServer) {
$this->runSingleServerTask($task);
} else {
$this->runTask($task);
}
$this->app->event->trigger(new TaskProcessed($task));
}
}
}
}
/**
* @param $task Task
* @return bool
*/
protected function serverShouldRun($task)
{
$key = $task->mutexName() . $this->startedAt->format('Hi');
if ($this->cache->has($key)) {
return false;
}
$this->cache->set($key, true, 60);
return true;
}
protected function runSingleServerTask($task)
{
if ($this->serverShouldRun($task)) {
$this->runTask($task);
} else {
$this->app->event->trigger(new TaskSkipped($task));
}
}
/**
* @param $task Task
*/
protected function runTask($task)
{
try {
$task->run();
} catch (Exception $e) {
$this->app->event->trigger(new TaskFailed($task, $e));
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace yunwuxin\cron;
use Swoole\Timer;
use think\swoole\Manager;
use yunwuxin\cron\command\Run;
use yunwuxin\cron\command\Schedule;
class Service extends \think\Service
{
public function boot()
{
$this->commands([
Run::class,
Schedule::class,
]);
$this->app->event->listen('swoole.init', function (Manager $manager) {
$manager->addWorker(function () use ($manager) {
Timer::tick(60 * 1000, function () use ($manager) {
$manager->runWithBarrier([$manager, 'runInSandbox'], function (Scheduler $scheduler) {
$scheduler->run();
});
});
}, "cron");
});
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace yunwuxin\cron;
use Closure;
use Cron\CronExpression;
use think\App;
use think\Cache;
abstract class Task
{
use ManagesFrequencies;
/** @var string|null 时区 */
public $timezone = null;
/** @var string 任务周期 */
public $expression = '* * * * *';
/** @var bool 任务是否可以重叠执行 */
public $withoutOverlapping = false;
/** @var int 最大执行时间(重叠执行检查用) */
public $expiresAt = 1440;
/** @var bool 分布式部署 是否仅在一台服务器上运行 */
public $onOneServer = false;
protected $filters = [];
protected $rejects = [];
/** @var Cache */
protected $cache;
/** @var App */
protected $app;
protected $vars;
public function __construct(App $app, $vars)
{
$this->app = $app;
// $this->cache = $cache;
$this->cache = $app->cache->store();
$this->vars = $vars;
$this->configure();
}
/**
* 是否到期执行
* @return bool
*/
public function isDue()
{
$cronExpression = new CronExpression($this->expression);
return $cronExpression->isDue('now', $this->timezone);
}
/**
* 配置任务
*/
protected function configure()
{
}
/**
* 执行任务
*/
protected function execute()
{
$this->app->invoke([$this, 'handle'], [], true);
}
final public function run()
{
if ($this->withoutOverlapping &&
!$this->createMutex()) {
return;
}
register_shutdown_function(function () {
$this->removeMutex();
});
try {
$this->execute();
} finally {
$this->removeMutex();
}
}
/**
* 过滤
* @return bool
*/
public function filtersPass()
{
foreach ($this->filters as $callback) {
if (!call_user_func($callback)) {
return false;
}
}
foreach ($this->rejects as $callback) {
if (call_user_func($callback)) {
return false;
}
}
return true;
}
/**
* 任务标识
*/
public function mutexName()
{
return 'task-' . sha1(static::class);
}
protected function removeMutex()
{
return $this->cache->delete($this->mutexName());
}
protected function createMutex()
{
$name = $this->mutexName();
return $this->cache->set($name, time(), $this->expiresAt);
}
protected function existsMutex()
{
if ($this->cache->has($this->mutexName())) {
$mutex = $this->cache->get($this->mutexName());
return $mutex + $this->expiresAt > time();
}
return false;
}
public function when(Closure $callback)
{
$this->filters[] = $callback;
return $this;
}
public function skip(Closure $callback)
{
$this->rejects[] = $callback;
return $this;
}
public function withoutOverlapping($expiresAt = 1440)
{
$this->withoutOverlapping = true;
$this->expiresAt = $expiresAt;
return $this->skip(function () {
return $this->existsMutex();
});
}
public function onOneServer()
{
$this->onOneServer = true;
return $this;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace yunwuxin\cron\command;
use Carbon\Carbon;
use think\console\Command;
use think\exception\Handle;
use yunwuxin\cron\event\TaskFailed;
use yunwuxin\cron\event\TaskProcessed;
use yunwuxin\cron\event\TaskSkipped;
use yunwuxin\cron\Scheduler;
class Run extends Command
{
/** @var Carbon */
protected $startedAt;
protected function configure()
{
$this->startedAt = Carbon::now();
$this->setName('cron:run');
}
public function handle(Scheduler $scheduler)
{
$this->listenForEvents();
$scheduler->run();
}
/**
* 注册事件
*/
protected function listenForEvents()
{
$this->app->event->listen(TaskProcessed::class, function (TaskProcessed $event) {
$this->output->writeln("Task {$event->getName()} run at " . Carbon::now());
});
$this->app->event->listen(TaskSkipped::class, function (TaskSkipped $event) {
$this->output->writeln('<info>Skipping task (has already run on another server):</info> ' . $event->getName());
});
$this->app->event->listen(TaskFailed::class, function (TaskFailed $event) {
$this->output->writeln("Task {$event->getName()} failed at " . Carbon::now());
/** @var Handle $handle */
$handle = $this->app->make(Handle::class);
$handle->renderForConsole($this->output, $event->exception);
$handle->report($event->exception);
});
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace yunwuxin\cron\command;
use Symfony\Component\Process\Process;
use think\console\Command;
use think\console\Input;
use think\console\Output;
class Schedule extends Command
{
protected function configure()
{
$this->setName('cron:schedule');
}
protected function execute(Input $input, Output $output)
{
if ('\\' == DIRECTORY_SEPARATOR) {
$command = 'start /B "Niucloud Schedule" "' . PHP_BINARY . '" think cron:run';
} else {
$command = 'nohup "' . PHP_BINARY . '" think cron:run >> /dev/null 2>&1 &';
}
$process = Process::fromShellCommandline($command);
while (true) {
$process->run();
sleep(60);
}
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace yunwuxin\cron\event;
use yunwuxin\cron\Task;
abstract class TaskEvent
{
public $task;
public function __construct(Task $task)
{
$this->task = $task;
}
public function getName()
{
return get_class($this->task);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace yunwuxin\cron\event;
class TaskFailed extends TaskEvent
{
public $exception;
public function __construct($task, $exception)
{
parent::__construct($task);
$this->exception = $exception;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace yunwuxin\cron\event;
class TaskProcessed extends TaskEvent
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace yunwuxin\cron\event;
class TaskSkipped extends TaskEvent
{
}

View File

@ -1 +0,0 @@
.idea

View File

@ -1,130 +0,0 @@
# schedule
thinkphp 任务调度
代码实现主要参考 laravel 相关用法请参考 laravel
具体用法:
第一步
运行指令
```
php think make:command Schedule schedule:run
```
会生成一个app\console\Schedule命令行指令类我们修改内容如下
```
namespace app\command;
use schedule\console\Command;
use think\console\Input;
use think\console\Output;
class Schedule extends Command
{
protected function configure()
{
$this->setName('schedule:run');
}
protected function execute(Input $input, Output $output)
{
//每天的上午十点和晚上八点执行这个命令
$this->command('test')->twiceDaily(10, 20);
parent::execute($input, $output);
}
}
```
继续运行指令
```
php think make:command Test test
```
第二步配置config/console.php文件
```
<?php
return [
'commands' => [
'schedule:run'=>\app\command\Schedule::class,
'test' => 'app\command\Test',
]
];
```
第三步,您应该在crontab中添加以下命令
```
* * * * * php /path/to/think schedule:run >> /dev/null 2>&1
```
时间表范例
此扩展支持Laravel Schedule的所有功能环境和维护模式除外。
Scheduling Closures
```
$this->call(function()
{
// Do some task...
})->hourly();
```
Running command of your application
```
$this->command('migrate')->cron('* * * * *');
```
Frequent Jobs
```
$this->command('foo')->everyFiveMinutes();
$this->command('foo')->everyTenMinutes();
$this->command('foo')->everyThirtyMinutes();
```
Daily Jobs
```
$this->command('foo')->daily();
```
Daily Jobs At A Specific Time (24 Hour Time)
```
$this->command('foo')->dailyAt('15:00');
```
Twice Daily Jobs
```
$this->command('foo')->twiceDaily();
```
Job That Runs Every Weekday
```
$this->command('foo')->weekdays();
```
Weekly Jobs
```
$this->command('foo')->weekly();
// Schedule weekly job for specific day (0-6) and time...
$this->command('foo')->weeklyOn(1, '8:00');
```
Monthly Jobs
```
$this->command('foo')->monthly();
```
Job That Runs On Specific Days
```
$this->command('foo')->mondays();
$this->command('foo')->tuesdays();
$this->command('foo')->wednesdays();
$this->command('foo')->thursdays();
$this->command('foo')->fridays();
$this->command('foo')->saturdays();
$this->command('foo')->sundays();
```
Only Allow Job To Run When Callback Is True
```
$this->command('foo')->monthly()->when(function()
{
return true;
});
```

View File

@ -1,24 +0,0 @@
{
"name": "yzh52521/schedule",
"description": "task schedule,schedule,thinkphp schedule,任务调度",
"keywords": [
"schedule",
"task schedule",
"thinkphp",
"thinkphp6",
"thinkphp5.1",
"think-schedule"
],
"type": "library",
"license": "MIT",
"minimum-stability": "dev",
"require": {
"php": ">=7.1",
"nesbot/carbon": "^2.0"
},
"autoload": {
"psr-4": {
"schedule\\": "src/"
}
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace schedule;
use schedule\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
class Run extends Command
{
protected function configure()
{
$this->setName('schedule:run');
}
protected function execute(Input $input, Output $output)
{
//每天的上午十点和晚上八点执行这个命令
$this->command('test')->twiceDaily(10, 20);
parent::execute($input, $output);
}
}

View File

@ -1,148 +0,0 @@
<?php
namespace schedule\console;
/**
* Abstract CRON expression field
*/
abstract class AbstractField implements FieldInterface
{
/**
* Check to see if a field is satisfied by a value
*
* @param string $dateValue Date value to check
* @param string $value Value to test
*
* @return bool
*/
public function isSatisfied($dateValue, $value)
{
if ($this->isIncrementsOfRanges($value)) {
return $this->isInIncrementsOfRanges($dateValue, $value);
} elseif ($this->isRange($value)) {
return $this->isInRange($dateValue, $value);
}
return $value == '*' || $dateValue == $value;
}
/**
* Check if a value is a range
*
* @param string $value Value to test
*
* @return bool
*/
public function isRange($value)
{
return strpos($value, '-') !== false;
}
/**
* Check if a value is an increments of ranges
*
* @param string $value Value to test
*
* @return bool
*/
public function isIncrementsOfRanges($value)
{
return strpos($value, '/') !== false;
}
/**
* Test if a value is within a range
*
* @param string $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInRange($dateValue, $value)
{
$parts = array_map('trim', explode('-', $value, 2));
return $dateValue >= $parts[0] && $dateValue <= $parts[1];
}
/**
* Test if a value is within an increments of ranges (offset[-to]/step size)
*
* @param string $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInIncrementsOfRanges($dateValue, $value)
{
$parts = array_map('trim', explode('/', $value, 2));
$stepSize = isset($parts[1]) ? (int) $parts[1] : 0;
if ($stepSize === 0) {
return false;
}
if (($parts[0] == '*' || $parts[0] === '0')) {
return (int) $dateValue % $stepSize == 0;
}
$range = explode('-', $parts[0], 2);
$offset = $range[0];
$to = isset($range[1]) ? $range[1] : $dateValue;
// Ensure that the date value is within the range
if ($dateValue < $offset || $dateValue > $to) {
return false;
}
if ($dateValue > $offset && 0 === $stepSize) {
return false;
}
for ($i = $offset; $i <= $to; $i+= $stepSize) {
if ($i == $dateValue) {
return true;
}
}
return false;
}
/**
* Returns a range of values for the given cron expression
*
* @param string $expression The expression to evaluate
* @param int $max Maximum offset for range
*
* @return array
*/
public function getRangeForExpression($expression, $max)
{
$values = array();
if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) {
if (!$this->isIncrementsOfRanges($expression)) {
list ($offset, $to) = explode('-', $expression);
$stepSize = 1;
}
else {
$range = array_map('trim', explode('/', $expression, 2));
$stepSize = isset($range[1]) ? $range[1] : 0;
$range = $range[0];
$range = explode('-', $range, 2);
$offset = $range[0];
$to = isset($range[1]) ? $range[1] : $max;
}
$offset = $offset == '*' ? 0 : $offset;
for ($i = $offset; $i <= $to; $i += $stepSize) {
$values[] = $i;
}
sort($values);
}
else {
$values = array($expression);
}
return $values;
}
}

View File

@ -1,104 +0,0 @@
<?php
namespace schedule\console;
use InvalidArgumentException;
use think\Container;
class CallbackEvent extends Event
{
protected $callback;
protected $parameters;
public function __construct($callback, array $parameters = [])
{
if (! is_string($callback) && ! is_callable($callback) && ! is_array($callback)) {
throw new InvalidArgumentException(
'Invalid scheduled callback event. Must be a string or callable.'
);
}
$this->callback = $callback;
$this->parameters = $parameters;
}
/**
* Run the given event.
*
* @param \think\Container $container
* @return mixed
*
* @throws \Exception
*/
public function run(Container $container)
{
$this->callBeforeCallbacks($container);
try {
if(is_object($this->callback)){
$response = $container->invokeFunction($this->callback, $this->parameters);
// $response = $container->invoke([$this->callback, 'doJob'], $this->parameters);
}else if(is_array($this->callback)){
$response = $container->invoke([$this->callback[0], $this->callback[1] ?? 'doJob'], $this->parameters);
}else{
$response = $container->invokeFunction($this->callback, $this->parameters);
}
// $response = is_object($this->callback)
// ? $container->invoke([$this->callback, 'doJob'], $this->parameters)
// : $container->invokeFunction($this->callback, $this->parameters);
// $container->make($this->callback)->invokeFunction('doJob', $this->parameters);
// $response = $container->invokeFunction($this->callback, $this->parameters);
} finally {
parent::callAfterCallbacks($container);
}
return $response;
}
public function isCallable($var, $syntaxOnly = false)
{
if(is_array($var)){
return true;
}
if (! is_array($var)) {
return is_callable($var, $syntaxOnly);
}
if ((! isset($var[0]) || ! isset($var[1])) ||
! is_string($var[1] ?? null)) {
return false;
}
if ($syntaxOnly &&
(is_string($var[0]) || is_object($var[0])) &&
is_string($var[1])) {
return true;
}
$class = is_object($var[0]) ? get_class($var[0]) : $var[0];
$method = $var[1];
if (! class_exists($class)) {
return false;
}
if (method_exists($class, $method)) {
return (new \ReflectionMethod($class, $method))->isPublic();
}
if (is_object($var[0]) && method_exists($class, '__call')) {
return (new \ReflectionMethod($class, '__call'))->isPublic();
}
if (! is_object($var[0]) && method_exists($class, '__callStatic')) {
return (new \ReflectionMethod($class, '__callStatic'))->isPublic();
}
return false;
}
}

View File

@ -1,84 +0,0 @@
<?php
namespace schedule\console;
use think\Queue;
use think\console\Command as ThinkCommand;
use think\Container;
use think\console\Input;
use think\console\Output;
class Command extends ThinkCommand
{
protected $app;
public $events = [];
public function __construct($name = null)
{
parent::__construct();
$this->app = Container::getInstance();
}
public function call($callback, array $parameters = [])
{
$this->events[] = $event = new CallbackEvent(
$callback, $parameters
);
return $event;
}
public function command($command, array $parameters = [])
{
$this->events[] = $event = new Event(
$command, $parameters
);
return $event;
}
public function job($job, $data, $queue = null)
{
return $this->call(function ($data) use ($job, $queue) {
Queue::push($job, $data, $queue);
}, [ $data ]);
}
/*
$this->command('article:pushed')->dailyAt("21:00");
/*$this->call(function () use ($input, $output){
echo '-------------';
echo 11;
})->twiceDaily(9, 20);
*/
protected function execute(Input $input, Output $output)
{
$eventsRan = false;
foreach ($this->dueEvents($this->events) as $event) {
//foreach ($this->events as $event) {
if (! $event->filtersPass($this->app)) {
continue;
}
$event->run($this->app);
$eventsRan = true;
}
if (! $eventsRan) {
$output->writeln('No scheduled commands are ready to run.');
}
}
public function dueEvents($app)
{
return collect($this->events)->filter(function($event) use ($app){
return $event->isDue($app);
});
}
}

View File

@ -1,390 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use RuntimeException;
/**
* CRON expression parser that can determine whether or not a CRON expression is
* due to run, the next run date and previous run date of a CRON expression.
* The determinations made by this class are accurate if checked run once per
* minute (seconds are dropped from date time comparisons).
*
* Schedule parts must map to:
* minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week
* [1-7|MON-SUN], and an optional year.
*
* @link http://en.wikipedia.org/wiki/Cron
*/
class CronExpression
{
const MINUTE = 0;
const HOUR = 1;
const DAY = 2;
const MONTH = 3;
const WEEKDAY = 4;
const YEAR = 5;
/**
* @var array CRON expression parts
*/
private $cronParts;
/**
* @var FieldFactory CRON field factory
*/
private $fieldFactory;
/**
* @var int Max iteration count when searching for next run date
*/
private $maxIterationCount = 1000;
/**
* @var array Order in which to test of cron parts
*/
private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE);
/**
* Factory method to create a new CronExpression.
*
* @param string $expression The CRON expression to create. There are
* several special predefined values which can be used to substitute the
* CRON expression:
*
* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 *
* `@monthly` - Run once a month, midnight, first of month - 0 0 1 * *
* `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0
* `@daily` - Run once a day, midnight - 0 0 * * *
* `@hourly` - Run once an hour, first minute - 0 * * * *
* @param FieldFactory $fieldFactory Field factory to use
*
* @return CronExpression
*/
public static function factory($expression, FieldFactory $fieldFactory = null)
{
$mappings = array(
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@hourly' => '0 * * * *'
);
if (isset($mappings[$expression])) {
$expression = $mappings[$expression];
}
return new static($expression, $fieldFactory ?: new FieldFactory());
}
/**
* Validate a CronExpression.
*
* @param string $expression The CRON expression to validate.
*
* @return bool True if a valid CRON expression was passed. False if not.
* @see \Cron\CronExpression::factory
*/
public static function isValidExpression($expression)
{
try {
self::factory($expression);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Parse a CRON expression
*
* @param string $expression CRON expression (e.g. '8 * * * *')
* @param FieldFactory $fieldFactory Factory to create cron fields
*/
public function __construct($expression, FieldFactory $fieldFactory)
{
$this->fieldFactory = $fieldFactory;
$this->setExpression($expression);
}
/**
* Set or change the CRON expression
*
* @param string $value CRON expression (e.g. 8 * * * *)
*
* @return CronExpression
* @throws \InvalidArgumentException if not a valid CRON expression
*/
public function setExpression($value)
{
$this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
if (count($this->cronParts) < 5) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
foreach ($this->cronParts as $position => $part) {
$this->setPart($position, $part);
}
return $this;
}
/**
* Set part of the CRON expression
*
* @param int $position The position of the CRON expression to set
* @param string $value The value to set
*
* @return CronExpression
* @throws \InvalidArgumentException if the value is not valid for the part
*/
public function setPart($position, $value)
{
if (!$this->fieldFactory->getField($position)->validate($value)) {
throw new InvalidArgumentException(
'Invalid CRON field value ' . $value . ' at position ' . $position
);
}
$this->cronParts[$position] = $value;
return $this;
}
/**
* Set max iteration count for searching next run dates
*
* @param int $maxIterationCount Max iteration count when searching for next run date
*
* @return CronExpression
*/
public function setMaxIterationCount($maxIterationCount)
{
$this->maxIterationCount = $maxIterationCount;
return $this;
}
/**
* Get a next run date relative to the current date or a specific date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning a
* matching next run date. 0, the default, will return the current
* date and time if the next run date falls on the current date and
* time. Setting this value to 1 will skip the first match and go to
* the second match. Setting this value to 2 will skip the first 2
* matches and so on.
* @param bool $allowCurrentDate Set to TRUE to return the current date if
* it matches the cron expression.
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
*/
public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
{
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate);
}
/**
* Get a previous run date relative to the current date or a specific date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
* @see \Cron\CronExpression::getNextRunDate
*/
public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
{
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate);
}
/**
* Get multiple run dates starting at the current date or a specific date
*
* @param int $total Set the total number of dates to calculate
* @param string|\DateTime $currentTime Relative calculation date
* @param bool $invert Set to TRUE to retrieve previous dates
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
*
* @return array Returns an array of run dates
*/
public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false)
{
$matches = array();
for ($i = 0; $i < max(0, $total); $i++) {
try {
$matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate);
} catch (RuntimeException $e) {
break;
}
}
return $matches;
}
/**
* Get all or part of the CRON expression
*
* @param string $part Specify the part to retrieve or NULL to get the full
* cron schedule string.
*
* @return string|null Returns the CRON expression, a part of the
* CRON expression, or NULL if the part was specified but not found
*/
public function getExpression($part = null)
{
if (null === $part) {
return implode(' ', $this->cronParts);
} elseif (array_key_exists($part, $this->cronParts)) {
return $this->cronParts[$part];
}
return null;
}
/**
* Helper method to output the full expression.
*
* @return string Full CRON expression
*/
public function __toString()
{
return $this->getExpression();
}
/**
* Determine if the cron is due to run based on the current date or a
* specific date. This method assumes that the current number of
* seconds are irrelevant, and should be called once per minute.
*
* @param string|\DateTime $currentTime Relative calculation date
*
* @return bool Returns TRUE if the cron is due to run or FALSE if not
*/
public function isDue($currentTime = 'now')
{
if ('now' === $currentTime) {
$currentDate = date('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} elseif ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
// Ensure time in 'current' timezone is used
$currentDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
$currentDate = $currentDate->format('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
$currentDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
$currentDate = $currentDate->format('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} else {
$currentTime = new DateTime($currentTime);
$currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0);
$currentDate = $currentTime->format('Y-m-d H:i');
$currentTime = $currentTime->getTimeStamp();
}
try {
return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime;
} catch (Exception $e) {
return false;
}
}
/**
* Get the next or previous run date of the expression relative to a date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $invert Set to TRUE to go backwards in time
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
*/
protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false)
{
if ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
$currentDate->setTimezone($currentTime->getTimezone());
} else {
$currentDate = new DateTime($currentTime ?: 'now');
$currentDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
}
$currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
$nextRun = clone $currentDate;
$nth = (int) $nth;
// We don't have to satisfy * or null fields
$parts = array();
$fields = array();
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
continue;
}
$parts[$position] = $part;
$fields[$position] = $this->fieldFactory->getField($position);
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; $i++) {
foreach ($parts as $position => $part) {
$satisfied = false;
// Get the field object used to validate this part
$field = $fields[$position];
// Check if this is singular or a list
if (strpos($part, ',') === false) {
$satisfied = $field->isSatisfiedBy($nextRun, $part);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart)) {
$satisfied = true;
break;
}
}
}
// If the field is not satisfied, then start over
if (!$satisfied) {
$field->increment($nextRun, $invert, $part);
continue 2;
}
}
// Skip this match if needed
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
$this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null);
continue;
}
return $nextRun;
}
// @codeCoverageIgnoreStart
throw new RuntimeException('Impossible CRON expression');
// @codeCoverageIgnoreEnd
}
}

View File

@ -1,173 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
/**
* Day of month field. Allows: * , / - ? L W
*
* 'L' stands for "last" and specifies the last day of the month.
*
* The 'W' character is used to specify the weekday (Monday-Friday) nearest the
* given day. As an example, if you were to specify "15W" as the value for the
* day-of-month field, the meaning is: "the nearest weekday to the 15th of the
* month". So if the 15th is a Saturday, the trigger will fire on Friday the
* 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If
* the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you
* specify "1W" as the value for day-of-month, and the 1st is a Saturday, the
* trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary
* of a month's days. The 'W' character can only be specified when the
* day-of-month is a single day, not a range or list of days.
*
* @author Michael Dowling <mtdowling@gmail.com>
*/
class DayOfMonthField extends AbstractField
{
/**
* Get the nearest day of the week for a given day in a month
*
* @param int $currentYear Current year
* @param int $currentMonth Current month
* @param int $targetDay Target day of the month
*
* @return \DateTime Returns the nearest date
*/
private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
{
$tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday");
$currentWeekday = (int) $target->format('N');
if ($currentWeekday < 6) {
return $target;
}
$lastDayOfMonth = $target->format('t');
foreach (array(-1, 1, -2, 2) as $i) {
$adjusted = $targetDay + $i;
if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
$target->setDate($currentYear, $currentMonth, $adjusted);
if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
return $target;
}
}
}
}
public function isSatisfiedBy(DateTime $date, $value)
{
// ? states that the field value is to be skipped
if ($value == '?') {
return true;
}
$fieldValue = $date->format('d');
// Check to see if this is the last day of the month
if ($value == 'L') {
return $fieldValue == $date->format('t');
}
// Check to see if this is the nearest weekday to a particular value
if (strpos($value, 'W')) {
// Parse the target day
$targetDay = substr($value, 0, strpos($value, 'W'));
// Find out if the current day is the nearest day of the week
return $date->format('j') == self::getNearestWeekday(
$date->format('Y'),
$date->format('m'),
$targetDay
)->format('j');
}
return $this->isSatisfied($date->format('d'), $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('previous day');
$date->setTime(23, 59);
} else {
$date->modify('next day');
$date->setTime(0, 0);
}
return $this;
}
/**
* Validates that the value is valid for the Day of the Month field
* Days of the month can contain values of 1-31, *, L, or ? by default. This can be augmented with lists via a ',',
* ranges via a '-', or with a '[0-9]W' to specify the closest weekday.
*
* @param string $value
* @return bool
*/
public function validate($value)
{
// Allow wildcards and a single L
if ($value === '?' || $value === '*' || $value === 'L') {
return true;
}
// If you only contain numbers and are within 1-31
if ((bool) preg_match('/^\d{1,2}$/', $value) && ($value >= 1 && $value <= 31)) {
return true;
}
// If you have a -, we will deal with each of your chunks
if ((bool) preg_match('/-/', $value)) {
// We cannot have a range within a list or vice versa
if ((bool) preg_match('/,/', $value)) {
return false;
}
$chunks = explode('-', $value);
foreach ($chunks as $chunk) {
if (!$this->validate($chunk)) {
return false;
}
}
return true;
}
// If you have a comma, we will deal with each value
if ((bool) preg_match('/,/', $value)) {
// We cannot have a range within a list or vice versa
if ((bool) preg_match('/-/', $value)) {
return false;
}
$chunks = explode(',', $value);
foreach ($chunks as $chunk) {
if (!$this->validate($chunk)) {
return false;
}
}
return true;
}
// If you contain a /, we'll deal with it
if ((bool) preg_match('/\//', $value)) {
$chunks = explode('/', $value);
foreach ($chunks as $chunk) {
if (!$this->validate($chunk)) {
return false;
}
}
return true;
}
// If you end in W, make sure that it has a numeric in front of it
if ((bool) preg_match('/^\d{1,2}W$/', $value)) {
return true;
}
return false;
}
}

View File

@ -1,141 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
use InvalidArgumentException;
/**
* Day of week field. Allows: * / , - ? L #
*
* Days of the week can be represented as a number 0-7 (0|7 = Sunday)
* or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT.
*
* 'L' stands for "last". It allows you to specify constructs such as
* "the last Friday" of a given month.
*
* '#' is allowed for the day-of-week field, and must be followed by a
* number between one and five. It allows you to specify constructs such as
* "the second Friday" of a given month.
*/
class DayOfWeekField extends AbstractField
{
public function isSatisfiedBy(DateTime $date, $value)
{
if ($value == '?') {
return true;
}
// Convert text day of the week values to integers
$value = $this->convertLiterals($value);
$currentYear = $date->format('Y');
$currentMonth = $date->format('m');
$lastDayOfMonth = $date->format('t');
// Find out if this is the last specific weekday of the month
if (strpos($value, 'L')) {
$weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L')));
$tdate = clone $date;
$tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
while ($tdate->format('w') != $weekday) {
$tdateClone = new DateTime();
$tdate = $tdateClone
->setTimezone($tdate->getTimezone())
->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
}
return $date->format('j') == $lastDayOfMonth;
}
// Handle # hash tokens
if (strpos($value, '#')) {
list($weekday, $nth) = explode('#', $value);
// 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601
if ($weekday === '0') {
$weekday = 7;
}
// Validate the hash fields
if ($weekday < 0 || $weekday > 7) {
throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given");
}
if ($nth > 5) {
throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month');
}
// The current weekday must match the targeted weekday to proceed
if ($date->format('N') != $weekday) {
return false;
}
$tdate = clone $date;
$tdate->setDate($currentYear, $currentMonth, 1);
$dayCount = 0;
$currentDay = 1;
while ($currentDay < $lastDayOfMonth + 1) {
if ($tdate->format('N') == $weekday) {
if (++$dayCount >= $nth) {
break;
}
}
$tdate->setDate($currentYear, $currentMonth, ++$currentDay);
}
return $date->format('j') == $currentDay;
}
// Handle day of the week values
if (strpos($value, '-')) {
$parts = explode('-', $value);
if ($parts[0] == '7') {
$parts[0] = '0';
} elseif ($parts[1] == '0') {
$parts[1] = '7';
}
$value = implode('-', $parts);
}
// Test to see which Sunday to use -- 0 == 7 == Sunday
$format = in_array(7, str_split($value)) ? 'N' : 'w';
$fieldValue = $date->format($format);
return $this->isSatisfied($fieldValue, $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('-1 day');
$date->setTime(23, 59, 0);
} else {
$date->modify('+1 day');
$date->setTime(0, 0, 0);
}
return $this;
}
public function validate($value)
{
$value = $this->convertLiterals($value);
foreach (explode(',', $value) as $expr) {
if (!preg_match('/^(\*|[0-7](L?|#[1-5]))([\/\,\-][0-7]+)*$/', $expr)) {
return false;
}
}
return true;
}
private function convertLiterals($string)
{
return str_ireplace(
array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'),
range(0, 6),
$string
);
}
}

View File

@ -1,123 +0,0 @@
<?php
namespace schedule\console;
use Carbon\Carbon;
use think\Container;
class Event
{
use ManagesFrequencies;
public $command;
protected $parameters;
public $timezone;
public $expression = '* * * * * *';
protected $filters = [];
protected $beforeCallbacks = [];
protected $afterCallbacks = [];
public function __construct($command, array $parameters = [])
{
$this->command = $command;
$this->parameters = $parameters;
}
public function run(Container $container)
{
$this->callBeforeCallbacks($container);
if (strpos(\think\App::VERSION, '6.0') !== false) {
\think\facade\Console::call($this->command, $this->parameters, 'console');
}else{
\think\Console::call($this->command, $this->parameters, 'console');
}
$this->callAfterCallbacks($container);
}
public function filtersPass($app)
{
foreach ($this->filters as $callback) {
if (! $app->call($callback)) {
return false;
}
}
return true;
}
public function isDue($app)
{
return $this->expressionPasses();
}
public function expressionPasses()
{
$date = Carbon::now();
if ($this->timezone) {
$date->setTimezone($this->timezone);
}
return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
}
public function when($callback)
{
$this->filters[] = is_callable($callback) ? $callback : function () use ($callback) {
return $callback;
};
return $this;
}
public function before(Closure $callback)
{
$this->beforeCallbacks[] = $callback;
return $this;
}
public function after(Closure $callback)
{
return $this->then($callback);
}
public function then(Closure $callback)
{
$this->afterCallbacks[] = $callback;
return $this;
}
public function callBeforeCallbacks(Container $container)
{
foreach ($this->beforeCallbacks as $callback) {
$container->invokeFunction($callback);
}
}
public function callAfterCallbacks(Container $container)
{
foreach ($this->afterCallbacks as $callback) {
$container->invokeFunction($callback);
}
}
public function timezone($timezone)
{
$this->timezone = $timezone;
return $this;
}
}

View File

@ -1,58 +0,0 @@
<?php
namespace schedule\console;
use InvalidArgumentException;
/**
* CRON field factory implementing a flyweight factory
* @link http://en.wikipedia.org/wiki/Cron
*/
class FieldFactory
{
/**
* @var array Cache of instantiated fields
*/
private $fields = array();
/**
* Get an instance of a field object for a cron expression position
*
* @param int $position CRON expression position value to retrieve
*
* @return FieldInterface
* @throws InvalidArgumentException if a position is not valid
*/
public function getField($position)
{
if (!isset($this->fields[$position])) {
switch ($position) {
case 0:
$this->fields[$position] = new MinutesField();
break;
case 1:
$this->fields[$position] = new HoursField();
break;
case 2:
$this->fields[$position] = new DayOfMonthField();
break;
case 3:
$this->fields[$position] = new MonthField();
break;
case 4:
$this->fields[$position] = new DayOfWeekField();
break;
case 5:
$this->fields[$position] = new YearField();
break;
default:
throw new InvalidArgumentException(
$position . ' is not a valid position'
);
}
}
return $this->fields[$position];
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
/**
* CRON field interface
*/
interface FieldInterface
{
/**
* Check if the respective value of a DateTime field satisfies a CRON exp
*
* @param DateTime $date DateTime object to check
* @param string $value CRON expression to test against
*
* @return bool Returns TRUE if satisfied, FALSE otherwise
*/
public function isSatisfiedBy(DateTime $date, $value);
/**
* When a CRON expression is not satisfied, this method is used to increment
* or decrement a DateTime object by the unit of the cron field
*
* @param DateTime $date DateTime object to change
* @param bool $invert (optional) Set to TRUE to decrement
*
* @return FieldInterface
*/
public function increment(DateTime $date, $invert = false);
/**
* Validates a CRON expression for a given field
*
* @param string $value CRON expression value to validate
*
* @return bool Returns TRUE if valid, FALSE otherwise
*/
public function validate($value);
}

View File

@ -1,71 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
use DateTimeZone;
/**
* Hours field. Allows: * , / -
*/
class HoursField extends AbstractField
{
public function isSatisfiedBy(DateTime $date, $value)
{
return $this->isSatisfied($date->format('H'), $value);
}
public function increment(DateTime $date, $invert = false, $parts = null)
{
// Change timezone to UTC temporarily. This will
// allow us to go back or forwards and hour even
// if DST will be changed between the hours.
if (is_null($parts) || $parts == '*') {
$timezone = $date->getTimezone();
$date->setTimezone(new DateTimeZone('UTC'));
if ($invert) {
$date->modify('-1 hour');
} else {
$date->modify('+1 hour');
}
$date->setTimezone($timezone);
$date->setTime($date->format('H'), $invert ? 59 : 0);
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$hours = array();
foreach ($parts as $part) {
$hours = array_merge($hours, $this->getRangeForExpression($part, 23));
}
$current_hour = $date->format('H');
$position = $invert ? count($hours) - 1 : 0;
if (count($hours) > 1) {
for ($i = 0; $i < count($hours) - 1; $i++) {
if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) ||
($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
$hour = $hours[$position];
if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) {
$date->modify(($invert ? '-' : '+') . '1 day');
$date->setTime($invert ? 23 : 0, $invert ? 59 : 0);
}
else {
$date->setTime($hour, $invert ? 59 : 0);
}
return $this;
}
public function validate($value)
{
return (bool) preg_match('/^[\*,\/\-0-9]+$/', $value);
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
/**
* Minutes field. Allows: * , / -
*/
class MinutesField extends AbstractField
{
public function isSatisfiedBy(DateTime $date, $value)
{
return $this->isSatisfied($date->format('i'), $value);
}
public function increment(DateTime $date, $invert = false, $parts = null)
{
if (is_null($parts)) {
if ($invert) {
$date->modify('-1 minute');
} else {
$date->modify('+1 minute');
}
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$minutes = array();
foreach ($parts as $part) {
$minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));
}
$current_minute = $date->format('i');
$position = $invert ? count($minutes) - 1 : 0;
if (count($minutes) > 1) {
for ($i = 0; $i < count($minutes) - 1; $i++) {
if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) ||
($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) {
$date->modify(($invert ? '-' : '+') . '1 hour');
$date->setTime($date->format('H'), $invert ? 59 : 0);
}
else {
$date->setTime($date->format('H'), $minutes[$position]);
}
return $this;
}
public function validate($value)
{
return (bool) preg_match('/^[\*,\/\-0-9]+$/', $value);
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
/**
* Month field. Allows: * , / -
*/
class MonthField extends AbstractField
{
public function isSatisfiedBy(DateTime $date, $value)
{
// Convert text month values to integers
$value = str_ireplace(
array(
'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'
),
range(1, 12),
$value
);
return $this->isSatisfied($date->format('m'), $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('last day of previous month');
$date->setTime(23, 59);
} else {
$date->modify('first day of next month');
$date->setTime(0, 0);
}
return $this;
}
public function validate($value)
{
return (bool) preg_match('/^[\*,\/\-0-9A-Z]+$/', $value);
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace schedule\console;
use DateTime;
/**
* Year field. Allows: * , / -
*/
class YearField extends AbstractField
{
public function isSatisfiedBy(DateTime $date, $value)
{
return $this->isSatisfied($date->format('Y'), $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('-1 year');
$date->setDate($date->format('Y'), 12, 31);
$date->setTime(23, 59, 0);
} else {
$date->modify('+1 year');
$date->setDate($date->format('Y'), 1, 1);
$date->setTime(0, 0, 0);
}
return $this;
}
public function validate($value)
{
return (bool) preg_match('/^[\*,\/\-0-9]+$/', $value);
}
}

View File

@ -1,8 +0,0 @@
<?php
namespace schedule\exceptions;
class Exception extends \think\Exception
{
}

Some files were not shown because too many files have changed in this diff Show More