├── .gitignore
├── LICENSE
├── README.md
├── composer.json
└── src
├── Adapters
├── AbstractImage.php
├── Gd.php
├── Gmagick.php
├── ImageAdapter.php
└── Imagick.php
├── Base
├── Box.php
├── BoxInterface.php
├── Font.php
├── FontInterface.php
├── Matrix.php
├── Point.php
└── PointInterface.php
├── Captcha.php
├── Colors.php
├── HintCaptcha.php
├── ICaptcha.php
├── Ico.php
├── Image.php
├── ImageCompare.php
├── ImageHelper.php
├── ImageManager.php
├── Node
├── BaseNode.php
├── BorderNode.php
├── BoxNode.php
├── CircleNode.php
├── ImgNode.php
├── LineNode.php
├── NodeHelper.php
├── RectNode.php
└── TextNode.php
├── QrCode.php
├── Renderer
└── QrCodeImageRenderer.php
├── SlideCaptcha.php
├── ThumbImage.php
└── WaterMark.php
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.phar
2 | /vendor/
3 |
4 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
5 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
6 | # composer.lock
7 | /.idea
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 zx648383079
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # image
2 | 使用 gd 对图片处理的封装,包含 验证码、水印、缩略图、二维码、图片拖拽验证
3 |
4 | ## 当前状态:一直开发中
5 |
6 | ## 简单使用教程
7 |
8 | [验证码](#captcha)
9 |
10 | [滑动验证码](#slider)
11 |
12 | [文字点击验证](#hint)
13 |
14 | [二维码](#qr)
15 |
16 | [水印](#water)
17 |
18 | [缩略图](#thumb)
19 |
20 | [图片比较(简单版)](#compare)
21 |
22 | [内容生成图片](#draw)
23 |
24 |
25 | ### 使用 iconfont 等类似字体图标
26 |
27 | ```php
28 | $image = new Image();
29 | $res = $image->instance();
30 | $res->create(ImageManager::createSize(200, 200));
31 | $res->fill('#fff');
32 | $res->text("\u{e709}", ImageManager::createFont(app_path('data/fonts/iconfont.ttf')), ImageManager::createPoint(100, 100));
33 | $image->show();
34 | ```
35 |
36 | ** 请注意:`\u{e709}` 表示一个字符,且字符串必须用双引号 **
37 |
38 | ### ico 生成
39 |
40 | ```php
41 | use Zodream\Image\Ico;
42 |
43 | $image = new Ico('1.png');
44 | $image->saveAsSize('1.ico', $image->getSizes());
45 | ```
46 |
47 |
48 | ### 验证码
49 |
50 | ```PHP
51 | use Zodream\Image\Captcha;
52 |
53 | $captcha = new Captcha();
54 | $captcha->setConfigs([
55 | 'width' => 200,
56 | 'fontSize' => 20,
57 | 'fontFamily' => 'Ubuntu_regular.ttf'
58 | ]);
59 | $source = $captcha->generate();
60 |
61 | $captcha->verify($_POST['captcha'], $source);
62 | ```
63 |
64 | 默认配置
65 |
66 | ```PHP
67 |
68 | [
69 | 'characters' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', //随机因子
70 | 'length' => 4, //验证码长度
71 | 'fontSize' => 0, //指定字体大小
72 | 'fontColor' => '', //指定字体颜色, 可以是数组
73 | 'fontFamily' => null, //指定的字体
74 | 'width' => 100, // 图片宽
75 | 'height' => 30, // 图片高
76 | 'angle' => 0, //角度
77 | 'sensitive' => true, // 大小写敏感
78 | 'mode' => 0 // 验证码模式: 0 文字 1 公式
79 | ]
80 |
81 | ```
82 |
83 |
84 | ### 二维码
85 |
86 | ```PHP
87 | use Zodream\Image\QrCode;
88 |
89 | $qr = new QrCode();
90 | $image = $qr->encode('123123');
91 | $image->save();
92 |
93 | ```
94 |
95 |
96 | ### 水印
97 |
98 | 文字水印
99 |
100 | ```php
101 | $image = new WaterMark();
102 | $image->instance()->loadResource('1.jpg');
103 | $image->addTextByDirection('water');
104 | ```
105 |
106 | 自定义水印
107 | ```php
108 | $image = new WaterMark();
109 | $image->instance()->loadResource($file);
110 | $font = new Font((string)app_path(config('disk.font')), 12, '#fff');
111 | $textBox = $image->instance()->fontSize($text, $font);
112 | // 根据文字的尺寸获取水印的位置
113 | list($x, $y) = $image->getPointByDirection(WaterMark::RightTop, $textBox->getWidth(), $textBox->getHeight(), 20);
114 | // 给文字添加阴影
115 | $image->addText($text, $x + 2, $y + 2, $font->getSize(), '#777', $font->getFile());
116 | $image->addText($text, $x, $y, $font->getSize(), $font->getColor(), $font->getFile());
117 | ```
118 |
119 |
120 | ### 滑动验证码
121 |
122 | ```PHP
123 | use Zodream\Image\SlideCaptcha;
124 |
125 | $captcha = new SlideCaptcha();
126 | $captcha->setConfigs([
127 | 'width' => 300,
128 | 'height' => 130,
129 | ]);
130 | $captcha->instance()->open('bg.jpg');
131 | $captcha->setShape('shape.jpg'); // 根据图片抠图
132 | $source = $captcha->generate();
133 |
134 | $captcha->verify($_POST['captcha'], $source);
135 |
136 | $imgData = $captcha->toArray();
137 | $html = '';
138 | foreach ($imgData['imageItems'] as $point) {
139 | $html .= sprintf('
', $point['x'], $point['y']);
140 | }
141 |
142 | $html = <<
144 | .slide-box {
145 | width: {$imgData['width']}px;
146 | height: {$imgData['height']}px;
147 | position: relative;
148 | }
149 | .slide-box .slide-img {
150 | float: left;
151 | margin: 0;
152 | padding: 0;
153 | background-image: url({$imgData['image']});
154 | background-repeat: no-repeat;
155 | width: {$imgData['imageItems'][0]['width']}px;
156 | height: {$imgData['imageItems'][0]['height']}px;
157 | }
158 | .slide-box .slide-cut {
159 | position: absolute;
160 | top: {$imgData['controlY']}px;
161 | background-image: url({$imgData['control']});
162 | background-repeat: no-repeat;
163 | width: {$imgData['controlWidth']}px;
164 | height: {$imgData['controlHeight']}px;
165 | z-index: 9;
166 | }
167 |
168 |
169 |
170 |
171 | {$html}
172 |
173 |
174 |
175 | HTML;
176 |
177 | ```
178 |
179 |
180 | ### 点击验证码
181 | 依次点击图片上的文字
182 |
183 | ```php
184 | $captcha = new HintCaptcha();
185 | $items = ['我', '就', '你', '哈'];
186 | $captcha->setConfigs([
187 | 'width' => 300,
188 | 'height' => 130,
189 | 'fontSize' => 20,
190 | 'fontFamily' => 'Yahei.ttf',
191 | 'words' => $items,
192 | 'count' => 3,
193 | ]);
194 | $captcha->instance()->open('images/banner.jpg');
195 | $source = $captcha->generate();
196 |
197 | $captcha->verify($_POST['captcha'], $source);
198 |
199 | $imgData = $captcha->toArray();
200 | ```
201 |
202 |
203 |
204 |
205 | ## 内容生成图片
206 |
207 | ```php
208 | $str = << 10,
227 | 'background' => 'white',
228 | 'width' => 470
229 | ])->append(
230 | ImgNode::create($img, [
231 | 'width' => '100%',
232 | 'height' => '100%'
233 | ]),
234 | TextNode::create('sbfajahaa', [
235 | 'size' => 20,
236 | 'letterSpace' => 20,
237 | 'padding' => [
238 | 10,
239 | 0,
240 | ],
241 | 'bold' => true,
242 | 'font' => $font
243 | ]),
244 | TextNode::create('1234avccg', [
245 | 'size' => 12,
246 | 'font' => $font,
247 | 'letterSpace' => 4,
248 | 'lineSpace' => 4,
249 | 'color' => '#ccc'
250 | ]),
251 | ImgNode::create($img, [
252 | 'width' => '100',
253 | 'height' => '100',
254 | 'center' => true
255 | ]),
256 | TextNode::create('sbfajahaa', [
257 | 'size' => 12,
258 | 'color' => '#ccc',
259 | 'letterSpace' => 4,
260 | 'lineSpace' => 4,
261 | 'wrap' => false,
262 | 'font' => $font,
263 | 'center' => true
264 | ]),
265 | BorderNode::create([
266 | 'size' => 1,
267 | 'fixed' => true,
268 | 'margin' => 10
269 | ]),
270 | LineNode::create(10, 10, 10, 100, [
271 | 'size' => 1,
272 | 'fixed' => true,
273 | 'color' => 'black'
274 | ]),
275 | RectNode::create([
276 | 'points' => [
277 | [0, 0],
278 | [200, 0],
279 | [0, 200],
280 | ],
281 | 'color' => 'black'
282 | ])
283 | );
284 | $box->beginDraw()->show();
285 | ```
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zodream/image",
3 | "description": "image, captcha",
4 | "keywords": ["image gd", "zodream"],
5 | "homepage": "https://github.com/zodream/image",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name" : "Jason Zou",
10 | "email" : "zodream@fixmail.com",
11 | "homepage" : "https://www.zodream.cn/",
12 | "role" : "Developer"
13 | }
14 | ],
15 | "require": {
16 | "php": ">=8.2",
17 | "khanamiryan/qrcode-detector-decoder": "^2.0.2",
18 | "bacon/bacon-qr-code": "^2.0.8"
19 | },
20 | "suggest": {
21 | "ext-gd": "*",
22 | "ext-imagick": "*",
23 | "ext-gmagick": "*"
24 | },
25 | "autoload": {
26 | "psr-4": {
27 | "Zodream\\Image\\": "src/"
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/Adapters/AbstractImage.php:
--------------------------------------------------------------------------------
1 | array(
12 | 'jpg',
13 | 'jpeg',
14 | 'jpe',
15 | 'jpc',
16 | 'jpeg2000',
17 | 'jp2',
18 | 'jb2'
19 | ),
20 | 'webp' => 'webp',
21 | 'png' => 'png',
22 | 'gif' => 'gif',
23 | 'wbmp' => 'wbmp',
24 | 'xbm' => 'xbm',
25 | 'gd' => 'gd',
26 | 'gd2' => 'gd2'
27 | );
28 |
29 | protected string|null $file = null;
30 |
31 | protected int $width;
32 |
33 | protected int $height;
34 |
35 | protected string|null $type;
36 |
37 | protected string|null $realType;
38 |
39 | protected $resource;
40 |
41 | public function loadResource(mixed $file) {
42 | if (is_null($file)) {
43 | return $this;
44 | }
45 | if ($file instanceof File) {
46 | $this->open((string)$file);
47 | return $this;
48 | }
49 | if (is_string($file) && is_file($file)) {
50 | $this->open($file);
51 | return $this;
52 | }
53 | if (is_string($file)) {
54 | $this->load($file);
55 | return $this;
56 | }
57 | $this->read($file);
58 | return $this;
59 | }
60 |
61 | /**
62 | * @return mixed
63 | */
64 | public function getResource() {
65 | return $this->resource;
66 | }
67 |
68 | public function setEmptyImage() {
69 | $this->resource = null;
70 | return $this;
71 | }
72 |
73 | public function getHeight(): int {
74 | return $this->height;
75 | }
76 |
77 | public function getWidth(): int {
78 | return $this->width;
79 | }
80 |
81 | public function getSize(): BoxInterface {
82 | return new Box($this->getWidth(), $this->getHeight());
83 | }
84 |
85 | public function getRealType(): string {
86 | return $this->realType;
87 | }
88 |
89 | /**
90 | * 设置真实类型
91 | * @param $type
92 | * @return static
93 | */
94 | public function setRealType(string $type) {
95 | if (empty($type)) {
96 | return $this;
97 | }
98 | foreach (self::ALLOW_TYPES as $key => $item) {
99 | if ((!is_array($item) && $item == $type)
100 | || (is_array($item) && in_array($type, $item))) {
101 | $this->realType = $type;
102 | return $this;
103 | }
104 | }
105 | return $this;
106 | }
107 |
108 | public function save() {
109 | return $this->saveAs($this->file);
110 | }
111 |
112 | public function toBase64(): string {
113 | ob_start ();
114 | $this->saveAs();
115 | $data = ob_get_contents();
116 | ob_end_clean();
117 | return 'data:image/'.$this->getRealType().';base64,'.base64_encode($data);
118 | }
119 | }
--------------------------------------------------------------------------------
/src/Adapters/Gd.php:
--------------------------------------------------------------------------------
1 | getWidth();
26 | $height = $size->getHeight();
27 |
28 | $resource = imagecreatetruecolor($width, $height);
29 |
30 | if (false === $resource) {
31 | throw new RuntimeException('Create operation failed');
32 | }
33 |
34 | if (empty($color)) {
35 | $color = '#fff';
36 | }
37 |
38 | // $index = imagecolorallocatealpha($resource, $color->getRed(), $color->getGreen(), $color->getBlue(), round(127 * (100 - $color->getAlpha()) / 100));
39 | //
40 | // if (false === $index) {
41 | // throw new RuntimeException('Unable to allocate color');
42 | // }
43 | //
44 | // if (false === imagefill($resource, 0, 0, $index)) {
45 | // throw new RuntimeException('Could not set background color fill');
46 | // }
47 | //
48 | // if ($color->getAlpha() <= 5) {
49 | // imagecolortransparent($resource, $index);
50 | // }
51 | $this->resource = $resource;
52 | $this->height = $height;
53 | $this->width = $width;
54 | $this->setRealType('jpeg');
55 | $this->fill($color);
56 | return $this;
57 | }
58 |
59 | public function open(mixed $path) {
60 | $path = (string)$path;
61 | if (!$this->check($path)) {
62 | throw new \Exception('file error');
63 | }
64 | $this->file = $path;
65 | $imageInfo = getimagesize($path);
66 | $this->width = $imageInfo[0];
67 | $this->height = $imageInfo[1];
68 | $this->type = empty($type)
69 | ? image_type_to_extension($imageInfo[2], false)
70 | : $type;
71 | $this->setRealType($this->type);
72 | if (false === $this->realType) {
73 | throw new \Exception('image type error');
74 | }
75 | $resource = call_user_func('imagecreatefrom'.$this->realType, $path);
76 | $this->wrap($resource);
77 | return $this;
78 | }
79 |
80 | public function load($string) {
81 | $this->wrap(imagecreatefromstring($string));
82 | $this->height = imagesy($this->resource);
83 | $this->width = imagesx($this->resource);
84 | return $this;
85 | }
86 |
87 | public function read($resource) {
88 | $this->wrap($resource);
89 | $this->height = imagesy($this->resource);
90 | $this->width = imagesx($this->resource);
91 | return $this;
92 | }
93 |
94 | protected function wrap($resource) {
95 | if (!imageistruecolor($resource)) {
96 | if (\function_exists('imagepalettetotruecolor')) {
97 | if (false === imagepalettetotruecolor($resource)) {
98 | throw new RuntimeException('Could not convert a palette based image to true color');
99 | }
100 | } else {
101 | list($width, $height) = array(imagesx($resource), imagesy($resource));
102 |
103 | // create transparent truecolor canvas
104 | $truecolor = imagecreatetruecolor($width, $height);
105 | $transparent = imagecolorallocatealpha($truecolor, 255, 255, 255, 127);
106 |
107 | imagealphablending($truecolor, false);
108 | imagefilledrectangle($truecolor, 0, 0, $width, $height, $transparent);
109 | imagealphablending($truecolor, false);
110 |
111 | imagecopy($truecolor, $resource, 0, 0, 0, 0, $width, $height);
112 |
113 | imagedestroy($resource);
114 | $resource = $truecolor;
115 | }
116 | }
117 | if (false === imagealphablending($resource, false) || false === imagesavealpha($resource, true)) {
118 | throw new RuntimeException('Could not set alphablending, savealpha and antialias values');
119 | }
120 | if (\function_exists('imageantialias')) {
121 | imageantialias($resource, true);
122 | }
123 | $this->resource = $resource;
124 | if ($this->realType === 'png') {
125 | $this->transparent([0, 0, 0, 1]);
126 | }
127 | }
128 |
129 | final public function crop(PointInterface $start, BoxInterface $size)
130 | {
131 | if (!$start->in($this->getSize())) {
132 | throw new OutOfBoundsException('Crop coordinates must start at minimum 0, 0 position from top left corner, crop height and width must be positive integers and must not exceed the current image borders');
133 | }
134 |
135 | $width = $size->getWidth();
136 | $height = $size->getHeight();
137 |
138 | $dest = $this->createImage($size, 'crop');
139 |
140 | if (false === imagecopy($dest, $this->resource, 0, 0, $start->getX(), $start->getY(), $width, $height)) {
141 | imagedestroy($dest);
142 | throw new RuntimeException('Image crop operation failed');
143 | }
144 |
145 | imagedestroy($this->resource);
146 |
147 | $this->resource = $dest;
148 |
149 | return $this;
150 | }
151 |
152 | final public function paste(ImageAdapter $image, PointInterface $start, int|float $alpha = 100)
153 | {
154 | if (!$image instanceof self) {
155 | throw new InvalidArgumentException(sprintf('Gd\Image can only paste() Gd\Image instances, %s given', get_class($image)));
156 | }
157 |
158 | $alpha = (int) round($alpha);
159 | if ($alpha < 0 || $alpha > 100) {
160 | throw new InvalidArgumentException(sprintf('The %1$s argument can range from %2$d to %3$d, but you specified %4$d.', '$alpha', 0, 100, $alpha));
161 | }
162 |
163 | $size = $image->getSize();
164 |
165 | if ($alpha === 100) {
166 | imagealphablending($this->resource, true);
167 | imagealphablending($image->resource, true);
168 |
169 | $success = imagecopy($this->resource, $image->resource, $start->getX(), $start->getY(), 0, 0, $size->getWidth(), $size->getHeight());
170 |
171 | imagealphablending($this->resource, false);
172 | imagealphablending($image->resource, false);
173 |
174 | if ($success === false) {
175 | throw new RuntimeException('Image paste operation failed');
176 | }
177 | } elseif ($alpha > 0) {
178 | if (false === imagecopymerge(/*dst_im*/$this->resource, /*src_im*/$image->resource, /*dst_x*/$start->getX(), /*dst_y*/$start->getY(), /*src_x*/0, /*src_y*/0, /*src_w*/$size->getWidth(), /*src_h*/$size->getHeight(), /*pct*/$alpha)) {
179 | throw new RuntimeException('Image paste operation failed');
180 | }
181 | }
182 |
183 | return $this;
184 | }
185 |
186 | /**
187 | * 截取一部分图片放在什么位置
188 | * @param ImageAdapter $src 源图
189 | * @param PointInterface $srcStart 源图截取的位置
190 | * @param BoxInterface $srcBox 源图截取的大小
191 | * @param PointInterface $start 放在什么位置
192 | * @param BoxInterface|null $box 是否放大
193 | * @param int $alpha 未实现
194 | * @return Gd
195 | */
196 | public function pastePart(ImageAdapter $src, PointInterface $srcStart, BoxInterface $srcBox, PointInterface $start, BoxInterface|null $box = null, int|float $alpha = 100) {
197 | if (!$src instanceof self) {
198 | throw new InvalidArgumentException(sprintf('Gd\Image can only paste() Gd\Image instances, %s given', get_class($src)));
199 | }
200 |
201 | $alpha = (int) round($alpha);
202 | if ($alpha < 0 || $alpha > 100) {
203 | throw new InvalidArgumentException(sprintf('The %1$s argument can range from %2$d to %3$d, but you specified %4$d.', '$alpha', 0, 100, $alpha));
204 | }
205 | if ($box === null) {
206 | $box = $srcBox;
207 | }
208 |
209 | imagealphablending($this->resource, true);
210 | imagealphablending($src->resource, true);
211 |
212 | $success = imagecopyresampled($this->resource, $src->resource,
213 | $start->getX(), $start->getY(), $srcStart->getX(), $srcStart->getY(),
214 | $box->getWidth(), $box->getHeight(), $srcBox->getWidth(), $srcBox->getHeight());
215 |
216 | imagealphablending($this->resource, false);
217 | imagealphablending($src->resource, false);
218 |
219 | if ($success === false) {
220 | throw new RuntimeException('Image paste operation failed');
221 | }
222 | return $this;
223 | }
224 |
225 | public function thumbnail(BoxInterface $box) {
226 | return $this->scale($box);
227 | }
228 |
229 | final public function resize(BoxInterface $size, $filter = ImageAdapter::FILTER_UNDEFINED)
230 | {
231 | if (ImageAdapter::FILTER_UNDEFINED !== $filter) {
232 | throw new InvalidArgumentException('Unsupported filter type, GD only supports ImageInterface::FILTER_UNDEFINED filter');
233 | }
234 |
235 | $width = $size->getWidth();
236 | $height = $size->getHeight();
237 |
238 | $dest = $this->createImage($size, 'resize');
239 |
240 | imagealphablending($this->resource, true);
241 | imagealphablending($dest, true);
242 |
243 | $success = imagecopyresampled($dest, $this->resource, 0, 0, 0, 0, $width, $height, imagesx($this->resource), imagesy($this->resource));
244 |
245 | imagealphablending($this->resource, false);
246 | imagealphablending($dest, false);
247 |
248 | if ($success === false) {
249 | imagedestroy($dest);
250 | throw new RuntimeException('Image resize operation failed');
251 | }
252 |
253 | imagedestroy($this->resource);
254 |
255 | $this->resource = $dest;
256 |
257 | return $this;
258 | }
259 |
260 | final public function rotate(int|float $angle, mixed $background = null)
261 | {
262 | if ($background === null) {
263 | $background = '#fff';
264 | }
265 | $color = $this->converterToColor($background);
266 | $resource = imagerotate($this->resource, -1 * $angle, $color);
267 |
268 | if (false === $resource) {
269 | throw new RuntimeException('Image rotate operation failed');
270 | }
271 |
272 | imagedestroy($this->resource);
273 | $this->resource = $resource;
274 |
275 | return $this;
276 | }
277 |
278 | public function scale(BoxInterface $box) {
279 | $resource = imagecreatetruecolor($box->getWidth(), $box->getHeight());
280 | $size = $this->getSize();
281 | imagecopyresampled($resource, $this->resource, 0, 0, 0, 0,
282 | $box->getWidth(), $box->getHeight(), $size->getWidth(), $size->getHeight());
283 | $this->close();
284 | $this->read($resource);
285 | return $this;
286 | }
287 |
288 | public function copy() {
289 | return clone $this;
290 | }
291 |
292 | public function fill(mixed $fill) {
293 | $size = $this->getSize();
294 |
295 | if (is_string($fill) || is_array($fill)) {
296 | imagefilledrectangle(
297 | $this->resource,
298 | 0,
299 | $size->getHeight(),
300 | $size->getWidth(),
301 | 0,
302 | $this->converterToColor($fill)
303 | );
304 | return $this;
305 | }
306 | if (!$fill instanceof ImageAdapter) {
307 | throw new RuntimeException('Fill operation failed');
308 | }
309 | for ($x = 0, $width = $size->getWidth(); $x < $width; $x++) {
310 | for ($y = 0, $height = $size->getHeight(); $y < $height; $y++) {
311 | if (false === imagesetpixel($this->resource, $x, $y,
312 | $fill->getColorAt(new Point($x, $y)))) {
313 | throw new RuntimeException('Fill operation failed');
314 | }
315 | }
316 | }
317 | return $this;
318 | }
319 |
320 |
321 | public function arc(PointInterface $center, BoxInterface $size, int $start, int $end, $color, int $thickness = 1)
322 | {
323 | $thickness = max(0, (int) round($thickness));
324 | if ($thickness === 0) {
325 | return $this;
326 | }
327 | imagesetthickness($this->resource, $thickness);
328 |
329 | if (false === imagealphablending($this->resource, true)) {
330 | throw new RuntimeException('Draw arc operation failed');
331 | }
332 |
333 | if (false === imagearc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->converterToColor($color))) {
334 | imagealphablending($this->resource, false);
335 | throw new RuntimeException('Draw arc operation failed');
336 | }
337 |
338 | if (false === imagealphablending($this->resource, false)) {
339 | throw new RuntimeException('Draw arc operation failed');
340 | }
341 |
342 | return $this;
343 | }
344 |
345 | public function chord(PointInterface $center, BoxInterface $size, int $start, int $end, $color, bool $fill = false, int $thickness = 1)
346 | {
347 | $thickness = max(0, (int) round($thickness));
348 | if ($thickness === 0 && !$fill) {
349 | return $this;
350 | }
351 | imagesetthickness($this->resource, $thickness);
352 |
353 | if (false === imagealphablending($this->resource, true)) {
354 | throw new RuntimeException('Draw chord operation failed');
355 | }
356 |
357 | if ($fill) {
358 | $style = IMG_ARC_CHORD;
359 | if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->converterToColor($color), $style)) {
360 | imagealphablending($this->resource, false);
361 | throw new RuntimeException('Draw chord operation failed');
362 | }
363 | } else {
364 | foreach (array(IMG_ARC_NOFILL, IMG_ARC_NOFILL | IMG_ARC_CHORD) as $style) {
365 | if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->converterToColor($color), $style)) {
366 | imagealphablending($this->resource, false);
367 | throw new RuntimeException('Draw chord operation failed');
368 | }
369 | }
370 | }
371 |
372 | if (false === imagealphablending($this->resource, false)) {
373 | throw new RuntimeException('Draw chord operation failed');
374 | }
375 |
376 | return $this;
377 | }
378 |
379 | public function circle(PointInterface $center, int|float $radius, $color, bool $fill = false, int $thickness = 1)
380 | {
381 | $diameter = $radius * 2;
382 |
383 | return $this->ellipse($center, new Box($diameter, $diameter), $color, $fill, $thickness);
384 | }
385 |
386 | public function ellipse(PointInterface $center, BoxInterface $size, $color, bool $fill = false, int $thickness = 1)
387 | {
388 | $thickness = max(0, (int) round($thickness));
389 | if ($thickness === 0 && !$fill) {
390 | return $this;
391 | }
392 | if (function_exists('imageantialias')) {
393 | imageantialias($this->resource, true);
394 | }
395 | imagesetthickness($this->resource, $thickness);
396 |
397 | if ($fill) {
398 | $callback = 'imagefilledellipse';
399 | } else {
400 | $callback = 'imageellipse';
401 | }
402 |
403 | if (function_exists('imageantialias')) {
404 | imageantialias($this->resource, true);
405 | }
406 | if (false === imagealphablending($this->resource, true)) {
407 | throw new RuntimeException('Draw ellipse operation failed');
408 | }
409 |
410 | if (function_exists('imageantialias')) {
411 | imageantialias($this->resource, true);
412 | }
413 | if (false === $callback($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $this->converterToColor($color))) {
414 | imagealphablending($this->resource, false);
415 | throw new RuntimeException('Draw ellipse operation failed');
416 | }
417 |
418 | if (false === imagealphablending($this->resource, false)) {
419 | throw new RuntimeException('Draw ellipse operation failed');
420 | }
421 |
422 | return $this;
423 | }
424 |
425 | public function line(PointInterface $start, PointInterface $end, $outline, int $thickness = 1)
426 | {
427 | $thickness = max(0, (int) round($thickness));
428 | if ($thickness === 0) {
429 | return $this;
430 | }
431 | imagesetthickness($this->resource, $thickness);
432 |
433 | if (false === imagealphablending($this->resource, true)) {
434 | throw new RuntimeException('Draw line operation failed');
435 | }
436 |
437 | if (false === imageline($this->resource, $start->getX(), $start->getY(), $end->getX(), $end->getY(), $this->converterToColor($outline))) {
438 | imagealphablending($this->resource, false);
439 | throw new RuntimeException('Draw line operation failed');
440 | }
441 |
442 | if (false === imagealphablending($this->resource, false)) {
443 | throw new RuntimeException('Draw line operation failed');
444 | }
445 |
446 | return $this;
447 | }
448 |
449 | public function pieSlice(PointInterface $center, BoxInterface $size, int $start, int $end, $color, bool $fill = false, int $thickness = 1)
450 | {
451 | $thickness = max(0, (int) round($thickness));
452 | if ($thickness === 0 && !$fill) {
453 | return $this;
454 | }
455 | imagesetthickness($this->resource, $thickness);
456 |
457 | if ($fill) {
458 | $style = IMG_ARC_EDGED;
459 | } else {
460 | $style = IMG_ARC_EDGED | IMG_ARC_NOFILL;
461 | }
462 |
463 | if (false === imagealphablending($this->resource, true)) {
464 | throw new RuntimeException('Draw chord operation failed');
465 | }
466 |
467 | if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->converterToColor($color), $style)) {
468 | imagealphablending($this->resource, false);
469 | throw new RuntimeException('Draw chord operation failed');
470 | }
471 |
472 | if (false === imagealphablending($this->resource, false)) {
473 | throw new RuntimeException('Draw chord operation failed');
474 | }
475 |
476 | return $this;
477 | }
478 |
479 | public function dot(PointInterface $position, $color)
480 | {
481 | if (false === imagealphablending($this->resource, true)) {
482 | throw new RuntimeException('Draw point operation failed');
483 | }
484 |
485 | if (false === imagesetpixel($this->resource, $position->getX(), $position->getY(), $this->converterToColor($color))) {
486 | imagealphablending($this->resource, false);
487 | throw new RuntimeException('Draw point operation failed');
488 | }
489 |
490 | if (false === imagealphablending($this->resource, false)) {
491 | throw new RuntimeException('Draw point operation failed');
492 | }
493 |
494 | return $this;
495 | }
496 |
497 | public function rectangle(PointInterface $leftTop, PointInterface $rightBottom, $color, bool $fill = false, int $thickness = 1)
498 | {
499 | $thickness = max(0, (int) round($thickness));
500 | if ($thickness === 0 && !$fill) {
501 | return $this;
502 | }
503 | imagesetthickness($this->resource, $thickness);
504 |
505 | $minX = min($leftTop->getX(), $rightBottom->getX());
506 | $maxX = max($leftTop->getX(), $rightBottom->getX());
507 | $minY = min($leftTop->getY(), $rightBottom->getY());
508 | $maxY = max($leftTop->getY(), $rightBottom->getY());
509 |
510 | if ($fill) {
511 | $callback = 'imagefilledrectangle';
512 | } else {
513 | $callback = 'imagerectangle';
514 | }
515 |
516 | if (false === imagealphablending($this->resource, true)) {
517 | throw new RuntimeException('Draw polygon operation failed');
518 | }
519 |
520 | if (false === $callback($this->resource, $minX, $minY, $maxX, $maxY, $this->converterToColor($color))) {
521 | imagealphablending($this->resource, false);
522 | throw new RuntimeException('Draw polygon operation failed');
523 | }
524 |
525 | if (false === imagealphablending($this->resource, false)) {
526 | throw new RuntimeException('Draw polygon operation failed');
527 | }
528 |
529 | return $this;
530 | }
531 |
532 | public function polygon(array $coordinates, $color, bool $fill = false, int $thickness = 1)
533 | {
534 | $thickness = max(0, (int) round($thickness));
535 | if ($thickness === 0 && !$fill) {
536 | return $this;
537 | }
538 | imagesetthickness($this->resource, $thickness);
539 |
540 | if (count($coordinates) < 3) {
541 | throw new InvalidArgumentException(sprintf('A polygon must consist of at least 3 points, %d given', count($coordinates)));
542 | }
543 |
544 | $points = call_user_func_array('array_merge', array_map(function (PointInterface $p) {
545 | return array($p->getX(), $p->getY());
546 | }, $coordinates));
547 |
548 | if ($fill) {
549 | $callback = 'imagefilledpolygon';
550 | } else {
551 | $callback = 'imagepolygon';
552 | }
553 |
554 | if (false === imagealphablending($this->resource, true)) {
555 | throw new RuntimeException('Draw polygon operation failed');
556 | }
557 |
558 | if (false === $callback($this->resource, $points, count($coordinates), $this->converterToColor($color))) {
559 | imagealphablending($this->resource, false);
560 | throw new RuntimeException('Draw polygon operation failed');
561 | }
562 |
563 | if (false === imagealphablending($this->resource, false)) {
564 | throw new RuntimeException('Draw polygon operation failed');
565 | }
566 |
567 | return $this;
568 | }
569 |
570 | public function text(string $string, FontInterface $font, PointInterface $position, int|float $angle = 0, int $width = 0)
571 | {
572 | $angle = -1 * $angle;
573 | $fontsize = $font->getSize();
574 | $fontFile = $font->getFile();
575 | $x = $position->getX();
576 | $y = $position->getY();// + $fontsize;
577 |
578 | if ($width !== 0) {
579 | $string = $font->wrapText($string, $width, $angle);
580 | }
581 |
582 | // if (false === imagealphablending($this->resource, true)) {
583 | // throw new RuntimeException('Font mask operation failed');
584 | // }
585 |
586 | if (!is_numeric($fontFile) && $fontFile && DIRECTORY_SEPARATOR === '\\') {
587 | // On Windows imagefttext() throws a "Could not find/open font" error if $fontfile is not an absolute path.
588 | $fontFileRealpath = realpath($fontFile);
589 | if ($fontFileRealpath !== false) {
590 | $fontFile = $fontFileRealpath;
591 | }
592 | }
593 | if (is_numeric($fontFile)) {
594 | //$y -= $fontsize;
595 | if (false === imagestring($this->resource, intval($fontFile), $x, $y, $string, $this->converterToColor($font->getColor()))) {
596 | imagealphablending($this->resource, false);
597 | throw new RuntimeException('Font mask operation failed');
598 | }
599 | } else if (false === imagefttext($this->resource, $fontsize, $angle, $x, $y, $this->converterToColor($font->getColor()), $fontFile, $string)) {
600 | imagealphablending($this->resource, false);
601 | throw new RuntimeException('Font mask operation failed');
602 | }
603 | if (false === imagealphablending($this->resource, false)) {
604 | throw new RuntimeException('Font mask operation failed');
605 | }
606 |
607 | return $this;
608 | }
609 |
610 | public function char(string|int $code, FontInterface $font, PointInterface $position,
611 | int|float $angle = 0) {
612 | return $this->text(is_int($code) ?
613 | mb_chr($code)// sprintf('%d;', $code)
614 | : $code, $font, $position, $angle, 0);
615 | // $fontFile = is_numeric($font->getFile()) ? intval($font->getFile()) :
616 | // imageloadfont($font->getFile());
617 | // if ($fontFile === false) {
618 | // throw new \Exception('font is error');
619 | // }
620 | // if (false === imagechar($this->resource, $fontFile, $position->getX(),
621 | // $position->getY(), is_int($code) ? mb_chr($code, 'UTF-8') : $code,
622 | // $this->converterToColor($font->getColor()))) {
623 | // throw new RuntimeException('Font mask operation failed');
624 | // }
625 | // return $this;
626 | }
627 |
628 | public function fontSize(string $string, FontInterface $font, int|float $angle = 0) {
629 | $box = imagettfbbox($font->getSize(), $angle, $font->getFile(), $string);
630 | return new Box(abs($box[4] - $box[0]), abs($box[5] - $box[1]));
631 | }
632 |
633 | /**
634 | * 将某个颜色定义为透明色
635 | * @param $color
636 | * @return static
637 | */
638 | public function transparent($color) {
639 | imagecolortransparent($this->resource, $this->converterToColor($color));
640 | imagealphablending($this->resource, false);
641 | imagesavealpha($this->resource, true);
642 | return $this;
643 | }
644 |
645 | public function gamma(float $correction)
646 | {
647 | if (false === imagegammacorrect($this->resource, 1.0, $correction)) {
648 | throw new RuntimeException('Failed to apply gamma correction to the image');
649 | }
650 |
651 | return $this;
652 | }
653 |
654 | public function negative()
655 | {
656 | if (false === imagefilter($this->resource, IMG_FILTER_NEGATE)) {
657 | throw new RuntimeException('Failed to negate the image');
658 | }
659 |
660 | return $this;
661 | }
662 |
663 | public function grayscale()
664 | {
665 | if (false === imagefilter($this->resource, IMG_FILTER_GRAYSCALE)) {
666 | throw new RuntimeException('Failed to grayscale the image');
667 | }
668 |
669 | return $this;
670 | }
671 |
672 | public function colorize($color)
673 | {
674 | $color = Colors::converter(...func_get_args());
675 | if (false === imagefilter($this->resource, IMG_FILTER_COLORIZE, $color[0], $color[1], $color[2])) {
676 | throw new RuntimeException('Failed to colorize the image');
677 | }
678 | return $this;
679 | }
680 |
681 | public function sharpen()
682 | {
683 | $sharpenMatrix = array(array(-1, -1, -1), array(-1, 16, -1), array(-1, -1, -1));
684 | $divisor = array_sum(array_map('array_sum', $sharpenMatrix));
685 |
686 | if (false === imageconvolution($this->resource, $sharpenMatrix, $divisor, 0)) {
687 | throw new RuntimeException('Failed to sharpen the image');
688 | }
689 |
690 | return $this;
691 | }
692 |
693 | public function blur(float $sigma = 1)
694 | {
695 | if (false === imagefilter($this->resource, IMG_FILTER_GAUSSIAN_BLUR)) {
696 | throw new RuntimeException('Failed to blur the image');
697 | }
698 |
699 | return $this;
700 | }
701 |
702 | public function brightness(float $brightness)
703 | {
704 | $gdBrightness = (int) round($brightness / 100 * 255);
705 | if ($gdBrightness < -255 || $gdBrightness > 255) {
706 | throw new InvalidArgumentException(sprintf('The %1$s argument can range from %2$d to %3$d, but you specified %4$d.', '$brightness', -100, 100, $brightness));
707 | }
708 | if (false === imagefilter($this->resource, IMG_FILTER_BRIGHTNESS, $gdBrightness)) {
709 | throw new RuntimeException('Failed to brightness the image');
710 | }
711 |
712 | return $this;
713 | }
714 |
715 | public function convolve(Matrix $matrix)
716 | {
717 | if ($matrix->getWidth() !== 3 || $matrix->getHeight() !== 3) {
718 | throw new InvalidArgumentException(sprintf('A convolution matrix must be 3x3 (%dx%d provided).', $matrix->getWidth(), $matrix->getHeight()));
719 | }
720 | if (false === imageconvolution($this->resource, $matrix->getMatrix(), 1, 0)) {
721 | throw new RuntimeException('Failed to convolve the image');
722 | }
723 |
724 | return $this;
725 | }
726 |
727 | public function converterToColor(mixed $color): mixed {
728 | $color = Colors::converter(...func_get_args());
729 | if (is_integer($color)) {
730 | return $color;
731 | }
732 | return imagecolorallocate($this->resource, (int)$color[0], (int)$color[1], (int)$color[2]);
733 | }
734 |
735 | public function converterFromColor(mixed $color): array {
736 | $result = imagecolorsforindex($this->resource, $color);
737 | return array(
738 | $result['red'],
739 | $result['green'],
740 | $result['blue'],
741 | $result['alpha'],
742 | );
743 | }
744 |
745 | public function getColorAt(PointInterface $point) {
746 | if (!$point->in($this->getSize())) {
747 | throw new RuntimeException(sprintf('Error getting color at point [%s,%s]. The point must be inside the image of size [%s,%s]', $point->getX(), $point->getY(), $this->getSize()->getWidth(), $this->getSize()->getHeight()));
748 | }
749 |
750 | return imagecolorat($this->resource, $point->getX(), $point->getY());
751 | //$info = imagecolorsforindex($this->resource, $index);
752 |
753 | //return $this->palette->color(array($info['red'], $info['green'], $info['blue']), max(min(100 - (int) round($info['alpha'] / 127 * 100), 100), 0));
754 | }
755 |
756 | /**
757 | * 另存为
758 | * @param string|null $output 如果为null 表示输出
759 | * @param string $type
760 | * @return bool
761 | */
762 | public function saveAs(mixed $output = null, string $type = ''): bool {
763 | $this->setRealType($type);
764 | if (!is_null($output)) {
765 | $output = (string)$output;
766 | }
767 | return call_user_func('image'.$this->realType, $this->resource, $output);
768 | }
769 |
770 | public function close() {
771 | if (is_resource($this->resource) && 'gd' === get_resource_type($this->resource)) {
772 | imagedestroy($this->resource);
773 | }
774 | $this->resource = null;
775 | }
776 |
777 | public function __destruct() {
778 | $this->close();
779 | }
780 |
781 | public function __clone() {
782 | $size = $this->getSize();
783 | $copy = $this->createImage($size, 'copy');
784 | if (false === imagecopy($copy, $this->resource, 0, 0, 0, 0, $size->getWidth(), $size->getHeight())) {
785 | imagedestroy($copy);
786 | throw new RuntimeException('Image copy operation failed');
787 | }
788 | $this->resource = $copy;
789 | }
790 |
791 | protected function check($file) {
792 | return is_file($file) && getimagesize($file) && extension_loaded('gd');
793 | }
794 |
795 | private function createImage(BoxInterface $size, $operation)
796 | {
797 | $resource = imagecreatetruecolor($size->getWidth(), $size->getHeight());
798 |
799 | if (false === $resource) {
800 | throw new RuntimeException('Image ' . $operation . ' failed');
801 | }
802 |
803 | if (false === imagealphablending($resource, false) || false === imagesavealpha($resource, true)) {
804 | throw new RuntimeException('Image ' . $operation . ' failed');
805 | }
806 |
807 | if (function_exists('imageantialias')) {
808 | imageantialias($resource, true);
809 | }
810 |
811 | $transparent = imagecolorallocatealpha($resource, 255, 255, 255, 127);
812 | imagefill($resource, 0, 0, $transparent);
813 | imagecolortransparent($resource, $transparent);
814 |
815 | return $resource;
816 | }
817 | }
--------------------------------------------------------------------------------
/src/Adapters/Gmagick.php:
--------------------------------------------------------------------------------
1 | resource = new \Gmagick((string)$path);
32 | } catch (\GmagickException $e) {
33 | throw new RuntimeException(sprintf('Unable to open image %s', $path), $e->getCode(), $e);
34 | }
35 |
36 | return $this;
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | *
42 | */
43 | public function create(BoxInterface $size, $color = null)
44 | {
45 | $width = $size->getWidth();
46 | $height = $size->getHeight();
47 |
48 | if (empty($color)) {
49 | $color = '#fff';
50 | }
51 | try {
52 | $gmagick = new \Gmagick();
53 | $pixel = $this->converterToColor($color);
54 |
55 | $gmagick->newimage($width, $height, $pixel->getcolor(false));
56 | $gmagick->setimagecolorspace(\Gmagick::COLORSPACE_TRANSPARENT);
57 | $gmagick->setimagebackgroundcolor($pixel);
58 |
59 | $this->resource = $gmagick;
60 | $this->refreshMeta();
61 | return $this;
62 | } catch (\GmagickException $e) {
63 | throw new RuntimeException('Could not create empty image', $e->getCode(), $e);
64 | }
65 | }
66 |
67 | /**
68 | * {@inheritdoc}
69 | *
70 | */
71 | public function load($string) {
72 | try {
73 | $imagick = new \Gmagick();
74 |
75 | $imagick->readImageBlob($string);
76 | $this->resource = $imagick;
77 | $this->refreshMeta();
78 | return $this;
79 | } catch (\GmagickException $e) {
80 | throw new RuntimeException('Could not load image from string', $e->getCode(), $e);
81 | }
82 | }
83 |
84 | /**
85 | * {@inheritdoc}
86 | *
87 | */
88 | public function read($resource)
89 | {
90 | if (!is_resource($resource)) {
91 | throw new InvalidArgumentException('Variable does not contain a stream resource');
92 | }
93 |
94 | $content = stream_get_contents($resource);
95 |
96 | if (false === $content) {
97 | throw new InvalidArgumentException('Couldn\'t read given resource');
98 | }
99 |
100 | try {
101 | $imagick = new \Gmagick();
102 | $imagick->readImageBlob($content);
103 | $this->resource = $imagick;
104 | $this->refreshMeta();
105 | } catch (\GmagickException $e) {
106 | throw new RuntimeException('Could not read image from resource', $e->getCode(), $e);
107 | }
108 |
109 | return $this;
110 | }
111 |
112 | protected function refreshMeta() {
113 | $this->width = $this->resource->getImageWidth();
114 | $this->height = $this->resource->getImageHeight();
115 | if (empty($this->realType)) {
116 | $this->setRealType('png');
117 | }
118 | }
119 |
120 |
121 | /**
122 | * {@inheritdoc}
123 | *
124 | */
125 | public function arc(PointInterface $center, BoxInterface $size, int $start, int $end, $color, int $thickness = 1)
126 | {
127 | $thickness = max(0, (int) round($thickness));
128 | if ($thickness === 0) {
129 | return $this;
130 | }
131 | $x = $center->getX();
132 | $y = $center->getY();
133 | $width = $size->getWidth();
134 | $height = $size->getHeight();
135 |
136 | try {
137 | $pixel = $this->converterToColor($color);
138 | $arc = new \GmagickDraw();
139 |
140 | $arc->setstrokecolor($pixel);
141 | $arc->setstrokewidth($thickness);
142 | $arc->setfillcolor('transparent');
143 | $arc->arc(
144 | $x - $width / 2,
145 | $y - $height / 2,
146 | $x + $width / 2,
147 | $y + $height / 2,
148 | $start,
149 | $end
150 | );
151 |
152 | $this->resource->drawImage($arc);
153 |
154 | $pixel = null;
155 |
156 | $arc = null;
157 | } catch (\GmagickException $e) {
158 | throw new RuntimeException('Draw arc operation failed', $e->getCode(), $e);
159 | }
160 |
161 | return $this;
162 | }
163 |
164 | /**
165 | * {@inheritdoc}
166 | *
167 | */
168 | public function chord(PointInterface $center, BoxInterface $size, int $start, int $end, $color, bool $fill = false, int $thickness = 1)
169 | {
170 | $thickness = max(0, (int) round($thickness));
171 | if ($thickness === 0 && !$fill) {
172 | return $this;
173 | }
174 | $x = $center->getX();
175 | $y = $center->getY();
176 | $width = $size->getWidth();
177 | $height = $size->getHeight();
178 |
179 | try {
180 | $pixel = $this->converterToColor($color);
181 | $chord = new \GmagickDraw();
182 |
183 | $chord->setstrokecolor($pixel);
184 | $chord->setstrokewidth($thickness);
185 |
186 | if ($fill) {
187 | $chord->setfillcolor($pixel);
188 | } else {
189 | $x1 = (int)round($x + $width / 2 * cos(deg2rad($start)));
190 | $y1 = (int)round($y + $height / 2 * sin(deg2rad($start)));
191 | $x2 = (int)round($x + $width / 2 * cos(deg2rad($end)));
192 | $y2 = (int)round($y + $height / 2 * sin(deg2rad($end)));
193 |
194 | $this->line(new Point($x1, $y1), new Point($x2, $y2), $color, $thickness);
195 |
196 | $chord->setfillcolor('transparent');
197 | }
198 |
199 | $chord->arc($x - $width / 2, $y - $height / 2, $x + $width / 2, $y + $height / 2, $start, $end);
200 |
201 | $this->resource->drawImage($chord);
202 |
203 | $pixel = null;
204 |
205 | $chord = null;
206 | } catch (\GmagickException $e) {
207 | throw new RuntimeException('Draw chord operation failed', $e->getCode(), $e);
208 | }
209 |
210 | return $this;
211 | }
212 |
213 | /**
214 | * {@inheritdoc}
215 | *
216 | */
217 | public function circle(PointInterface $center, int|float $radius, $color, bool $fill = false, int $thickness = 1)
218 | {
219 | $diameter = $radius * 2;
220 |
221 | return $this->ellipse($center, new Box($diameter, $diameter), $color, $fill, $thickness);
222 | }
223 |
224 | /**
225 | * {@inheritdoc}
226 | *
227 | */
228 | public function ellipse(PointInterface $center, BoxInterface $size, $color, bool $fill = false, int $thickness = 1)
229 | {
230 | $thickness = max(0, (int) round($thickness));
231 | if ($thickness === 0 && !$fill) {
232 | return $this;
233 | }
234 | $width = $size->getWidth();
235 | $height = $size->getHeight();
236 |
237 | try {
238 | $pixel = $this->converterToColor($color);
239 | $ellipse = new \GmagickDraw();
240 |
241 | $ellipse->setstrokecolor($pixel);
242 | $ellipse->setstrokewidth($thickness);
243 |
244 | if ($fill) {
245 | $ellipse->setfillcolor($pixel);
246 | } else {
247 | $ellipse->setfillcolor('transparent');
248 | }
249 |
250 | $ellipse->ellipse(
251 | $center->getX(),
252 | $center->getY(),
253 | $width / 2,
254 | $height / 2,
255 | 0, 360
256 | );
257 |
258 | $this->resource->drawImage($ellipse);
259 |
260 | $pixel = null;
261 |
262 | $ellipse = null;
263 | } catch (\GmagickException $e) {
264 | throw new RuntimeException('Draw ellipse operation failed', $e->getCode(), $e);
265 | }
266 |
267 | return $this;
268 | }
269 |
270 | /**
271 | * {@inheritdoc}
272 | *
273 | */
274 | public function line(PointInterface $start, PointInterface $end, $outline, int $thickness = 1)
275 | {
276 | $thickness = max(0, (int) round($thickness));
277 | if ($thickness === 0) {
278 | return $this;
279 | }
280 | try {
281 | $pixel = $this->converterToColor($outline);
282 | $line = new \GmagickDraw();
283 |
284 | $line->setstrokecolor($pixel);
285 | $line->setstrokewidth($thickness);
286 | $line->setfillcolor($pixel);
287 | $line->line(
288 | $start->getX(),
289 | $start->getY(),
290 | $end->getX(),
291 | $end->getY()
292 | );
293 |
294 | $this->resource->drawImage($line);
295 |
296 | $pixel = null;
297 |
298 | $line = null;
299 | } catch (\GmagickException $e) {
300 | throw new RuntimeException('Draw line operation failed', $e->getCode(), $e);
301 | }
302 |
303 | return $this;
304 | }
305 |
306 | /**
307 | * {@inheritdoc}
308 | *
309 | */
310 | public function pieSlice(PointInterface $center, BoxInterface $size, int $start, int $end, $color, bool $fill = false, int $thickness = 1)
311 | {
312 | $thickness = max(0, (int) round($thickness));
313 | if ($thickness === 0 && !$fill) {
314 | return $this;
315 | }
316 | $width = $size->getWidth();
317 | $height = $size->getHeight();
318 |
319 | $x1 = (int)round($center->getX() + $width / 2 * cos(deg2rad($start)));
320 | $y1 = (int)round($center->getY() + $height / 2 * sin(deg2rad($start)));
321 | $x2 = (int)round($center->getX() + $width / 2 * cos(deg2rad($end)));
322 | $y2 = (int)round($center->getY() + $height / 2 * sin(deg2rad($end)));
323 |
324 | if ($fill) {
325 | $this->chord($center, $size, $start, $end, $color, true, $thickness);
326 | $this->polygon(
327 | array(
328 | $center,
329 | new Point($x1, $y1),
330 | new Point($x2, $y2),
331 | ),
332 | $color,
333 | true,
334 | $thickness
335 | );
336 | } else {
337 | $this->arc($center, $size, $start, $end, $color, $thickness);
338 | $this->line($center, new Point($x1, $y1), $color, $thickness);
339 | $this->line($center, new Point($x2, $y2), $color, $thickness);
340 | }
341 |
342 | return $this;
343 | }
344 |
345 | /**
346 | * {@inheritdoc}
347 | *
348 | */
349 | public function dot(PointInterface $position, $color)
350 | {
351 | $x = $position->getX();
352 | $y = $position->getY();
353 |
354 | try {
355 | $pixel = $this->converterToColor($color);
356 | $point = new \GmagickDraw();
357 |
358 | $point->setfillcolor($pixel);
359 | $point->point($x, $y);
360 |
361 | $this->resource->drawimage($point);
362 |
363 | $pixel = null;
364 | $point = null;
365 | } catch (\GmagickException $e) {
366 | throw new RuntimeException('Draw point operation failed', $e->getCode(), $e);
367 | }
368 |
369 | return $this;
370 | }
371 |
372 | /**
373 | * {@inheritdoc}
374 | *
375 | */
376 | public function rectangle(PointInterface $leftTop, PointInterface $rightBottom, $color, bool $fill = false, int $thickness = 1)
377 | {
378 | $thickness = max(0, (int) round($thickness));
379 | if ($thickness === 0 && !$fill) {
380 | return $this;
381 | }
382 | $minX = min($leftTop->getX(), $rightBottom->getX());
383 | $maxX = max($leftTop->getX(), $rightBottom->getX());
384 | $minY = min($leftTop->getY(), $rightBottom->getY());
385 | $maxY = max($leftTop->getY(), $rightBottom->getY());
386 |
387 | try {
388 | $pixel = $this->converterToColor($color);
389 | $rectangle = new \GmagickDraw();
390 |
391 | $rectangle->setstrokecolor($pixel);
392 | $rectangle->setstrokewidth($thickness);
393 |
394 | if ($fill) {
395 | $rectangle->setfillcolor($pixel);
396 | } else {
397 | $rectangle->setfillcolor('transparent');
398 | }
399 | $rectangle->rectangle($minX, $minY, $maxX, $maxY);
400 | $this->resource->drawImage($rectangle);
401 | } catch (\GmagickException $e) {
402 | throw new RuntimeException('Draw polygon operation failed', $e->getCode(), $e);
403 | }
404 |
405 | return $this;
406 | }
407 |
408 | /**
409 | * {@inheritdoc}
410 | *
411 | */
412 | public function polygon(array $coordinates, $color, bool $fill = false, int $thickness = 1)
413 | {
414 | if (count($coordinates) < 3) {
415 | throw new InvalidArgumentException(sprintf('Polygon must consist of at least 3 coordinates, %d given', count($coordinates)));
416 | }
417 | $thickness = max(0, (int) round($thickness));
418 | if ($thickness === 0 && !$fill) {
419 | return $this;
420 | }
421 |
422 | $points = array_map(function (PointInterface $p) {
423 | return array('x' => $p->getX(), 'y' => $p->getY());
424 | }, $coordinates);
425 |
426 | try {
427 | $pixel = $this->converterToColor($color);
428 | $polygon = new \GmagickDraw();
429 |
430 | $polygon->setstrokecolor($pixel);
431 | $polygon->setstrokewidth($thickness);
432 |
433 | if ($fill) {
434 | $polygon->setfillcolor($pixel);
435 | } else {
436 | $polygon->setfillcolor('transparent');
437 | }
438 |
439 | $polygon->polygon($points);
440 |
441 | $this->resource->drawImage($polygon);
442 |
443 | unset($pixel, $polygon);
444 | } catch (\GmagickException $e) {
445 | throw new RuntimeException('Draw polygon operation failed', $e->getCode(), $e);
446 | }
447 |
448 | return $this;
449 | }
450 |
451 | /**
452 | * {@inheritdoc}
453 | *
454 | */
455 | public function text(string $string, FontInterface $font, PointInterface $position, int|float $angle = 0, int $width = 0)
456 | {
457 | try {
458 | $pixel = $this->converterToColor($font->getColor());
459 | $text = new \GmagickDraw();
460 |
461 | $text->setfont($font->getFile());
462 | /*
463 | * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027
464 | *
465 | * ensure font resolution is the same as GD's hard-coded 96
466 | */
467 | $text->setfontsize((int) ($font->getSize() * (96 / 72)));
468 | $text->setfillcolor($pixel);
469 |
470 | if ($width !== 0) {
471 | $string = $font->wrapText($string, $width, $angle);
472 | }
473 |
474 | $info = $this->resource->queryfontmetrics($text, $string);
475 | $rad = deg2rad($angle);
476 | $cos = cos($rad);
477 | $sin = sin($rad);
478 |
479 | $x1 = round(0 * $cos - 0 * $sin);
480 | $x2 = round($info['textWidth'] * $cos - $info['textHeight'] * $sin);
481 | $y1 = round(0 * $sin + 0 * $cos);
482 | $y2 = round($info['textWidth'] * $sin + $info['textHeight'] * $cos);
483 |
484 | $xdiff = 0 - min($x1, $x2);
485 | $ydiff = 0 - min($y1, $y2);
486 |
487 | $this->resource->annotateimage($text, $position->getX() + $x1 + $xdiff, $position->getY() + $y2 + $ydiff, $angle, $string);
488 |
489 | unset($pixel, $text);
490 | } catch (\GmagickException $e) {
491 | throw new RuntimeException('Draw text operation failed', $e->getCode(), $e);
492 | }
493 |
494 | return $this;
495 | }
496 |
497 | public function char(string|int $code, FontInterface $font, PointInterface $position, int|float $angle = 0) {
498 | return $this->text(is_int($code) ? mb_chr($code, 'UTF-8') : $code, $font, $position, $angle, 0);
499 | }
500 |
501 | public function fontSize(string $string, FontInterface $font, int|float $angle = 0)
502 | {
503 | $text = new \GmagickDraw();
504 |
505 | $text->setfont($font->getFile());
506 | /*
507 | * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027
508 | *
509 | * ensure font resolution is the same as GD's hard-coded 96
510 | */
511 | $text->setfontsize((int) ($font->getSize() * (96 / 72)));
512 | $text->setfontstyle(\Gmagick::STYLE_OBLIQUE);
513 |
514 | $info = $this->resource->queryfontmetrics($text, $string);
515 | return new Box($info['textWidth'], $info['textHeight']);
516 | }
517 |
518 | /**
519 | * {@inheritdoc}
520 | *
521 | */
522 | public function gamma(float $correction)
523 | {
524 | try {
525 | $this->resource->gammaimage($correction);
526 | } catch (\GmagickException $e) {
527 | throw new RuntimeException('Failed to apply gamma correction to the image', $e->getCode(), $e);
528 | }
529 |
530 | return $this;
531 | }
532 |
533 | /**
534 | * {@inheritdoc}
535 | *
536 | */
537 | public function negative()
538 | {
539 | if (!method_exists($this->resource, 'negateimage')) {
540 | throw new Exception('Gmagick version 1.1.0 RC3 is required for negative effect');
541 | }
542 |
543 | try {
544 | $this->resource->negateimage(false, \Gmagick::CHANNEL_ALL);
545 | } catch (\GmagickException $e) {
546 | throw new RuntimeException('Failed to negate the image', $e->getCode(), $e);
547 | }
548 |
549 | return $this;
550 | }
551 |
552 | /**
553 | * {@inheritdoc}
554 | *
555 | */
556 | public function grayscale()
557 | {
558 | try {
559 | $this->resource->setImageType(2);
560 | } catch (\GmagickException $e) {
561 | throw new RuntimeException('Failed to grayscale the image', $e->getCode(), $e);
562 | }
563 |
564 | return $this;
565 | }
566 |
567 | /**
568 | * {@inheritdoc}
569 | *
570 | */
571 | public function colorize($color)
572 | {
573 | throw new Exception('Gmagick does not support colorize');
574 | }
575 |
576 | /**
577 | * {@inheritdoc}
578 | *
579 | */
580 | public function sharpen()
581 | {
582 | throw new Exception('Gmagick does not support sharpen yet');
583 | }
584 |
585 | /**
586 | * {@inheritdoc}
587 | *
588 | */
589 | public function blur(float $sigma = 1)
590 | {
591 | try {
592 | $this->resource->blurImage(0, $sigma);
593 | } catch (\GmagickException $e) {
594 | throw new RuntimeException('Failed to blur the image', $e->getCode(), $e);
595 | }
596 |
597 | return $this;
598 | }
599 |
600 | /**
601 | * {@inheritdoc}
602 | *
603 | */
604 | public function brightness(float $brightness)
605 | {
606 | $brightness = (int) round($brightness);
607 | if ($brightness < -100 || $brightness > 100) {
608 | throw new InvalidArgumentException(sprintf('The %1$s argument can range from %2$d to %3$d, but you specified %4$d.', '$brightness', -100, 100, $brightness));
609 | }
610 | try {
611 | // This *emulates* setting the brightness
612 | $sign = $brightness < 0 ? -1 : 1;
613 | $v = abs($brightness) / 100;
614 | if ($sign > 0) {
615 | $v = (2 / (sin(($v * .99999 * M_PI_2) + M_PI_2))) - 2;
616 | }
617 | $this->resource->modulateimage(100 + $sign * $v * 100, 100, 100);
618 | } catch (\GmagickException $e) {
619 | throw new RuntimeException('Failed to brightness the image');
620 | }
621 |
622 | return $this;
623 | }
624 |
625 | /**
626 | * {@inheritdoc}
627 | *
628 | */
629 | public function convolve(Matrix $matrix)
630 | {
631 | if (!method_exists($this->resource, 'convolveimage')) {
632 | // convolveimage has been added in gmagick 2.0.1RC2
633 | throw new Exception('The version of Gmagick extension is too old: it does not support convolve.');
634 | }
635 | if ($matrix->getWidth() !== 3 || $matrix->getHeight() !== 3) {
636 | throw new InvalidArgumentException(sprintf('A convolution matrix must be 3x3 (%dx%d provided).', $matrix->getWidth(), $matrix->getHeight()));
637 | }
638 | try {
639 | $this->resource->convolveimage($matrix->getValueList());
640 | } catch (\ImagickException $e) {
641 | throw new RuntimeException('Failed to convolve the image');
642 | }
643 |
644 | return $this;
645 | }
646 |
647 | /**
648 | * {@inheritdoc}
649 | *
650 | */
651 | public function strip()
652 | {
653 | try {
654 | try {
655 | // $this->profile($this->palette->profile());
656 | } catch (\Exception $e) {
657 | // here we discard setting the profile as the previous incorporated profile
658 | // is corrupted, let's now strip the image
659 | }
660 | $this->resource->stripimage();
661 | } catch (\GmagickException $e) {
662 | throw new RuntimeException('Strip operation failed', $e->getCode(), $e);
663 | }
664 |
665 | return $this;
666 | }
667 |
668 | /**
669 | * {@inheritdoc}
670 | *
671 | */
672 | public function paste(ImageAdapter $image, PointInterface $start, int|float $alpha = 100)
673 | {
674 | if (!$image instanceof self) {
675 | throw new InvalidArgumentException(sprintf('Gmagick\Image can only paste() Gmagick\Image instances, %s given', get_class($image)));
676 | }
677 |
678 | $alpha = (int) round($alpha);
679 | if ($alpha < 0 || $alpha > 100) {
680 | throw new InvalidArgumentException(sprintf('The %1$s argument can range from %2$d to %3$d, but you specified %4$d.', '$alpha', 0, 100, $alpha));
681 | }
682 |
683 | if ($alpha === 100) {
684 | try {
685 | $this->resource->compositeimage($image->resource, \Gmagick::COMPOSITE_DEFAULT, $start->getX(), $start->getY());
686 | } catch (\GmagickException $e) {
687 | throw new RuntimeException('Paste operation failed', $e->getCode(), $e);
688 | }
689 | } elseif ($alpha > 0) {
690 | throw new Exception('Gmagick doesn\'t support paste with alpha.', 1);
691 | }
692 |
693 | return $this;
694 | }
695 |
696 | /**
697 | * {@inheritdoc}
698 | *
699 | */
700 | public function resize(BoxInterface $size, $filter = ImageAdapter::FILTER_UNDEFINED)
701 | {
702 | static $supportedFilters = array(
703 |
704 | );
705 |
706 | if (!array_key_exists($filter, $supportedFilters)) {
707 | throw new InvalidArgumentException('Unsupported filter type');
708 | }
709 |
710 | try {
711 | $this->resource->resizeimage($size->getWidth(), $size->getHeight(), $supportedFilters[$filter], 1);
712 | } catch (\GmagickException $e) {
713 | throw new RuntimeException('Resize operation failed', $e->getCode(), $e);
714 | }
715 |
716 | return $this;
717 | }
718 |
719 | /**
720 | * {@inheritdoc}
721 | *
722 | */
723 | public function rotate(int|float $angle, mixed $background = null)
724 | {
725 | try {
726 | if ($background === null) {
727 | $background = '#fff';
728 | }
729 | $pixel = $this->converterToColor($background);
730 |
731 | $this->resource->rotateimage($pixel, $angle);
732 |
733 | unset($pixel);
734 | } catch (\GmagickException $e) {
735 | throw new RuntimeException('Rotate operation failed', $e->getCode(), $e);
736 | }
737 |
738 | return $this;
739 | }
740 |
741 | /**
742 | * Applies options before save or output.
743 | *
744 | * @param \Gmagick $image
745 | * @param array $options
746 | * @param string $path
747 | *
748 | */
749 | private function applyImageOptions(\Gmagick $image, array $options, $path)
750 | {
751 | if (isset($options['format'])) {
752 | $format = $options['format'];
753 | } elseif ('' !== $extension = pathinfo($path, \PATHINFO_EXTENSION)) {
754 | $format = $extension;
755 | } else {
756 | $format = pathinfo($image->getImageFilename(), \PATHINFO_EXTENSION);
757 | }
758 |
759 | $format = strtolower($format);
760 |
761 | switch ($format) {
762 | case 'jpeg':
763 | case 'jpg':
764 | case 'pjpeg':
765 | if (!isset($options['jpeg_quality'])) {
766 | if (isset($options['quality'])) {
767 | $options['jpeg_quality'] = $options['quality'];
768 | }
769 | }
770 | if (isset($options['jpeg_quality'])) {
771 | $image->setCompressionQuality($options['jpeg_quality']);
772 | }
773 | if (isset($options['jpeg_sampling_factors'])) {
774 | if (!is_array($options['jpeg_sampling_factors']) || \count($options['jpeg_sampling_factors']) < 1) {
775 | throw new InvalidArgumentException('jpeg_sampling_factors option should be an array of integers');
776 | }
777 | $image->setSamplingFactors(array_map(function ($factor) {
778 | return (int) $factor;
779 | }, $options['jpeg_sampling_factors']));
780 | }
781 | break;
782 | case 'png':
783 | if (!isset($options['png_compression_level'])) {
784 | if (isset($options['quality'])) {
785 | $options['png_compression_level'] = round((100 - $options['quality']) * 9 / 100);
786 | }
787 | }
788 | if (isset($options['png_compression_level'])) {
789 | if ($options['png_compression_level'] < 0 || $options['png_compression_level'] > 9) {
790 | throw new InvalidArgumentException('png_compression_level option should be an integer from 0 to 9');
791 | }
792 | }
793 | if (isset($options['png_compression_filter'])) {
794 | if ($options['png_compression_filter'] < 0 || $options['png_compression_filter'] > 9) {
795 | throw new InvalidArgumentException('png_compression_filter option should be an integer from 0 to 9');
796 | }
797 | }
798 | if (isset($options['png_compression_level']) || isset($options['png_compression_filter'])) {
799 | // first digit: compression level (default: 7)
800 | $compression = isset($options['png_compression_level']) ? $options['png_compression_level'] * 10 : 70;
801 | // second digit: compression filter (default: 5)
802 | $compression += $options['png_compression_filter'] ?? 5;
803 | $image->setCompressionQuality($compression);
804 | }
805 | break;
806 | case 'webp':
807 | if (!isset($options['webp_quality'])) {
808 | if (isset($options['quality'])) {
809 | $options['webp_quality'] = $options['quality'];
810 | }
811 | }
812 | if (isset($options['webp_quality'])) {
813 | $image->setCompressionQuality($options['webp_quality']);
814 | }
815 | break;
816 | }
817 | if (isset($options['resolution-units']) && isset($options['resolution-x']) && isset($options['resolution-y'])) {
818 | switch ($options['resolution-units']) {
819 | case ImageAdapter::RESOLUTION_PIXELSPERCENTIMETER:
820 | $image->setimageunits(\Gmagick::RESOLUTION_PIXELSPERCENTIMETER);
821 | break;
822 | case ImageAdapter::RESOLUTION_PIXELSPERINCH:
823 | $image->setimageunits(\Gmagick::RESOLUTION_PIXELSPERINCH);
824 | break;
825 | default:
826 | throw new InvalidArgumentException('Unsupported image unit format');
827 | }
828 | $image->setimageresolution($options['resolution-x'], $options['resolution-y']);
829 | }
830 | }
831 |
832 | /**
833 | * {@inheritdoc}
834 | *
835 | */
836 | public function save()
837 | {
838 | $path = $this->file;
839 |
840 | if ('' === trim($path)) {
841 | throw new RuntimeException('You can omit save path only if image has been open from a file');
842 | }
843 |
844 | try {
845 | $this->resource->writeimage($path, true);
846 | } catch (\GmagickException $e) {
847 | throw new RuntimeException('Save operation failed', $e->getCode(), $e);
848 | }
849 |
850 | return $this;
851 | }
852 |
853 |
854 | /**
855 | * Destroys allocated gmagick resources.
856 | */
857 | public function __destruct()
858 | {
859 | if ($this->resource instanceof \Gmagick) {
860 | $this->resource->clear();
861 | $this->resource->destroy();
862 | }
863 | }
864 |
865 | /**
866 | * {@inheritdoc}
867 | *
868 | */
869 | public function __clone()
870 | {
871 | $this->resource = clone $this->resource;
872 | }
873 |
874 | /**
875 | * Gets specifically formatted color string from Color instance.
876 | *
877 | * @return \GmagickPixel
878 | */
879 | public function converterToColor(mixed $color): GmagickPixel
880 | {
881 | // if () {
882 | // throw new InvalidArgumentException('Gmagick doesn\'t support transparency');
883 | // }
884 |
885 | return new \GmagickPixel((string) $color);
886 | }
887 |
888 | public function getHeight(): int
889 | {
890 | return $this->resource->getimageheight();
891 | }
892 |
893 | public function getWidth(): int
894 | {
895 | return $this->resource->getimagewidth();
896 | }
897 |
898 | public function scale(BoxInterface $box)
899 | {
900 | $this->resource->scaleimage($box->getWidth(), $box->getHeight());
901 | return $this;
902 | }
903 |
904 | public function getColorAt(PointInterface $point)
905 | {
906 | }
907 |
908 | public function copy()
909 | {
910 | // TODO: Implement copy() method.
911 | }
912 |
913 | public function crop(PointInterface $start, BoxInterface $size)
914 | {
915 | $this->resource->cropimage($size->getWidth(), $size->getHeight(), $start->getX(), $start->getY());
916 | return $this;
917 | }
918 |
919 | public function saveAs(mixed $output = null, string $type = ''): bool
920 | {
921 | $this->resource->writeimage($output, true);
922 | return true;
923 | }
924 |
925 | public function fill($fill)
926 | {
927 | // TODO: Implement fill() method.
928 | }
929 |
930 | public function pastePart(ImageAdapter $src, PointInterface $srcStart, BoxInterface $srcBox, PointInterface $start, BoxInterface|null $box = null, int|float $alpha = 100)
931 | {
932 | // TODO: Implement pastePart() method.
933 | }
934 |
935 | public function transparent(mixed $color)
936 | {
937 | throw new Exception('gmagick is not support transparent');
938 | }
939 |
940 | public function converterFromColor(mixed $color): mixed
941 | {
942 | if (is_array($color)) {
943 | return $color;
944 | }
945 | if (!$color instanceof \GmagickPixel) {
946 | $color = new \GmagickPixel((string)$color);
947 | }
948 | $res = $color->getColor(true);
949 | return [$res['r'], $res['g'], $res['b'], $res['a']];
950 | }
951 |
952 | public function thumbnail(BoxInterface $box)
953 | {
954 | $this->resource->thumbnailimage($box->getWidth(), $box->getHeight());
955 | return $this;
956 | }
957 |
958 | protected function addFont(\GmagickDraw $draw, int|string $fontName) {
959 | if (is_file($fontName)) {
960 | $draw->setFont($fontName);
961 | } else {
962 | $draw->setFont($this->converterFont($fontName));
963 | }
964 | }
965 |
966 | protected function converterFont(int|string $fontName): string {
967 | if (!is_numeric($fontName)) {
968 | return $fontName;
969 | }
970 | $fontItems = $this->resource->queryFonts();
971 | $i = intval($fontName);
972 | if (isset($fontItems[$i])) {
973 | return $fontItems[$i];
974 | }
975 | throw new \Exception('Font is error');
976 | }
977 |
978 | private function getFilter(string $filter): int {
979 | return match ($filter) {
980 | ImageAdapter::FILTER_UNDEFINED => \Gmagick::FILTER_UNDEFINED,
981 | ImageAdapter::FILTER_BESSEL => \Gmagick::FILTER_BESSEL,
982 | ImageAdapter::FILTER_BLACKMAN => \Gmagick::FILTER_BLACKMAN,
983 | ImageAdapter::FILTER_BOX => \Gmagick::FILTER_BOX,
984 | ImageAdapter::FILTER_CATROM => \Gmagick::FILTER_CATROM,
985 | ImageAdapter::FILTER_CUBIC => \Gmagick::FILTER_CUBIC,
986 | ImageAdapter::FILTER_GAUSSIAN => \Gmagick::FILTER_GAUSSIAN,
987 | ImageAdapter::FILTER_HANNING => \Gmagick::FILTER_HANNING,
988 | ImageAdapter::FILTER_HAMMING => \Gmagick::FILTER_HAMMING,
989 | ImageAdapter::FILTER_HERMITE => \Gmagick::FILTER_HERMITE,
990 | ImageAdapter::FILTER_LANCZOS => \Gmagick::FILTER_LANCZOS,
991 | ImageAdapter::FILTER_MITCHELL => \Gmagick::FILTER_MITCHELL,
992 | ImageAdapter::FILTER_POINT => \Gmagick::FILTER_POINT,
993 | ImageAdapter::FILTER_QUADRATIC => \Gmagick::FILTER_QUADRATIC,
994 | ImageAdapter::FILTER_SINC => \Gmagick::FILTER_SINC,
995 | ImageAdapter::FILTER_TRIANGLE => \Gmagick::FILTER_TRIANGLE,
996 | default => throw new \Exception('error filter'),
997 | };
998 | }
999 | }
--------------------------------------------------------------------------------
/src/Adapters/ImageAdapter.php:
--------------------------------------------------------------------------------
1 | width = $width;
27 | $this->height = $height;
28 | if ($this->width < 1 || $this->height < 1) {
29 | throw new InvalidArgumentException(sprintf('Length of either side cannot be 0 or negative, current size is %sx%s', $width, $height));
30 | }
31 | }
32 |
33 | public function getWidth(): int {
34 | return $this->width;
35 | }
36 |
37 | public function getHeight(): int {
38 | return $this->height;
39 | }
40 |
41 | public function scale(float|int $ratio) {
42 | $width = max(1, round($ratio * $this->width));
43 | $height = max(1, round($ratio * $this->height));
44 |
45 | return new self($width, $height);
46 | }
47 |
48 | public function increase(int $size) {
49 | return new self($size + $this->width, $size + $this->height);
50 | }
51 |
52 | public function contains(BoxInterface $box, PointInterface|null $start = null): bool {
53 | $start = $start ? $start : new Point(0, 0);
54 |
55 | return $start->in($this) && $this->width >= $box->getWidth() + $start->getX() && $this->height >= $box->getHeight() + $start->getY();
56 | }
57 |
58 | public function square(): int {
59 | return $this->width * $this->height;
60 | }
61 |
62 | public function __toString(): string {
63 | return sprintf('%dx%d px', $this->width, $this->height);
64 | }
65 |
66 | public function widen(int $width) {
67 | return $this->scale($width / $this->width);
68 | }
69 |
70 | public function heighten(int $height) {
71 | return $this->scale($height / $this->height);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Base/BoxInterface.php:
--------------------------------------------------------------------------------
1 | file = is_string($file) ? $file : intval($file);
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | *
33 | */
34 | final public function getFile()
35 | {
36 | return $this->file;
37 | }
38 |
39 | /**
40 | * {@inheritdoc}
41 | *
42 | */
43 | final public function getSize(): int
44 | {
45 | return $this->size;
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | *
51 | */
52 | final public function getColor(): mixed
53 | {
54 | return $this->color;
55 | }
56 |
57 | /**
58 | * {@inheritdoc}
59 | *
60 | */
61 | public function wrapText(mixed $string, int $maxWidth, int $angle = 0): string
62 | {
63 | $string = (string) $string;
64 | if ($string === '') {
65 | return $string;
66 | }
67 | $maxWidth = (int) round($maxWidth);
68 | if ($maxWidth < 1) {
69 | throw new InvalidArgumentException(sprintf('The $maxWidth parameter of wrapText must be greater than 0.'));
70 | }
71 | $words = explode(' ', $string);
72 | $lines = array();
73 | $currentLine = null;
74 | foreach ($words as $word) {
75 | if ($currentLine === null) {
76 | $currentLine = $word;
77 | } else {
78 | $testLine = $currentLine . ' ' . $word;
79 | $testBox = $this->box($testLine, $angle);
80 | if ($testBox->getWidth() <= $maxWidth) {
81 | $currentLine = $testLine;
82 | } else {
83 | $lines[] = $currentLine;
84 | $currentLine = $word;
85 | }
86 | }
87 | }
88 | if ($currentLine !== null) {
89 | $lines[] = $currentLine;
90 | }
91 |
92 | return implode("\n", $lines);
93 | }
94 |
95 | public function box(string $string, int $angle = 0): Box {
96 | return ImageManager::create()->fontSize($string, $this, $angle);
97 | }
98 | }
--------------------------------------------------------------------------------
/src/Base/FontInterface.php:
--------------------------------------------------------------------------------
1 | width = (int) round($width);
41 | if ($this->width < 1) {
42 | throw new InvalidArgumentException('width has to be > 0');
43 | }
44 | $this->height = (int) round($height);
45 | if ($this->height < 1) {
46 | throw new InvalidArgumentException('height has to be > 0');
47 | }
48 | $expectedElements = $width * $height;
49 | $providedElements = count($elements);
50 | if ($providedElements > $expectedElements) {
51 | throw new InvalidArgumentException('there are more provided elements than space in the matrix');
52 | }
53 | $this->elements = array_values($elements);
54 | if ($providedElements < $expectedElements) {
55 | $this->elements = array_merge(
56 | $this->elements,
57 | array_fill($providedElements, $expectedElements - $providedElements, 0)
58 | );
59 | }
60 | }
61 |
62 | /**
63 | * Get the matrix width.
64 | *
65 | * @return int
66 | */
67 | public function getWidth(): int
68 | {
69 | return $this->width;
70 | }
71 |
72 | /**
73 | * Get the matrix height.
74 | *
75 | * @return int
76 | */
77 | public function getHeight(): int
78 | {
79 | return $this->height;
80 | }
81 |
82 | /**
83 | * Set the value of a cell.
84 | *
85 | * @param int $x
86 | * @param int $y
87 | * @param int|float $value
88 | */
89 | public function setElementAt($x, $y, $value)
90 | {
91 | $this->elements[$this->calculatePosition($x, $y)] = $value;
92 | }
93 |
94 | /**
95 | * Get the value of a cell.
96 | *
97 | * @param int $x
98 | * @param int $y
99 | *
100 | * @return int|float
101 | */
102 | public function getElementAt($x, $y)
103 | {
104 | return $this->elements[$this->calculatePosition($x, $y)];
105 | }
106 |
107 | /**
108 | * Return all the matrix values, as a monodimensional array.
109 | *
110 | * @return int[]|float[]
111 | */
112 | public function getValueList()
113 | {
114 | return $this->elements;
115 | }
116 |
117 | /**
118 | * Return all the matrix values, as a bidimensional array (every array item contains the values of a row).
119 | *
120 | * @return int[]|float[]
121 | */
122 | public function getMatrix()
123 | {
124 | return array_chunk($this->elements, $this->getWidth());
125 | }
126 |
127 | /**
128 | * Returns a new Matrix instance, representing the normalized value of this matrix.
129 | *
130 | * @return static
131 | */
132 | public function normalize()
133 | {
134 | $values = $this->getValueList();
135 | $divisor = array_sum($values);
136 | if ($divisor == 0 || $divisor == 1) {
137 | return clone $this;
138 | }
139 | $normalizedElements = array();
140 | foreach ($values as $value) {
141 | $normalizedElements[] = $value / $divisor;
142 | }
143 |
144 | return new static($this->getWidth(), $this->getHeight(), $normalizedElements);
145 | }
146 |
147 | /**
148 | * Calculate the offset position of a cell.
149 | *
150 | * @param int $x
151 | * @param int $y
152 | *
153 | *
154 | * @return int
155 | */
156 | protected function calculatePosition($x, $y)
157 | {
158 | if (0 > $x || 0 > $y || $this->width <= $x || $this->height <= $y) {
159 | throw new OutOfBoundsException(sprintf('There is no position (%s, %s) in this matrix', $x, $y));
160 | }
161 |
162 | return $y * $this->height + $x;
163 | }
164 | }
--------------------------------------------------------------------------------
/src/Base/Point.php:
--------------------------------------------------------------------------------
1 | x;
15 | }
16 |
17 | public function getY(): int {
18 | return $this->y;
19 | }
20 |
21 | public function in(BoxInterface $box): bool {
22 | return $this->x < $box->getWidth() && $this->y < $box->getHeight();
23 | }
24 |
25 | public function move(int $amount): PointInterface {
26 | return new self($this->x + $amount, $this->y + $amount);
27 | }
28 |
29 | public function __toString(): string {
30 | return sprintf('(%d, %d)', $this->x, $this->y);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Base/PointInterface.php:
--------------------------------------------------------------------------------
1 | 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', //随机因子
33 | 'length' => 4, //验证码长度
34 | 'fontSize' => 0, //指定字体大小
35 | 'fontColor' => '', //指定字体颜色, 可以是数组
36 | 'fontFamily' => 3, //指定的字体
37 | 'width' => 100,
38 | 'height' => 30,
39 | 'angle' => 0, //角度
40 | 'sensitive' => true, // 大小写敏感
41 | 'mode' => 0 // 验证码模式: 0 文字 1 公式
42 | ];
43 |
44 | public function getRealType(): string {
45 | return 'png';
46 | }
47 |
48 | public function isOnlyImage(): bool {
49 | return true;
50 | }
51 |
52 | public function setConfigs(array $configs): void {
53 | $this->configs = array_merge($this->configs, $configs);
54 | }
55 |
56 | /**
57 | * 获取验证码
58 | * @return string
59 | * @throws \Exception
60 | */
61 | public function getCode(): string {
62 | if (empty($this->code)) {
63 | $this->createCode();
64 | }
65 | return $this->code;
66 | }
67 |
68 | /**
69 | * 生成
70 | * @param int $level 干扰等级
71 | * @throws \Exception
72 | */
73 | public function generate(): mixed {
74 | $this->getCode();
75 | $this->width = $this->configs['width'];
76 | $this->height = $this->configs['height'];
77 | $this->createBg();
78 | $this->createText();
79 | $this->createLine(intval($this->configs['level'] ?? 1));
80 | $this->instance()->setRealType($this->getRealType());
81 | return ['code' => $this->code, 'sensitive' => $this->configs['sensitive']];
82 | }
83 |
84 |
85 | /**
86 | * 生成随机码
87 | * @param bool $setSession 保存到session里
88 | * @return $this
89 | * @throws \Exception
90 | */
91 | public function createCode(bool $setSession = true) {
92 | list($this->code, $this->chars) = $this->configs['mode'] == 1
93 | ? $this->generateFormula() : $this->generateRandomChar();
94 | // if ($setSession) {
95 | // session()->set(self::SESSION_KEY, [
96 | // 'sensitive' => $this->configs['sensitive'],
97 | // 'key' => Hash::make($this->configs['sensitive'] || is_numeric($this->code) ? $this->code : strtolower($this->code))
98 | // ]);
99 | // }
100 | return $this;
101 | }
102 |
103 | protected function generateRandomChar(): array {
104 | $charset = $this->configs['characters'];
105 | $_len = strlen($charset) - 1;
106 | $count = intval($this->configs['length']);
107 | $chars = [];
108 | for ($i = 0; $i < $count; $i ++) {
109 | $chars[] = $charset[mt_rand(0, $_len)];
110 | }
111 | return [implode('', $chars), $chars];
112 | }
113 |
114 | protected function generateFormula(): array {
115 | $tags = is_numeric($this->configs['fontFamily']) ?
116 | ['+', '-', '*', '/'] : ['加', '减', '乘', '除'];
117 | $tag = mt_rand(0, 3);
118 | $first = mt_rand(1, 99);
119 | $second = mt_rand(1, 99);
120 | $result = 0;
121 | if ($tag == 0) {
122 | $result = $first + $second;
123 | } elseif ($tag == 1) {
124 | if ($first < $second) {
125 | list($first, $second) = [$second, $first];
126 | }
127 | $result = $first - $second;
128 | } elseif ($tag == 2) {
129 | $result = $first * $second;
130 | } elseif ($tag == 3) {
131 | if ($first < $second) {
132 | list($first, $second) = [$second, $first];
133 | }
134 | $result = floor($first / $second);
135 | $first = $result * $second;
136 | }
137 | return [$result, [$first, $tags[$tag], $second, '=?']];
138 | }
139 |
140 | /**
141 | * 生成背景
142 | */
143 | protected function createBg(): void {
144 | $this->instance()->create(new Box($this->width, $this->height),
145 | [mt_rand(157, 255), mt_rand(157, 255), mt_rand(157, 255)]);
146 | }
147 |
148 | /**
149 | * 生成文字
150 | */
151 | protected function createText(): void {
152 | $length = count($this->chars);
153 | $width = $this->width / ($length + 1);
154 | $left = $width * .5;
155 | $maxHeight = (int)($this->height - $left);
156 | for ($i = 0; $i < $length; $i ++) {
157 | $size = $this->fontSize();
158 | $angle = $size > $this->height ? 0 : $this->angle();
159 | $height = intval((abs(cos($angle)) + abs(sin($angle))) * $size);
160 | $this->instance()->text(
161 | $this->chars[$i],
162 | new Font($this->configs['fontFamily'], $size, $this->fontColor($i)),
163 | new Point((int)($left + $width * $i),
164 | $height > $maxHeight
165 | ? $height : mt_rand($height, $maxHeight)),
166 | $angle
167 | );
168 | }
169 | }
170 |
171 | /**
172 | *
173 | * @return int
174 | */
175 | protected function angle(): int {
176 | if (empty($this->configs['angle'])) {
177 | return mt_rand(-30, 30);
178 | }
179 | return mt_rand(-1 * $this->configs['angle'], $this->configs['angle']);
180 | }
181 |
182 | protected function fontSize(): int {
183 | if (!empty($this->configs['fontSize'])) {
184 | return $this->configs['fontSize'];
185 | }
186 | return rand($this->height - 10, $this->height);
187 | }
188 |
189 | /**
190 | * 获取字体颜色
191 | * @param integer $i
192 | * @return array|mixed
193 | */
194 | protected function fontColor(int $i): mixed {
195 | if (empty($this->configs['fontColor'])) {
196 | return [mt_rand(0, 156), mt_rand(0, 156), mt_rand(0, 156)];
197 | }
198 | if (!is_array($this->configs['fontColor'])) {
199 | return $this->configs['fontColor'];
200 | }
201 | if (count($this->configs['fontColor']) <= $i) {
202 | return [mt_rand(0, 156), mt_rand(0, 156), mt_rand(0, 156)];
203 | }
204 | return $this->configs['fontColor'][$i];
205 | }
206 |
207 | /**
208 | * 生成线条、雪花
209 | * @param int $level
210 | */
211 | protected function createLine(int $level = 1) {
212 | //线条
213 | for ($i = 0; $i < 6; $i ++) {
214 | $this->instance()->line(
215 | new Point(mt_rand(0, $this->width),
216 | mt_rand(0, $this->height)),
217 | new Point(mt_rand(0, $this->width),
218 | mt_rand(0, $this->height)),
219 | [mt_rand(0, 156), mt_rand(0, 156), mt_rand(0, 156)]);
220 | }
221 | //雪花
222 | for ($i = 0, $length = $level * 20; $i < $length; $i ++) {
223 | $this->instance()->text(
224 | '*',
225 | new Font(
226 | is_numeric($this->configs['fontFamily'])
227 | ? mt_rand(1, 5) : $this->configs['fontFamily']
228 | , 16, [mt_rand(200, 255), mt_rand(200, 255), mt_rand(200, 255)]),
229 | new Point(mt_rand(0, $this->width),
230 | mt_rand(0, $this->height))
231 | );
232 | }
233 | }
234 |
235 | /**
236 | * 验证
237 | * @param string $value
238 | * @return bool
239 | * @throws \Exception
240 | */
241 | public function verify(mixed $value, mixed $source): bool {
242 | if (!is_array($source) || empty($source)) {
243 | return false;
244 | }
245 | if (!$source['sensitive']) {
246 | $value = strtolower((string)$value);
247 | $source['code'] = strtolower((string)$source['code']);
248 | }
249 | return (string)$value === (string)$source['code'];
250 | // if (!session()->has(self::SESSION_KEY)) {
251 | // return false;
252 | // }
253 | // $data = session()->get(self::SESSION_KEY);
254 | // if (!$data['sensitive'] && !is_numeric($value)) {
255 | // $value = strtolower($value);
256 | // }
257 | // session()->delete(self::SESSION_KEY);
258 | // return Hash::verify($value, $data['key']);
259 | }
260 |
261 | public function toArray(): array {
262 | return [
263 | 'image' => $this->toBase64(),
264 | 'width' => $this->width,
265 | 'height' => $this->height,
266 | ];
267 | }
268 | }
--------------------------------------------------------------------------------
/src/Colors.php:
--------------------------------------------------------------------------------
1 | '240,248,255',
13 | 'LightSalmon' => '255,160,122',
14 | 'AntiqueWhite' => '250,235,215',
15 | 'LightSeaGreen' => '32,178,170',
16 | 'Aqua' => '0,255,255',
17 | 'LightSkyBlue' => '135,206,250',
18 | 'Aquamarine' => '127,255,212',
19 | 'LightSlateGray' => '119,136,153',
20 | 'Azure' => '240,255,255',
21 | 'LightSteelBlue' => '176,196,222',
22 | 'Beige' => '245,245,220',
23 | 'LightYellow' => '255,255,224',
24 | 'Bisque' => '255,228,196',
25 | 'Lime' => '0,255,0',
26 | 'Black' => '0,0,0',
27 | 'LimeGreen' => '50,205,50',
28 | 'BlanchedAlmond' => '255,255,205',
29 | 'Linen' => '250,240,230',
30 | 'Blue' => '0,0,255',
31 | 'Magenta' => '255,0,255',
32 | 'BlueViolet' => '138,43,226',
33 | 'Maroon' => '128,0,0',
34 | 'Brown' => '165,42,42',
35 | 'MediumAquamarine' => '102,205,170',
36 | 'BurlyWood' => '222,184,135',
37 | 'MediumBlue' => '0,0,205',
38 | 'CadetBlue' => '95,158,160',
39 | 'MediumOrchid' => '186,85,211',
40 | 'Chartreuse' => '127,255,0',
41 | 'MediumPurple' => '147,112,219',
42 | 'Chocolate' => '210,105,30',
43 | 'MediumSeaGreen' => '60,179,113',
44 | 'Coral' => '255,127,80',
45 | 'MediumSlateBlue' => '123,104,238',
46 | 'CornflowerBlue' => '100,149,237',
47 | 'MediumSpringGreen' => '0,250,154',
48 | 'Cornsilk' => '255,248,220',
49 | 'MediumTurquoise' => '72,209,204',
50 | 'Crimson' => '220,20,60',
51 | 'MediumVioletRed' => '199,21,112',
52 | 'Cyan' => '0,255,255',
53 | 'MidnightBlue' => '25,25,112',
54 | 'DarkBlue' => '0,0,139',
55 | 'MintCream' => '245,255,250',
56 | 'DarkCyan' => '0,139,139',
57 | 'MistyRose' => '255,228,225',
58 | 'DarkGoldenrod' => '184,134,11',
59 | 'Moccasin' => '255,228,181',
60 | 'DarkGray' => '169,169,169',
61 | 'NavajoWhite' => '255,222,173',
62 | 'DarkGreen' => '0,100,0',
63 | 'Navy' => '0,0,128',
64 | 'DarkKhaki' => '189,183,107',
65 | 'OldLace' => '253,245,230',
66 | 'DarkMagena' => '139,0,139',
67 | 'Olive' => '128,128,0',
68 | 'DarkOliveGreen' => '85,107,47',
69 | 'OliveDrab' => '107,142,45',
70 | 'DarkOrange' => '255,140,0',
71 | 'Orange' => '255,165,0',
72 | 'DarkOrchid' => '153,50,204',
73 | 'OrangeRed' => '255,69,0',
74 | 'DarkRed' => '139,0,0',
75 | 'Orchid' => '218,112,214',
76 | 'DarkSalmon' => '233,150,122',
77 | 'PaleGoldenrod' => '238,232,170',
78 | 'DarkSeaGreen' => '143,188,143',
79 | 'PaleGreen' => '152,251,152',
80 | 'DarkSlateBlue' => '72,61,139',
81 | 'PaleTurquoise' => '175,238,238',
82 | 'DarkSlateGray' => '40,79,79',
83 | 'PaleVioletRed' => '219,112,147',
84 | 'DarkTurquoise' => '0,206,209',
85 | 'PapayaWhip' => '255,239,213',
86 | 'DarkViolet' => '148,0,211',
87 | 'PeachPuff' => '255,218,155',
88 | 'DeepPink' => '255,20,147',
89 | 'Peru' => '205,133,63',
90 | 'DeepSkyBlue' => '0,191,255',
91 | 'Pink' => '255,192,203',
92 | 'DimGray' => '105,105,105',
93 | 'Plum' => '221,160,221',
94 | 'DodgerBlue' => '30,144,255',
95 | 'PowderBlue' => '176,224,230',
96 | 'Firebrick' => '178,34,34',
97 | 'Purple' => '128,0,128',
98 | 'FloralWhite' => '255,250,240',
99 | 'Red' => '255,0,0',
100 | 'ForestGreen' => '34,139,34',
101 | 'RosyBrown' => '188,143,143',
102 | 'Fuschia' => '255,0,255',
103 | 'RoyalBlue' => '65,105,225',
104 | 'Gainsboro' => '220,220,220',
105 | 'SaddleBrown' => '139,69,19',
106 | 'GhostWhite' => '248,248,255',
107 | 'Salmon' => '250,128,114',
108 | 'Gold' => '255,215,0',
109 | 'SandyBrown' => '244,164,96',
110 | 'Goldenrod' => '218,165,32',
111 | 'SeaGreen' => '46,139,87',
112 | 'Gray' => '128,128,128',
113 | 'Seashell' => '255,245,238',
114 | 'Green' => '0,128,0',
115 | 'Sienna' => '160,82,45',
116 | 'GreenYellow' => '173,255,47',
117 | 'Silver' => '192,192,192',
118 | 'Honeydew' => '240,255,240',
119 | 'SkyBlue' => '135,206,235',
120 | 'HotPink' => '255,105,180',
121 | 'SlateBlue' => '106,90,205',
122 | 'IndianRed' => '205,92,92',
123 | 'SlateGray' => '112,128,144',
124 | 'Indigo' => '75,0,130',
125 | 'Snow' => '255,250,250',
126 | 'Ivory' => '255,240,240',
127 | 'SpringGreen' => '0,255,127',
128 | 'Khaki' => '240,230,140',
129 | 'SteelBlue' => '70,130,180',
130 | 'Lavender' => '230,230,250',
131 | 'Tan' => '210,180,140',
132 | 'LavenderBlush' => '255,240,245',
133 | 'Teal' => '0,128,128',
134 | 'LawnGreen' => '124,252,0',
135 | 'Thistle' => '216,191,216',
136 | 'LemonChiffon' => '255,250,205',
137 | 'Tomato' => '253,99,71',
138 | 'LightBlue' => '173,216,230',
139 | 'Turquoise' => '64,224,208',
140 | 'LightCoral' => '240,128,128',
141 | 'Violet' => '238,130,238',
142 | 'LightCyan' => '224,255,255',
143 | 'Wheat' => '245,222,179',
144 | 'LightGoldenrodYellow' => '250,250,210',
145 | 'White' => '255,255,255',
146 | 'LightGreen' => '144,238,144',
147 | 'WhiteSmoke' => '245,245,245',
148 | 'LightGray' => '211,211,211',
149 | 'Yellow' => '255,255,0',
150 | 'LightPink' => '255,182,193',
151 | 'YellowGreen' => '154,205,50',
152 | ];
153 |
154 | public static function converter(mixed $color): int|string|array {
155 | if (func_num_args() == 1 && is_int($color)) {
156 | return $color;
157 | }
158 | if (func_num_args() >= 3) {
159 | return func_num_args();
160 | }
161 | if (is_array($color) && count($color) >= 3) {
162 | return $color;
163 | }
164 | if (is_string($color) && str_starts_with($color, '#')) {
165 | return static::transformRGB($color);
166 | }
167 | $name = Str::studly((string)$color);
168 | if (isset(self::$maps[$name])) {
169 | $args = explode(',', self::$maps[$name], 3);
170 | $args[] = 1;
171 | return $args;
172 | }
173 | throw new Exception(
174 | __('Color[{color}] IS ERROR!', [
175 | 'color' => $color
176 | ])
177 | );
178 | }
179 |
180 | public static function transformRGB(string $color = '#000000'): array {
181 | if (strlen($color) == 4) {
182 | $red = substr($color, 1, 1);
183 | $green = substr($color, 2, 1);
184 | $blue = substr($color, 3, 1);
185 | $red .= $red;
186 | $green .= $green;
187 | $blue .= $blue;
188 | } else {
189 | $red = substr($color, 1, 2);
190 | $green = substr($color, 3, 2);
191 | $blue = substr($color, 5, 2);
192 | }
193 | return array(
194 | hexdec($red),
195 | hexdec($green),
196 | hexdec($blue),
197 | 1,
198 | );
199 | }
200 | }
--------------------------------------------------------------------------------
/src/HintCaptcha.php:
--------------------------------------------------------------------------------
1 | 3,
14 | ];
15 |
16 | /**
17 | * @var integer[]
18 | */
19 | protected array $point = [];
20 |
21 | /**
22 | * @var ImageAdapter
23 | */
24 | protected ImageAdapter|null $shapeImage = null;
25 |
26 | public function isOnlyImage(): bool {
27 | return false;
28 | }
29 |
30 | public function setConfigs(array $configs): void {
31 | $this->configs = array_merge($this->configs, $configs);
32 | }
33 |
34 | public function generate(): mixed {
35 | $this->instance()->scale(new Box($this->configs['width'],
36 | $this->configs['height']));
37 | $this->drawBox();
38 | return $this->point;
39 | }
40 |
41 | public function drawBox(): void {
42 | $image = $this->instance();
43 | $width = $image->getWidth();
44 | $height = $image->getHeight();
45 | $font = $this->configs['fontFamily'];
46 | $fontSize = $this->configs['fontSize'];
47 | $words = $this->configs['words'];
48 | $count = $this->configs['count'];
49 | $maxWidth = $width - $fontSize;
50 | $maxHeight = $height - $fontSize;
51 | $this->shapeImage = ImageManager::create()->create(new Box($count * $fontSize, $fontSize), '#fff');
52 | $darkFont = new Font($font, intval($fontSize * .6), '#000');
53 | $items = [];
54 | foreach ($words as $i => $word) {
55 | $x = random_int($fontSize, $maxWidth - $fontSize);
56 | $y = random_int($fontSize, $maxHeight - $fontSize);
57 | $this->instance()->char($word, new Font($font, $fontSize, '#000'),
58 | new Point(
59 | $x, $y
60 | ));
61 | if ($count > $i) {
62 | $this->shapeImage->char($word, $darkFont, new Point($i * $fontSize, $darkFont->getSize()));
63 | $items[] = [
64 | $x, $y
65 | ];
66 | }
67 | }
68 | $this->point = $items;
69 | }
70 |
71 | public function verify(mixed $value, mixed $source): bool {
72 | if (empty($value) || empty($source)) {
73 | return false;
74 | }
75 | $size = intval($this->configs['fontSize']);
76 | foreach ($source as $i => $p) {
77 | if (!isset($value[$i])) {
78 | return false;
79 | }
80 | $srcX = ImageHelper::x($p);
81 | $srcY = ImageHelper::y($p);
82 | if (
83 | !ImageHelper::inBound($value[$i], $srcX, $srcY, $size, $size)
84 | ) {
85 | return false;
86 | }
87 | }
88 | return true;
89 | }
90 |
91 | public function toArray(): array {
92 | return [
93 | 'image' => $this->toBase64(),
94 | 'width' => $this->instance()->getWidth(),
95 | 'height' => $this->instance()->getHeight(),
96 | 'count' => $this->configs['count'],
97 | 'control' => $this->shapeImage->toBase64()
98 | ];
99 | }
100 | }
--------------------------------------------------------------------------------
/src/ICaptcha.php:
--------------------------------------------------------------------------------
1 | instance()->getSize()];
35 | }
36 | $images = [];
37 | foreach ($sizes as $size) {
38 | $image = $this->createLayer($size);
39 | if (!empty($image)) {
40 | $images[] = $image;
41 | }
42 | }
43 | $data = pack( 'vvv', 0, 1, count( $images ) );
44 | $pixel_data = '';
45 | $icon_dir_entry_size = 16;
46 | $offset = 6 + ( $icon_dir_entry_size * count( $images ) );
47 | foreach ( $images as $image ) {
48 | $data .= pack( 'CCCCvvVV', $image['width'], $image['height'], $image['color_palette_colors'], 0, 1, $image['bits_per_pixel'], $image['size'], $offset );
49 | $pixel_data .= $image['data'];
50 | $offset += $image['size'];
51 | }
52 | $data .= $pixel_data;
53 | unset( $pixel_data );
54 | if ( false === ( $fh = fopen((string)$output, 'w' ) ) )
55 | return false;
56 |
57 | if ( false === (fwrite($fh, $data ) ) ) {
58 | fclose($fh);
59 | return false;
60 | }
61 | fclose($fh);
62 | return true;
63 | }
64 |
65 | private function createLayer($size) {
66 | if (is_array($size)) {
67 | list($width, $height) = $size;
68 | } elseif (is_string($size) && strpos($size, 'x') > 0) {
69 | list($width, $height) = explode('x', $size, 2);
70 | } elseif ($size instanceof BoxInterface) {
71 | $width = $size->getWidth();
72 | $height = $size->getHeight();
73 | } else {
74 | $height = $width = intval($size);
75 | }
76 | $new_im = clone $this->instance();
77 | $new_im->scale(new Box($width, $height));
78 |
79 | $pixel_data = array();
80 | $opacity_data = array();
81 | $current_opacity_val = 0;
82 |
83 | for ( $y = $height - 1; $y >= 0; $y-- ) {
84 | for ( $x = 0; $x < $width; $x++ ) {
85 | $color = $new_im->getColorAt(new Point($x, $y));
86 |
87 | $alpha = ( $color & 0x7F000000 ) >> 24;
88 | $alpha = ( 1 - ( $alpha / 127 ) ) * 255;
89 |
90 | $color &= 0xFFFFFF;
91 | $color |= 0xFF000000 & ( $alpha << 24 );
92 |
93 | $pixel_data[] = $color;
94 |
95 |
96 | $opacity = ( $alpha <= 127 ) ? 1 : 0;
97 |
98 | $current_opacity_val = ( $current_opacity_val << 1 ) | $opacity;
99 |
100 | if ( ( ( $x + 1 ) % 32 ) == 0 ) {
101 | $opacity_data[] = $current_opacity_val;
102 | $current_opacity_val = 0;
103 | }
104 | }
105 |
106 | if ( ( $x % 32 ) > 0 ) {
107 | while ( ( $x++ % 32 ) > 0 )
108 | $current_opacity_val = $current_opacity_val << 1;
109 |
110 | $opacity_data[] = $current_opacity_val;
111 | $current_opacity_val = 0;
112 | }
113 | }
114 |
115 | $image_header_size = 40;
116 | $color_mask_size = $width * $height * 4;
117 | $opacity_mask_size = ( ceil( $width / 32 ) * 4 ) * $height;
118 |
119 |
120 | $data = pack( 'VVVvvVVVVVV', 40, $width, ( $height * 2 ), 1, 32, 0, 0, 0, 0, 0, 0 );
121 |
122 | foreach ( $pixel_data as $color )
123 | $data .= pack( 'V', $color );
124 |
125 | foreach ( $opacity_data as $opacity )
126 | $data .= pack( 'N', $opacity );
127 |
128 |
129 | return [
130 | 'width' => $width,
131 | 'height' => $height,
132 | 'color_palette_colors' => 0,
133 | 'bits_per_pixel' => 32,
134 | 'size' => $image_header_size + $color_mask_size + $opacity_mask_size,
135 | 'data' => $data,
136 | ];
137 | }
138 | }
--------------------------------------------------------------------------------
/src/Image.php:
--------------------------------------------------------------------------------
1 | resource) {
20 | return $this->resource;
21 | }
22 | return $this->resource = ImageManager::create();
23 | }
24 |
25 | public function getRealType(): string {
26 | return $this->resource->getRealType();
27 | }
28 |
29 | public function getHashValue(): string {
30 | $w = 8;
31 | $h = 8;
32 | $image = clone $this->instance();
33 | $image->scale(new Box($w, $h));
34 | $total = 0;
35 | $array = array();
36 | for( $y = 0; $y < $h; $y++) {
37 | for ($x = 0; $x < $w; $x++) {
38 | $gray = ($image->getColorAt(new Point($x, $y)) >> 8) & 0xFF;
39 | if(!isset($array[$y])) $array[$y] = array();
40 | $array[$y][$x] = $gray;
41 | $total += $gray;
42 | }
43 | }
44 | unset($image);
45 | $average = intval($total / ($w * $h * 2));
46 | $hash = '';
47 | for($y = 0; $y < $h; $y++) {
48 | for($x = 0; $x < $w; $x++) {
49 | $hash .= ($array[$y][$x] >= $average) ? '1' : '0';
50 | }
51 | }
52 | return $hash;
53 | }
54 |
55 | /**
56 | * 保存,如果路径不存在则输出
57 | * @return bool
58 | */
59 | public function save(): bool {
60 | return $this->instance()->save();
61 | }
62 |
63 | /**
64 | * 另存为
65 | * @param string|null $output 如果为null 表示输出
66 | * @param string $type
67 | * @return bool
68 | */
69 | public function saveAs(string|null $output = null, string $type = ''): bool {
70 | return $this->instance()->saveAs($output, $type);
71 | }
72 |
73 | public function show() {
74 | if (!function_exists('app')) {
75 | throw new \Exception('not support show');
76 | }
77 | return app('response')->image($this)->send();
78 | }
79 |
80 | /**
81 | * 转化成base64编码
82 | * @return string
83 | */
84 | public function toBase64(): string {
85 | return $this->instance()->toBase64();
86 | }
87 | }
--------------------------------------------------------------------------------
/src/ImageCompare.php:
--------------------------------------------------------------------------------
1 | getHashValue();
13 | $hash2 = $image->getHashValue();
14 | if (strlen($hash1) !== strlen($hash2)) {
15 | return false;
16 | }
17 | $count = 0;
18 | $len = strlen($hash1);
19 | for ($i = 0; $i < $len; $i++) {
20 | if ($hash1[$i] !== $hash2[$i]) {
21 | $count++;
22 | }
23 | }
24 | return $count <= 10;
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/src/ImageHelper.php:
--------------------------------------------------------------------------------
1 | instance()->create($source->getSize());
23 | $length = $max - $min + 1;
24 | $count = ceil($length / $rows);
25 | $width = $source->getWidth() / $count;
26 | $height = $source->getHeight() / $rows;
27 | $points = [];
28 | foreach ($args as $i => $arg) {
29 | $arg = $arg - $min;
30 | $x = ($arg % $count) * $width;
31 | $y = floor($arg / $count) * $height;
32 | $srcX = ($i % $count) * $width;
33 | $srcY = floor($i / $count) * $height;
34 | // 计算显示是图片唯一
35 | $points[] = [- $x, -$y];
36 | $image->instance()->pastePart($source, new Point((int)$srcX, (int)$srcY),
37 | new Box($width, $height), new Point((int)$x, (int)$y));
38 |
39 | }
40 | return [$image, $points, [$width, $height]];
41 | }
42 |
43 | /**
44 | * 计算两点之间的直线距离
45 | * @param int|float $fromX
46 | * @param int|float $fromY
47 | * @param int|float $toX
48 | * @param int|float $toY
49 | * @return float
50 | */
51 | public static function distance(int|float $fromX, int|float $fromY,
52 | int|float $toX, int|float $toY): float {
53 | return sqrt(pow($fromX - $toX, 2) + pow($fromY - $toY, 2));
54 | }
55 |
56 | /**
57 | * 获取坐标 x 值
58 | * @param mixed $point
59 | * @return float
60 | */
61 | public static function x(mixed $point): float {
62 | if ($point instanceof PointInterface) {
63 | return $point->getX();
64 | }
65 | if (!is_array($point)) {
66 | return floatval($point);
67 | }
68 | if (isset($point['x'])) {
69 | return floatval($point['x']);
70 | }
71 | return isset($point[0]) ? floatval($point[0]) : 0;
72 | }
73 |
74 | /**
75 | * 获取坐标 y 值
76 | * @param mixed $point
77 | * @return float
78 | */
79 | public static function y(mixed $point): float {
80 | if ($point instanceof PointInterface) {
81 | return $point->getY();
82 | }
83 | if (!is_array($point)) {
84 | return floatval($point);
85 | }
86 | if (isset($point['y'])) {
87 | return floatval($point['y']);
88 | }
89 | return isset($point[1]) ? floatval($point[1]) : 0;
90 | }
91 |
92 | /**
93 | * 判断点在区域内
94 | * @param mixed $point
95 | * @param int|float $x
96 | * @param int|float $y
97 | * @param int|float $width
98 | * @param int|float $height
99 | * @return bool
100 | */
101 | public static function inBound(mixed $point, int|float $x, int|float $y,
102 | int|float $width, int|float $height): bool {
103 | $pX = static::x($point);
104 | $pY = static::y($point);
105 | return $pX >= $x && $pX <= $x + $width && $pY >= $y && $pY <= $y + $height;
106 | }
107 |
108 | /**
109 | * 生成成随机数
110 | * @param int $min
111 | * @param int $max
112 | * @param int $count
113 | * @return int[]
114 | */
115 | public static function randomInt(int $min, int $max, int $count = 2): array {
116 | if ($max - $min <= $count) {
117 | throw new \Exception('range is error');
118 | }
119 | $items = [];
120 | while ($count > 0) {
121 | $i = random_int($min, $max);
122 | if (in_array($i, $items)) {
123 | continue;
124 | }
125 | $items[] = $i;
126 | $count --;
127 | }
128 | return $items;
129 | }
130 | }
--------------------------------------------------------------------------------
/src/ImageManager.php:
--------------------------------------------------------------------------------
1 | Gd::class,
20 | 'gd2' => Gd::class,
21 | 'imagick' => Imagick::class,
22 | 'gmagick' => Gmagick::class,
23 | ];
24 |
25 | /**
26 | * @param string $driver
27 | * @return ImageAdapter
28 | */
29 | public static function create(string $driver = ''): ImageAdapter {
30 | if (empty($driver)) {
31 | $driver = !function_exists('config') ? 'gd' : config('image.driver', 'gd');
32 | }
33 | if (isset(self::$map[$driver])) {
34 | $driver = self::$map[$driver];
35 | }
36 | return new $driver();
37 | }
38 |
39 | /**
40 | * 创建字体
41 | * @param mixed $file
42 | * @param int $size
43 | * @param mixed $color
44 | * @return FontInterface
45 | */
46 | public static function createFont(mixed $file, int $size = 16, mixed $color = '#000') : FontInterface
47 | {
48 | return new Font(is_int($file) ? $file : (string)$file, $size, $color);
49 | }
50 |
51 | /**
52 | * 创建尺寸
53 | * @param float|int $width
54 | * @param float|int $height
55 | * @return BoxInterface
56 | */
57 | public static function createSize(float|int $width, float|int $height): BoxInterface
58 | {
59 | return new Box($width, $height);
60 | }
61 |
62 | /**
63 | * 创建点
64 | * @param float|int $x
65 | * @param float|int $y
66 | * @return PointInterface
67 | */
68 | public static function createPoint(float|int $x, float|int $y): PointInterface
69 | {
70 | return new Point((int)$x, (int)$y);
71 | }
72 | }
--------------------------------------------------------------------------------
/src/Node/BaseNode.php:
--------------------------------------------------------------------------------
1 | styles = $properties;
28 | $this->computed = [];
29 | return $this;
30 | }
31 |
32 | /**
33 | * 设置属性
34 | * @param string $name
35 | * @param $value
36 | * @return $this
37 | */
38 | public function style(string $name, mixed $value) {
39 | $this->styles[$name] = $value;
40 | $this->computed = [];
41 | return $this;
42 | }
43 |
44 | /**
45 | * 获取计算完成的属性
46 | * @param $name
47 | * @return mixed|null
48 | */
49 | public function computedStyle(string $name): mixed {
50 | return $this->computed[$name] ?? null;
51 | }
52 |
53 | public function getTop(): int {
54 | return $this->computed['y']
55 | - $this->computed['margin'][0];
56 | }
57 |
58 | public function getLeft(): int {
59 | return $this->computed['x'] - $this->computed['margin'][3];
60 | }
61 |
62 | public function getRight(): int {
63 | return $this->computed['x'] + $this->computed['width']
64 | + $this->computed['margin'][1];
65 | }
66 |
67 | public function getBottom(): int {
68 | return $this->computed['y'] + $this->computed['height'] + $this->computed['margin'][2];
69 | }
70 |
71 | public function innerX(): int {
72 | return $this->computed['x']
73 | + $this->computed['padding'][3];
74 | }
75 |
76 | public function innerY(): int {
77 | return $this->computed['y']
78 | + $this->computed['padding'][0];
79 | }
80 |
81 | protected function isFlow(): bool {
82 | return isset($this->computed['position']) &&
83 | ($this->computed['position'] === 'absolute' ||
84 | $this->computed['position'] === 'fixed');
85 | }
86 |
87 | /**
88 | * 获取元素在父元素的占位高度
89 | * @return int
90 | */
91 | public function placeholderHeight(): int {
92 | if (array_key_exists('placeholderHeight', $this->computed)) {
93 | return $this->computed['placeholderHeight'];
94 | }
95 | return $this->isFlow() ?
96 | 0 : $this->computed['outerHeight'];
97 | }
98 |
99 | /**
100 | * 重新计算属性
101 | * @param array $parentStyles
102 | */
103 | public function refresh(array $parentStyles): void {
104 | $styles = $this->styles;
105 | if (!isset($parentStyles['viewWidth'])) {
106 | $parentStyles['viewWidth'] = $styles['width'];
107 | $parentStyles['viewHeight'] = $styles['height'] ?? 0;
108 | }
109 | $styles['padding'] = NodeHelper::padding($styles);
110 | $styles['margin'] = NodeHelper::padding($styles, 'margin');
111 | $styles['baseX'] = $parentStyles['x'] ?? 0;
112 | $styles['baseY'] = $parentStyles['y'] ?? 0;
113 | $copyKeys = ['color', 'font-size', 'font', 'viewWidth', 'viewHeight', 'parentX', 'parentY'];
114 | foreach ($copyKeys as $key) {
115 | if (isset($styles[$key])) {
116 | continue;
117 | }
118 | if (isset($parentStyles[$key])) {
119 | $styles[$key] = $parentStyles[$key];
120 | }
121 | }
122 | $styles = $this->refreshPosition($styles, $parentStyles);
123 | $parentInnerWidth = $parentStyles['innerWidth'] ?? $styles['width'];
124 | $styles = $this->refreshSize($styles, $parentInnerWidth, $parentStyles);
125 | if (isset($styles['center'])) {
126 | $styles['x'] = ($parentStyles['outerWidth'] - $styles['width']) / 2;
127 | }
128 | $this->computed = $styles;
129 | }
130 |
131 | protected function refreshPosition(array $styles, array $parentStyles): array {
132 | if (isset($styles['fixed'])) {
133 | $styles['position'] = 'fixed';
134 | }
135 | if (isset($styles['x'])) {
136 | $styles['x'] +=
137 | (!isset($styles['position']) || $styles['position'] !== 'fixed'
138 | || $styles['position'] !== 'absolute' ? $parentStyles['x'] : 0) + $styles['margin'][1];
139 | } elseif (isset($styles['margin-left'])) {
140 | $styles['x'] = (isset($parentStyles['brother']) ?
141 | $parentStyles['brother']->getRight() : 0) + $styles['margin'][3];
142 | $styles['y'] = (isset($parentStyles['brother']) ? $parentStyles['brother']->getTop() : 0) + $styles['margin'][0];
143 | $styles['position'] = $styles['position'] ?? 'absolute';
144 | } else {
145 | $styles['x'] = ($parentStyles['x'] ?? 0)
146 | + $styles['margin'][1];
147 | }
148 | if (!isset($styles['y'])) {
149 | if (isset($styles['margin-top'])) {
150 | $styles['y'] = (isset($parentStyles['brother']) ? $parentStyles['brother']->getBottom() : 0)
151 | + $styles['margin'][0];
152 | } else {
153 | $styles['y'] = ($parentStyles['y'] ?? 0)
154 | + $styles['margin'][0];
155 | }
156 | } elseif (isset($this->styles['y'])) {
157 | $styles['y'] += ($parentStyles['parentY'] ?? 0)
158 | + $styles['margin'][0];
159 | }
160 |
161 | return $styles;
162 | }
163 |
164 | protected function refreshSize(array $styles, int $parentInnerWidth, array $parentStyles): array {
165 | if (isset($styles['width'])) {
166 | $styles['outerWidth'] = $styles['width'] + $styles['margin'][1] + $styles['margin'][3];
167 | } else {
168 | $styles['outerWidth'] = $parentInnerWidth;
169 | $styles['width'] = $parentInnerWidth - $styles['margin'][1] - $styles['margin'][3];
170 | }
171 | $styles['innerWidth'] = $styles['width'] - $styles['padding'][1] - $styles['padding'][3];
172 | if (isset($styles['height'])) {
173 | $styles['outerHeight'] = $styles['height'] + $styles['margin'][2]
174 | + $styles['margin'][0];
175 | $styles['innerHeight'] = $styles['height'] - $styles['padding'][0] - $styles['padding'][2];
176 | }
177 | return $styles;
178 | }
179 |
180 | /**
181 | * 绘制元素
182 | * @param Image $box
183 | */
184 | abstract public function draw(Image $box): void;
185 | }
--------------------------------------------------------------------------------
/src/Node/BorderNode.php:
--------------------------------------------------------------------------------
1 | content) {
16 | $styles['width'] = isset($styles['width']) ?
17 | NodeHelper::width($styles['width'], $parentStyles) : $parentInnerWidth;
18 | $styles['height'] = isset($styles['width']) ?
19 | NodeHelper::width($styles['height'], $parentStyles) : 1;
20 | }
21 | if (!isset($styles['width']) || !isset($styles['height'])) {
22 | $this->content->style('width', 'auto');
23 | $this->content->refresh(array_merge($parentStyles, [
24 | 'x' => $styles['x'] + $styles['padding'][3],
25 | 'y' => $styles['y'] + $styles['padding'][0],
26 | ]));
27 | $styles['width'] = $styles['width'] ?? $this->content->computedStyle('outerWidth');
28 | $styles['height'] = $styles['height'] ?? $this->content->computedStyle('outerHeight');
29 | }
30 | $styles['radius'] = NodeHelper::padding($styles, 'radius');
31 | return parent::refreshSize($styles, $parentInnerWidth, $parentStyles);
32 | }
33 |
34 | public function draw(Image $box): void {
35 | $startX = $this->computed['x'];
36 | $startY = $this->computed['y'];
37 | $endX = $startX + $this->computed['width'];
38 | $endY = $startY + $this->computed['height'];
39 | $color = $box->instance()->converterToColor($this->computed['color']);
40 | $radius = $this->computed['radius'];
41 | $each = function ($radius, $cb) {
42 | if ($radius < 1) {
43 | return;
44 | }
45 | for ($i = 1; $i < $radius; $i ++) {
46 | $j = $radius - sqrt(pow($radius, 2) - pow(abs($radius - $i), 2));
47 | $cb($i, intval($j));
48 | }
49 | };
50 | // top-left
51 | $each($radius[0], function ($i, $j) use ($box, $startX, $startY, $color) {
52 | $box->instance()->dot(new Point($startX + $i, $startY + $j), $color);
53 | });
54 | // top
55 | $box->instance()->line(new Point($startX + $radius[0], $startY),
56 | new Point($endX - $radius[1], $startY), $color);
57 | // top-right
58 | $each($radius[1], function ($i, $j) use ($box, $endX, $startY, $color) {
59 | $box->instance()->dot(new Point($endX - $i, $startY + $j), $color);
60 | });
61 | // right
62 | $box->instance()->line(new Point($endX, $startY + $radius[1]),
63 | new Point($endX, $endY - $radius[2]), $color);
64 | // bottom-right
65 | $each($radius[2], function ($i, $j) use ($box, $endX, $endY, $color) {
66 | $box->instance()->dot(new Point($endX - $i, $endY - $j), $color);
67 | });
68 | // bottom
69 | $box->instance()->line(new Point($endX - $radius[2], $endY),
70 | new Point($startX + $radius[3], $endY), $color);
71 | // bottom-left
72 | $each($radius[3], function ($i, $j) use ($box, $startX, $endY, $color) {
73 | $box->instance()->dot(new Point($startX + $i, $endY - $j), $color);
74 | });
75 | // left
76 | $box->instance()->line(new Point($startX, $endY - $radius[3]),
77 | new Point($startX, $startY + $radius[0]), $color);
78 | $this->content->draw($box);
79 | }
80 |
81 | /**
82 | * @param array|BaseNode|null $content
83 | * @param array{size: int, fixed: bool, margin: int} $properties
84 | * @return BaseNode
85 | */
86 | public static function create(array|BaseNode|null $content, array $properties = []): BaseNode {
87 | if (is_array($content)) {
88 | list($content, $properties) = [null, $content];
89 | }
90 | return (new static($content))->setStyles($properties);
91 | }
92 | }
--------------------------------------------------------------------------------
/src/Node/BoxNode.php:
--------------------------------------------------------------------------------
1 | children = array_merge($this->children, $nodes);
23 | $this->computed = [];
24 | return $this;
25 | }
26 |
27 | protected function refreshSize(array $styles, $parentInnerWidth, array $parentStyles): array {
28 | $styles = parent::refreshSize($styles, $parentInnerWidth, $parentStyles);
29 | $styles['parentX'] = $styles['x'];
30 | $styles['parentY'] = $styles['y'];
31 | $parentStyles = array_merge($parentStyles, $styles, [
32 | 'x' => $styles['x'] + $styles['padding'][3],
33 | 'y' => $styles['y'] + $styles['padding'][0],
34 | ]);
35 | $oldY = $parentStyles['y'];
36 | $maxY = $oldY;
37 | foreach ($this->children as $node) {
38 | $node->refresh($parentStyles);
39 | $parentStyles['y'] += $node->placeholderHeight();
40 | $parentStyles['brother'] = $node;
41 | $maxY = max($maxY, $node->getBottom());
42 | }
43 | if (!isset($this->styles['height'])) {
44 | $maxY -= $oldY;
45 | $styles['innerHeight'] = $maxY;
46 | $styles['height'] = $maxY + $styles['padding'][2] + $styles['padding'][3];
47 | $styles['outerHeight'] = $styles['height'] + $styles['margin'][2]
48 | + $styles['margin'][0];
49 | } else {
50 | $styles['outerHeight'] = $this->styles['height'] + $styles['margin'][0] + $styles['margin'][2];
51 | $styles['innerHeight'] = $this->styles['height'] - $styles['padding'][0] - $styles['padding'][2];
52 | }
53 | $styles['radius'] = NodeHelper::padding($styles, 'radius');
54 | return $styles;
55 | }
56 |
57 | public function draw(Image $box): void {
58 | if (isset($this->computed['background'])) {
59 | $x = $this->computed['x'];
60 | $y = $this->computed['y'];
61 | $width = $this->computed['width'];
62 | $height = $this->computed['height'];
63 | $radius = $this->computed['radius'];
64 | if ($this->computed['background'] instanceof ImgNode) {
65 | $this->drawBackgroundImage($box, $this->computed['background'], $x, $y, $width, $height, $radius);
66 | } else {
67 | $this->drawFill($box, $this->computed['background'], $x, $y, $width, $height, $radius);
68 | }
69 | }
70 | foreach ($this->children as $node) {
71 | $node->draw($box);
72 | }
73 | }
74 |
75 | /**
76 | * 从这里开始画
77 | * @return Image
78 | */
79 | public function beginDraw(): Image {
80 | $this->refresh([
81 | 'color' => '#000',
82 | 'font-size' => 16
83 | ]);
84 | $box = new Image();
85 | $box->instance()->create(new Box($this->computed['outerWidth'], $this->computed['outerHeight']));
86 | if (!isset($this->computed['background'])) {
87 | $this->computed['background'] = '#fff';
88 | }
89 | $this->draw($box);
90 | return $box;
91 | }
92 |
93 | protected function drawFill(Image $box, string $color, int $x, int $y, int $width, int $height, float|int $radius): void {
94 | if ($this->isEmpty($radius)) {
95 | $box->instance()->fill($color);
96 | return;
97 | }
98 | $img = ImageManager::create()
99 | ->create(new Box($width, $height), $color);
100 | $bg = $img->converterFromColor($img->converterToColor($color));
101 | $tempColor = [255 - $bg[0], 255 - $bg[1], 255 - $bg[2], 1];
102 | $this->setColorOutBox($img, $tempColor, $radius);
103 | $img->transparent($tempColor);
104 | $box->instance()->paste($img, new Point($x, $y));
105 | unset($img);
106 | }
107 |
108 | protected function drawBackgroundImage(Image $box, ImgNode $node, $x, $y, $width, $height, $radius) {
109 | if ($this->isEmpty($radius)) {
110 | $node->refresh($this->computed);
111 | $node->refreshAsBackground($this->computed);
112 | $node->draw($box);
113 | return;
114 | }
115 | $image = clone $node->getImage();
116 | $image->scale(new Box($width, $height));
117 | $tempColor = [0, 0, 0, 1];
118 | $this->setColorOutBox($image, $tempColor, $radius);
119 | $image->transparent($tempColor);
120 | $box->instance()->paste($image, new Point($x, $y));
121 | }
122 |
123 | protected function setColorOutBox(ImageAdapter $box, $color, $radius) {
124 | $width = $box->getWidth();
125 | $height = $box->getHeight();
126 | $each = function ($radius, $cb) {
127 | if ($radius < 1) {
128 | return;
129 | }
130 | for ($i = 0; $i < $radius; $i ++) {
131 | for ($j = 0; $j < $radius; $j ++) {
132 | if (pow($radius - $i, 2) + pow($radius - $j, 2)
133 | > pow($radius, 2)) {
134 | $cb($i, $j);
135 | } else {
136 | break;
137 | }
138 | }
139 | }
140 | };
141 | $each($radius[0], function ($i, $j) use ($box, $color) {
142 | $box->dot(new Point($i, $j), $color);
143 | });
144 | $each($radius[1], function ($i, $j) use ($width, $box, $color) {
145 | $box->dot(new Point($width - $i, $j), $color);
146 | });
147 | $each($radius[2], function ($i, $j) use ($width, $height, $box, $color) {
148 | $box->dot(new Point($width - $i, $height - $j), $color);
149 | });
150 | $each($radius[3], function ($i, $j) use ($width, $height, $box, $color) {
151 | $box->dot(new Point($i, $height - $j), $color);
152 | });
153 | }
154 |
155 | protected function isBoxInner($x, $y, $width, $height, $radius): bool {
156 | if ($radius[0] > 0) {
157 | if ($x < $radius[0] && $y < $radius[0]) {
158 | return pow($radius[0] - $x, 2) + pow($radius[0] - $y, 2)
159 | < pow($radius[0], 2);
160 | }
161 | }
162 | if ($radius[1] > 0) {
163 | if ($x > $width - $radius[1] && $y < $radius[1]) {
164 | return pow($x - $width + $radius[1], 2) + pow($radius[1] - $y, 2)
165 | < pow($radius[1], 2);
166 | }
167 | }
168 | if ($radius[2] > 0) {
169 | if ($x > $width - $radius[2] && $y > $height - $radius[2]) {
170 | return pow($x - $width + $radius[2], 2) + pow($y - $height + $radius[2], 2)
171 | < pow($radius[2], 2);
172 | }
173 | }
174 | if ($radius[3] > 0) {
175 | if ($x < $radius[3] && $y > $height - $radius[3]) {
176 | return pow($radius[3] - $x, 2) + pow($y - $height + $radius[3], 2)
177 | < pow($radius[3], 2);
178 | }
179 | }
180 | return $x > 0 && $x < $width && $y > 0 && $y < $height;
181 | }
182 |
183 | protected function isEmpty($data): bool {
184 | if (empty($data)) {
185 | return true;
186 | }
187 | foreach ($data as $val) {
188 | if (!empty($val)) {
189 | return false;
190 | }
191 | }
192 | return true;
193 | }
194 |
195 | /**
196 | * @param array{padding: int, background: string, width: int} $properties
197 | * @return BoxNode
198 | */
199 | public static function create(array $properties = []) {
200 | return (new static())->setStyles($properties);
201 | }
202 |
203 | public static function parse(string $content): BoxNode {
204 | $lines = explode(PHP_EOL, ltrim($content));
205 | // 修复linux换行符
206 | if (preg_match('/^\[(.+)\]$/', trim($lines[0]), $match)) {
207 | $box = static::create(static::parseProperties($match[1]));
208 | array_shift($lines);
209 | } else {
210 | $box = new static();
211 | }
212 | foreach ($lines as $line) {
213 | $box->append(static::parseNode($line));
214 | }
215 | return $box;
216 | }
217 |
218 | protected static function parseNode(string $content): BaseNode {
219 | if (!preg_match('/^\[(.+)\](.*)$/', $content, $match)) {
220 | return TextNode::create($content);
221 | }
222 | $properties = static::parseProperties($match[1]);
223 | if (isset($properties['img'])) {
224 | return ImgNode::create($match[2], $properties);
225 | }
226 | return TextNode::create($match[2], $properties);
227 | }
228 |
229 | protected static function parseProperties(string $content): array {
230 | $properties = [];
231 | foreach (explode(' ', $content) as $line) {
232 | $args = explode('=', $line, 2);
233 | if (count($args) === 1) {
234 | $args[1] = true;
235 | if (str_starts_with($args[0], '!')) {
236 | $args[1] = false;
237 | $args[0] = substr($args[0], 1);
238 | }
239 |
240 | }
241 | $properties[$args[0]] = static::parseVal($args[1]);
242 | }
243 | return $properties;
244 | }
245 |
246 | protected static function parseVal(mixed $val): mixed {
247 | if (is_numeric($val)) {
248 | return $val;
249 | }
250 | if ($val === 'true') {
251 | return true;
252 | }
253 | if ($val === 'false') {
254 | return false;
255 | }
256 | return $val;
257 | }
258 | }
--------------------------------------------------------------------------------
/src/Node/CircleNode.php:
--------------------------------------------------------------------------------
1 | computed['fill'])) {
18 | $box->instance()->circle(new Point($this->computed['x'], $this->computed['y']), $this->computed['radius'], $this->computed['color'], false);
19 | return;
20 | }
21 | if (is_string($this->computed['fill'])) {
22 | $box->instance()->circle(new Point($this->computed['x'], $this->computed['y']),
23 | $this->computed['radius'], $this->computed['fill'], true);
24 | return;
25 | }
26 | $node = $this->computed['fill'];
27 | $width = $this->computed['radius'] * 2;
28 | if ($node instanceof ImgNode) {
29 | $image = clone $node->getImage();
30 | $size = $image->getSize();
31 | if ($image->getWidth() < $image->getHeight()) {
32 | $image->scale($size->widen($width));
33 | } else {
34 | $image->scale($size->heighten($width));
35 | }
36 | $r = $this->computed['radius'];
37 | $startX = $this->computed['x'] - $r;
38 | $startY = $this->computed['y'] - $r;
39 | $isBlack = function ($rgb) {
40 | return $rgb[0] === 0 && $rgb[1] === 0 && $rgb[2] === 0 && $rgb[3] === 0;
41 | };
42 | for ($x = 0; $x < $width; $x ++) {
43 | for ($y = 0; $y < $width; $y ++) {
44 | if (pow($x - $r, 2) + pow($y - $r, 2) < pow($r, 2)) {
45 | $color = $image->getColorAt(new Point($x, $y));
46 | if ($image->getRealType() === 'png' &&
47 | $isBlack($image->converterFromColor($color))) {
48 | continue;
49 | }
50 | $box->instance()->dot(new Point($x + $startX, $y + $startY),
51 | $color);
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * @param string $x
60 | * @param string $y
61 | * @param int|float $radius
62 | * @param string $fill
63 | * @param bool|string $color
64 | * @param int $thickness
65 | * @return CircleNode
66 | */
67 | public static function create(string $x, string $y, int|float $radius, string $fill, bool|string $color = false, int $thickness = 1) {
68 | return (new static())->setStyles(compact('x', 'y', 'radius', 'fill', 'color', 'thickness'));
69 | }
70 | }
--------------------------------------------------------------------------------
/src/Node/ImgNode.php:
--------------------------------------------------------------------------------
1 | setSrc($src);
26 | $this->setStyles($properties);
27 | }
28 |
29 | /**
30 | * @param string $src
31 | * @return ImgNode
32 | */
33 | public function setSrc(string $src) {
34 | $this->src = trim($src);
35 | $this->image = null;
36 | return $this;
37 | }
38 |
39 | public function getImage() {
40 | if (empty($this->image)) {
41 | $this->image = ImageManager::create()->open($this->src);
42 | }
43 | return $this->image;
44 | }
45 |
46 | protected function refreshSize(array $styles, int $parentInnerWidth, array $parentStyles): array {
47 | $styles['width'] = $this->getWidth($parentStyles);
48 | $styles['height'] = $this->getHeight($parentStyles);
49 | return parent::refreshSize($styles, $parentInnerWidth, $parentStyles);
50 | }
51 |
52 | public function refreshAsBackground(array $parentStyles): void {
53 | if (isset($this->styles['full'])) {
54 | $this->computed['width'] = $parentStyles['width'];
55 | $this->computed['height'] = $parentStyles['height'];
56 | return;
57 | }
58 | if (isset($this->styles['width']) && strpos($this->styles['width'], '%')) {
59 | $this->computed['width'] = NodeHelper::percentage($parentStyles['width'], $this->styles['width']);
60 | }
61 | if (isset($this->styles['height']) && strpos($this->styles['height'], '%')) {
62 | $this->computed['height'] = NodeHelper::percentage($parentStyles['height'], $this->styles['height']);
63 | }
64 | }
65 |
66 | public function outerHeight(): int {
67 | return $this->computed['outerHeight'];
68 | }
69 |
70 | protected function getWidth(array $properties) {
71 | if (!isset($this->styles['width'])
72 | && isset($this->styles['height'])
73 | && is_numeric($this->styles['height'])) {
74 | return $this->styles['height'] * $this->getImage()->getWidth()
75 | / $this->getImage()->getHeight();
76 | }
77 | $width = NodeHelper::width(isset($this->styles['width']) ? $this->styles['width'] : null, $properties);
78 | if (!empty($width)) {
79 | return $width;
80 | }
81 | return $this->getImage()->getWidth();
82 | }
83 |
84 | protected function getHeight(array $properties) {
85 | if (!isset($this->styles['height'])
86 | && isset($this->styles['width'])
87 | && is_numeric($this->styles['width'])) {
88 | return $this->styles['width'] * $this->getImage()->getHeight() / $this->getImage()->getWidth();
89 | }
90 | $width = NodeHelper::width($this->styles['height'] ?? null, $properties, 'height');
91 | if (!empty($width)) {
92 | return $width;
93 | }
94 | return $this->getImage()->getHeight();
95 | }
96 |
97 | public function draw(Image $box): void {
98 | $img = $this->getImage();
99 | if ($img->getWidth() != $this->computed['width'] || $img->getHeight() != $this->computed['height']) {
100 | $img = clone $this->getImage();
101 | $img->scale(new Box($this->computed['width'], $this->computed['height']));
102 | }
103 | $box->instance()->paste($img, new Point($this->computed['x'], $this->computed['y']));
104 | }
105 |
106 | /**
107 | * @param string|File $src
108 | * @param array{width: int, height: int, center: bool} $properties
109 | * @return ImgNode
110 | */
111 | public static function create(string|File $src, array $properties = []) {
112 | return (new static((string)$src))->setStyles($properties);
113 | }
114 | }
--------------------------------------------------------------------------------
/src/Node/LineNode.php:
--------------------------------------------------------------------------------
1 | styles['points'] = $this->styles['points'] ?? [];
12 | if ($this->styles['fixed']) {
13 | $this->computed['placeholderHeight'] = 0;
14 | return;
15 | }
16 | $this->styles['padding'] = NodeHelper::padding($this->styles);
17 | $this->styles['margin'] = NodeHelper::padding($this->styles, 'margin');
18 | if (!isset($this->styles['points'][0]) || empty($this->styles['points'][0])) {
19 | $this->styles['points'][0] = [
20 | $parentStyles['x'] + $this->styles['margin'][3] + $this->styles['padding'][3],
21 | $parentStyles['y'] + $this->styles['margin'][0] + $this->styles['padding'][0],
22 | ];
23 | }
24 | $outerHeight = $this->styles['margin'][0] + $this->styles['padding'][0] +
25 | $this->styles['margin'][2] + $this->styles['padding'][2];
26 | if (isset($this->styles['points'][1])) {
27 | $this->computed['placeholderHeight'] = abs($this->styles['points'][0][1] - $this->styles['points'][1][1])
28 | + $outerHeight;
29 | return;
30 | }
31 | if (isset($this->styles['width'])) {
32 | $width = NodeHelper::width($this->styles['width'], $parentStyles);
33 | $this->styles['points'][1] = [
34 | $this->styles['points'][0][0] + $width,
35 | $this->styles['points'][0][1]
36 | ];
37 | $this->computed['placeholderHeight'] = $outerHeight;
38 | return;
39 | }
40 | if (isset($this->styles['height'])) {
41 | $height = NodeHelper::width($this->styles['height'], $parentStyles);
42 | $this->styles['points'][1] = [
43 | $this->styles['points'][0][0],
44 | $this->styles['points'][0][1] + $height
45 | ];
46 | $this->computed['placeholderHeight'] = $height + $outerHeight;
47 | return;
48 | }
49 | $this->computed['placeholderHeight'] = 0;
50 | }
51 |
52 | public function draw(Image $box): void {
53 | $points = $this->styles['points'];
54 | for ($i = count($points) - 1; $i > 0; $i --) {
55 | $box->instance()->line(new Point($points[$i][0], $points[$i][1]),
56 | new Point($points[$i - 1][0], $points[$i - 1][1]), $this->styles['color']);
57 | }
58 | }
59 |
60 | /**
61 | * @param array|int $x
62 | * @param int $y
63 | * @param int $x2
64 | * @param int $y2
65 | * @param array{size: int, fixed: bool, color: string} $properties
66 | * @return LineNode
67 | */
68 | public static function create(array|int $x, int $y = 0, int $x2 = 0, int $y2 = 0, array $properties = []) {
69 | if (is_array($x)) {
70 | $properties = $x;
71 | } else {
72 | $properties['points'] = [
73 | [$x, $y],
74 | [$x2, $y2]
75 | ];
76 | }
77 | return (new static())
78 | ->setStyles($properties);
79 | }
80 |
81 | }
--------------------------------------------------------------------------------
/src/Node/NodeHelper.php:
--------------------------------------------------------------------------------
1 | $val) {
15 | if (count($map) <= $i) {
16 | break;
17 | }
18 | $name = sprintf('%s-%s', $key, $map[$i]);
19 | if (isset($properties[$name])) {
20 | $data[$i] = $properties[$name];
21 | }
22 | }
23 | return $data;
24 | };
25 | if (!isset($properties[$key])) {
26 | return $formatVal([0, 0, 0, 0]);
27 | }
28 | $property = $properties[$key];
29 | if (!is_array($property)) {
30 | $property = explode(',', $property);
31 | }
32 | $count = count($property);
33 | if ($count === 1) {
34 | return $formatVal([$property[0], $property[0], $property[0], $property[0]]);
35 | }
36 | if ($count === 2) {
37 | return $formatVal([$property[0], $property[1], $property[0], $property[1]]);
38 | }
39 | if ($count === 3) {
40 | return $formatVal([$property[0], $property[1], $property[2], $property[1]]);
41 | }
42 | return $formatVal($property);
43 | }
44 |
45 | public static function width($value, array $parentProperties, $property = 'width') {
46 | if (empty($value)) {
47 | return null;
48 | }
49 | if (is_numeric($value)) {
50 | return $value;
51 | }
52 | if (!preg_match('/([\d\.]+)\s*(%|vw|vh)/', $value, $match)) {
53 | return null;
54 | }
55 |
56 | $arg = floatval($match[1]);
57 | if ($match[2] === '%') {
58 | $key = 'inner'.Str::studly($property);
59 | if (!isset($parentProperties[$key])) {
60 | $key = 'innerWidth';
61 | }
62 | return $arg * $parentProperties[$key] / 100;
63 | }
64 | if ($match[2] === 'vw') {
65 | return $arg * $parentProperties['viewWidth'] / 100;
66 | }
67 | return $arg * $parentProperties['viewHeight'] / 100;
68 | }
69 |
70 | /**
71 | * 根据百分比算值
72 | * @param $length
73 | * @param $val
74 | * @return int
75 | */
76 | public static function percentage($length, $val) {
77 | if (is_numeric($val)) {
78 | return intval($val);
79 | }
80 | if (!preg_match('/([\d\.]+)\s*(%|vw|vh)/', $val, $match)) {
81 | return intval($val);
82 | }
83 | return intval($length * floatval($match[1]) / 100);
84 | }
85 |
86 | public static function step($cb, $start, $end, $step = 1) {
87 | $diff = ($start > $end ? -1 : 1) * abs($step === 0 ? 1 : $step);
88 | while (true) {
89 | $cb($start);
90 | if (($diff > 0 && $start >= $end) || ($diff < 0 && $start <= $end)) {
91 | return;
92 | }
93 | $start += $diff;
94 | if (($diff > 0 && $start > $end) || ($diff < 0 && $start < $end)) {
95 | $start = $end;
96 | }
97 | }
98 | }
99 |
100 | public static function orDefault($key, array $args, array $args1, mixed $default = 0): mixed {
101 | if (isset($args[$key])) {
102 | return $args[$key];
103 | }
104 | if (isset($args1[$key])) {
105 | return $args1[$key];
106 | }
107 | return $default;
108 | }
109 |
110 | }
--------------------------------------------------------------------------------
/src/Node/RectNode.php:
--------------------------------------------------------------------------------
1 | instance()->rectangle(new Point($this->computed['x'], $this->computed['y'] + $this->computed['height']),
12 | new Point($this->computed['x'] + $this->computed['width'], $this->computed['y']), $this->computed['color']);
13 | }
14 |
15 | /**
16 | * @param array{points: array[], color: string} $properties
17 | * @return RectNode
18 | */
19 | public static function create(array $properties) {
20 | return (new static())->setStyles($properties);
21 | }
22 | }
--------------------------------------------------------------------------------
/src/Node/TextNode.php:
--------------------------------------------------------------------------------
1 | text($content);
31 | }
32 |
33 | protected function getTmpImage() {
34 | if (empty($this->tmpImage)) {
35 | $this->tmpImage = ImageManager::create()->create(new Box(100, 30));
36 | }
37 | return $this->tmpImage;
38 | }
39 |
40 | protected function isWrap() {
41 | return !array_key_exists('wrap', $this->styles)
42 | || $this->styles['wrap'] !== false;
43 | }
44 |
45 | /**
46 | * @param string $content
47 | * @return $this
48 | */
49 | public function text(string $content) {
50 | $this->content = $content;
51 | return $this;
52 | }
53 |
54 | protected function refreshSize(array $styles, int $parentInnerWidth, array $parentStyles): array
55 | {
56 | $innerWidth = !isset($styles['width']) || $styles['width'] === 'auto' ? $parentInnerWidth :
57 | ($this->styles['width'] - $styles['padding'][1] - $styles['padding'][3]);
58 | $styles['lineCenter'] = $parentStyles['innerWidth'] / 2 + $parentStyles['x'];
59 | $styles['font-size'] = NodeHelper::orDefault('font-size', $styles, $parentStyles, 16);
60 | $styles['lineSpace'] = NodeHelper::orDefault('lineSpace', $styles, $parentStyles, 6);
61 | $styles['letterSpace'] = NodeHelper::orDefault('letterSpace', $styles, $parentStyles, 0);
62 | $styles['color'] = NodeHelper::orDefault('color', $styles, $parentStyles, '#333');
63 | $styles['font'] = NodeHelper::orDefault('font', $styles, $parentStyles, 1);
64 |
65 | if (str_starts_with($styles['font'], '@')) {
66 | $styles['font'] = $parentStyles[substr($styles['font'], 1)];
67 | }
68 | $this->font = new Font($styles['font'], $styles['font-size'],
69 | $styles['color']);
70 | $this->computed = $styles;
71 | list($styles['lines'], $styles['contentWidth']) = $this->getLines($innerWidth);
72 | if (isset($styles['width']) && $styles['width'] === 'auto') {
73 | $styles['width'] = $styles['contentWidth'] + $styles['padding'][1] + $styles['padding'][3];
74 | }
75 | $styles['height'] = count($styles['lines'])
76 | * ($styles['font-size'] + $styles['lineSpace']) + $styles['padding'][0] + $styles['padding'][2];
77 | return parent::refreshSize($styles, $parentInnerWidth, $parentStyles);
78 | }
79 |
80 | public function draw(Image $box): void {
81 | // $space = ($this->computed['font-size'] + $this->computed['letterSpace']) / 2;
82 | $lineSpace = $this->computed['font-size'] + $this->computed['lineSpace'];
83 | $x = $this->innerX();
84 | $y = $this->innerY() + $this->computed['font-size'];
85 | $center = isset($this->computed['center']);
86 | foreach ($this->computed['lines'] as $line) {
87 | $startX = $x;
88 | if ($center) {
89 | $startX = $this->computed['lineCenter'] - $this->getFontWidth($line) / 2;
90 | }
91 | $box->instance()->text($line, $this->font, new Point($startX, $y));
92 | // foreach ($line as $font) {
93 | // if (!is_null($font)) {
94 | // $box->text($font, $startX, $y, $this->computed['font-size'],
95 | // $this->computed['color'], $this->computed['font']);
96 | // }
97 | // $startX += $space;
98 | // }
99 | $y += $lineSpace;
100 | }
101 | }
102 |
103 | protected function getLines(int $maxWidth): array {
104 | if (!$this->isWrap()) {
105 | return [
106 | [$this->content],
107 | min($this->getFontWidth($this->content), $maxWidth),
108 | ];
109 | }
110 | $lines = [];
111 | $length = mb_strlen($this->content);
112 | $start = 0;
113 | $width = 0;
114 | for ($i = 1; $i <= $length; $i ++) {
115 | $line = mb_substr($this->content, $start, $i - $start);
116 | $w = $this->getFontWidth($line);
117 | if (
118 | $w > $maxWidth
119 | ) {
120 | $lines[] = mb_substr($this->content, $start, $i - $start - 1);
121 | $width = $maxWidth;
122 | $start = $i - 1;
123 | } elseif ($i === $length) {
124 | $lines[] = $line;
125 | $width = max($width, $w);
126 | break;
127 | }
128 | }
129 | return [$lines, $width];
130 | }
131 |
132 | protected function getFontWidth($font) {
133 | $box = $this->getTmpImage()->fontSize($font, $this->font, 0);
134 | return $box->getWidth();
135 | }
136 |
137 | /**
138 | * @param string $content
139 | * @param array{size: int, color: string, letterSpace: int, lineSpace: int, wrap: bool, font: FontInterface, center: bool} $properties
140 | * @return TextNode
141 | */
142 | public static function create(string $content, array $properties = []) {
143 | return (new static($content))->setStyles($properties);
144 | }
145 | }
--------------------------------------------------------------------------------
/src/QrCode.php:
--------------------------------------------------------------------------------
1 | level = $level;
42 | return $this;
43 | }
44 |
45 | /**
46 | * 尺寸
47 | * @param int $width
48 | * @param int $height
49 | * @return $this
50 | */
51 | public function setSize(int $width, int $height) {
52 | $this->width = $width;
53 | $this->height = $height;
54 | return $this;
55 | }
56 |
57 | /**
58 | * @param string $encoding
59 | * @return QrCode
60 | */
61 | public function setEncoding(string $encoding) {
62 | $this->encoding = $encoding;
63 | return $this;
64 | }
65 |
66 | /**
67 | * 生成二维码
68 | * @param string $value
69 | * @return $this
70 | */
71 | public function encode(string $value) {
72 | $renderer = new QrCodeImageRenderer(new RendererStyle($this->width));
73 | $renderer->render(Encoder::encode($value, ErrorCorrectionLevel::forBits($this->level), $this->encoding));
74 | $this->resource = $renderer->getImage();
75 | return $this;
76 | }
77 |
78 | /**
79 | * 添加LOGO
80 | * @param string|Image|resource $logo
81 | * @return $this
82 | */
83 | public function addLogo($logo) {
84 | if ($logo instanceof Image) {
85 | $logo = $logo->instance();
86 | } elseif (!$logo instanceof ImageAdapter) {
87 | $logo = ImageManager::create()->loadResource($logo);
88 | }
89 | $width = ($this->width - $logo->getWidth()) / 2;
90 | $logoWidth = $this->width / 5;
91 | $logo->scale(new Box($logoWidth, $logoWidth));
92 | $this->instance()->paste($logo, new Point($width,
93 | $width));
94 | return $this;
95 | }
96 |
97 | /**
98 | * 发送邮件二维码
99 | * @param $email
100 | * @param null $subject
101 | * @param null $body
102 | * @return QrCode
103 | */
104 | public function email($email, $subject = null, $body = null) {
105 | $email = 'mailto:'.$email;
106 | if (!is_null($subject) || !is_null($body)) {
107 | $email .= '?'.http_build_query(compact('subject', 'body'));
108 | }
109 | return $this->encode($email);
110 | }
111 |
112 | /**
113 | * 地理位置二维码
114 | * @param $latitude
115 | * @param $longitude
116 | * @return QrCode
117 | */
118 | public function geo($latitude, $longitude) {
119 | return $this->encode(sprintf('geo:%s,%s', $latitude, $longitude));
120 | }
121 |
122 | /**
123 | * 电话二维码
124 | * @param $phone
125 | * @return QrCode
126 | */
127 | public function tel($phone) {
128 | return $this->encode('tel:'.$phone);
129 | }
130 |
131 | /**
132 | * 发送短信二维码
133 | * @param $phone
134 | * @param null $message
135 | * @return QrCode
136 | */
137 | public function sms($phone, $message = null) {
138 | $phone = 'sms:'.$phone;
139 | if (!is_null($message)) {
140 | $phone .= ':'. $message;
141 | }
142 | return $this->encode($phone);
143 | }
144 |
145 | /**
146 | * WIFI 二维码
147 | * @param string $ssid 网络的SSID
148 | * @param string $password
149 | * @param string $encryption WPA/WEP
150 | * @param bool $hidden true/false 是否是隐藏网络
151 | * @return QrCode
152 | */
153 | public function wifi($ssid = null, $password = null, $encryption = null, $hidden = null) {
154 | $wifi = 'WIFI:';
155 | if (!is_null($encryption)) {
156 | $wifi .= 'T:'.$encryption.';';
157 | }
158 | if (!is_null($ssid)) {
159 | $wifi .= 'S:'.$ssid.';';
160 | }
161 | if (!is_null($password)) {
162 | $wifi .= 'P:'.$password.';';
163 | }
164 | if (!is_null($hidden)) {
165 | $wifi .= 'H:'.($hidden === true ? 'true' : 'false').';';
166 | }
167 | return $this->encode($wifi);
168 | }
169 |
170 | /**
171 | * 比特币
172 | * @param $address
173 | * @param $amount
174 | * @param $label
175 | * @param $message
176 | * @param $returnAddress
177 | * @return QrCode
178 | */
179 | public function btc($address, $amount, $label, $message, $returnAddress) {
180 | return $this->encode(sprintf('bitcoin:%s?%s', $address, http_build_query([
181 | 'amount' => $amount,
182 | 'label' => $label,
183 | '$message' => $message,
184 | 'r' => $returnAddress,
185 | ])));
186 | }
187 |
188 | /**
189 | * 解码
190 | * @return string
191 | */
192 | public function decode() {
193 | return (new QrReader($this->instance()->getResource(), QrReader::SOURCE_TYPE_RESOURCE))->text();
194 | }
195 | }
--------------------------------------------------------------------------------
/src/Renderer/QrCodeImageRenderer.php:
--------------------------------------------------------------------------------
1 | rendererStyle->getSize();
33 | $margin = $this->rendererStyle->getMargin();
34 | $matrix = $qrCode->getMatrix();
35 | $matrixSize = $matrix->getWidth();
36 |
37 | if ($matrixSize !== $matrix->getHeight()) {
38 | throw new InvalidArgumentException('Matrix must have the same width and height');
39 | }
40 | $totalSize = $matrixSize + ($margin * 2);
41 | $this->blockSize = $size / $totalSize;
42 |
43 | $topPadding = $leftPadding = (int) (($size - ($matrixSize * $this->blockSize)) / 2);
44 |
45 | $fill = $this->rendererStyle->getFill();
46 | $this->addColor('background', $fill->getBackgroundColor());
47 | $this->addColor('foreground', $fill->getForegroundColor());
48 | $this->image = ImageManager::create()->create(new Box($size, $size), $this->colors['background']);
49 |
50 | for ($inputY = 0, $outputY = $topPadding; $inputY < $matrixSize; $inputY++, $outputY += $this->blockSize) {
51 | for ($inputX = 0, $outputX = $leftPadding; $inputX < $matrixSize; $inputX++, $outputX += $this->blockSize) {
52 | if ($matrix->get($inputX, $inputY) === 1) {
53 | $this->drawBlock($outputX, $outputY, 'foreground');
54 | }
55 | }
56 | }
57 |
58 | return $this->getByteStream();
59 | }
60 |
61 |
62 | public function getImage() {
63 | return $this->image;
64 | }
65 |
66 |
67 | protected function getByteStream(): string {
68 | ob_start();
69 | $this->image->saveAs();
70 | $blob = ob_get_contents();
71 | ob_end_clean();
72 | return $blob;
73 | }
74 |
75 | protected function drawBlock(int|float $x, int|float $y, string $colorId) {
76 | $this->image->rectangle(
77 | new Point((int)$x, (int)$y),
78 | new Point((int)($x + $this->blockSize - 1), (int)($y + $this->blockSize - 1)),
79 | $this->colors[$colorId],
80 | true,
81 | );
82 | }
83 |
84 | protected function addColor(string $id, ColorInterface $color) {
85 | $this->colors[$id] = $this->formatColor($color);
86 | }
87 |
88 | protected function formatColor(ColorInterface $color) : array {
89 | $alpha = 100;
90 | if ($color instanceof Alpha) {
91 | $alpha = $color->getAlpha();
92 | $color = $color->getBaseColor();
93 | }
94 | $rgb = $color->toRgb();
95 | return [$rgb->getRed(), $rgb->getGreen(), $rgb->getBlue(), $alpha / 100];
96 | }
97 | }
--------------------------------------------------------------------------------
/src/SlideCaptcha.php:
--------------------------------------------------------------------------------
1 | .5,
13 | 'slices' => 8, // 切片数量
14 | 'width' => 300,
15 | 'height' => 130,
16 | 'shapeWidth' => 20,
17 | 'shapeHeight' => 20
18 | ];
19 |
20 | /**
21 | * @var integer[]
22 | */
23 | protected array $point = [];
24 |
25 | /**
26 | * @var ImageAdapter
27 | */
28 | protected ImageAdapter|null $shapeImage = null;
29 |
30 | /**
31 | * @var ImageAdapter
32 | */
33 | protected ImageAdapter|null $slideImage = null;
34 |
35 | public function setConfigs(array $configs): void {
36 | $this->configs = array_merge($this->configs, $configs);
37 | }
38 |
39 | public function isOnlyImage(): bool {
40 | return false;
41 | }
42 |
43 | public function setShape(mixed $shape) {
44 | if ($shape instanceof Image) {
45 | $shape = $shape->instance();
46 | } elseif (!$shape instanceof ImageAdapter) {
47 | $shape = ImageManager::create()->loadResource($shape);
48 | }
49 | if ($shape->getWidth() > $this->configs['width'] || $shape->getHeight() > $this->configs['height']) {
50 | $shape->scale(new Box($this->configs['shapeWidth'], $this->configs['shapeHeight']));
51 | }
52 | $this->shapeImage = $shape;
53 | return $this;
54 | }
55 |
56 | public function setPoint(int $x, int $y) {
57 | $this->point = [$x, $y];
58 | return $this;
59 | }
60 |
61 | /**
62 | * @return integer[]
63 | */
64 | public function getPoint(): array {
65 | return $this->point;
66 | }
67 |
68 | /**
69 | * @return ImageAdapter
70 | */
71 | public function getSlideImage(): ImageAdapter {
72 | return $this->slideImage;
73 | }
74 |
75 | public function generate(): mixed {
76 | $this->instance()->scale(new Box($this->configs['width'],
77 | $this->configs['height']));
78 | $this->drawBox();
79 | return $this->point;
80 | }
81 |
82 | public function drawBox(): void {
83 | $width = $this->shapeImage->getWidth();
84 | $height = $this->shapeImage->getHeight();
85 | if (empty($this->point)) {
86 | $this->point = [
87 | rand($width, $this->instance()->getWidth() - $width),
88 | rand(0, $this->instance()->getHeight() - $height)
89 | ];
90 | }
91 | $this->slideImage = ImageManager::create()->create(new Box($width, $height))
92 | ->setRealType('png');
93 | $alpha = floatval($this->configs['alpha']);
94 | for ($i = 0; $i < $width; $i ++) {
95 | for ($j = 0; $j < $height; $j ++) {
96 | $current = $this->isValidBound($i, $j);
97 | if (!$current) {
98 | continue;
99 | }
100 | $real_x = $i + $this->point[0];
101 | $real_y = $j + $this->point[1];
102 | $color = $this->instance()->getColorAt(new Point($real_x, $real_y));
103 | $this->slideImage->dot(new Point($i, $j), $color);
104 | list($r, $g, $b) = $this->instance()->converterFromColor($color);
105 | $this->instance()->dot(new Point($real_x, $real_y), [
106 | floor($r * $alpha),
107 | floor($g * $alpha),
108 | floor($b * $alpha),
109 | ]);
110 | }
111 | }
112 | $this->slideImage->transparent([0, 0, 0]);
113 | }
114 |
115 | /**
116 | * 是否是有效的区域
117 | * @param int $x
118 | * @param int $y
119 | * @return true
120 | */
121 | public function isValidBound(int $x, int $y): bool {
122 | list($r, $g, $b) = $this->shapeImage->converterFromColor(
123 | $this->shapeImage->getColorAt(new Point($x, $y)));
124 | return $r < 240 || $g < 240 || $b < 240;
125 | }
126 |
127 | /**
128 | * 按指定数值打乱排序重新生成图片
129 | * @param int[] $args
130 | * @param int $rows 多少层 默认2层
131 | * @return array [Image, point[], [width, height]]
132 | */
133 | public function sortBy(array $args, int $rows = 2): array {
134 | return ImageHelper::sortBy($this->instance(), $args, $rows);
135 | }
136 |
137 | public function verify(mixed $value, mixed $source): bool {
138 | $x = ImageHelper::x($value);
139 | $srcX = ImageHelper::x($source);
140 | return abs($x - $srcX) < 5;
141 | }
142 |
143 | public function toArray(): array {
144 | $args = range(0, intval($this->configs['slices']) - 1);
145 | shuffle($args);
146 | list($bg, $points, $size) = $this->sortBy($args);
147 | return [
148 | 'image' => $bg->toBase64(),
149 | 'width' => $this->instance()->getWidth(),
150 | 'height' => $this->instance()->getHeight(),
151 | 'imageItems' => array_map(function ($item) use ($size) {
152 | return [
153 | 'x' => $item[0],
154 | 'y' => $item[1],
155 | 'width' => $size[0],
156 | 'height' => $size[1]
157 | ];
158 | }, $points),
159 | 'control' => $this->slideImage->toBase64(),
160 | 'controlWidth' => $this->slideImage->getWidth(),
161 | 'controlHeight' => $this->slideImage->getHeight(),
162 | 'controlY' => $this->point[1]
163 | ];
164 | }
165 |
166 | }
--------------------------------------------------------------------------------
/src/ThumbImage.php:
--------------------------------------------------------------------------------
1 | instance()->getWidth();
23 | $height = $this->instance()->getHeight();
24 | if ($thumbWidth <= 0) {
25 | $thumbWidth = $auto ? ($thumbHeight / $height * $width) : $width;
26 | } elseif ($thumbHeight <= 0) {
27 | $thumbHeight = $auto ? ($thumbWidth / $width * $height) : $height;
28 | } elseif($auto) {
29 | $rate = min($height / $thumbHeight, $width / $thumbWidth);
30 | $thumbWidth *= $rate;
31 | $thumbHeight *= $rate;
32 | }
33 | $thumb = $this->instance();
34 | $thumb->thumbnail(new Box($width, $height));
35 | $thumb->saveAs($output);
36 | return $output;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/WaterMark.php:
--------------------------------------------------------------------------------
1 | instance()->getWidth() / 3;
37 | $height = $this->instance()->getHeight() / 3;
38 | return array(
39 | $direction % 3 * $width,
40 | $direction / 3 * $height,
41 | $width,
42 | $height
43 | );
44 | }
45 | return match ($direction) {
46 | self::Center => array(
47 | ($this->instance()->getWidth() - $width) / 2,
48 | ($this->instance()->getHeight() - $height) / 2,
49 | $width,
50 | $height
51 | ),
52 | self::Left => array(
53 | $padding,
54 | ($this->instance()->getHeight() - $height) / 2,
55 | $width,
56 | $height
57 | ),
58 | self::Top => array(
59 | ($this->instance()->getWidth() - $width) / 2,
60 | $padding,
61 | $width,
62 | $height
63 | ),
64 | self::RightTop => array(
65 | $this->instance()->getWidth() - $width - $padding,
66 | $padding,
67 | $width,
68 | $height
69 | ),
70 | self::Right => array(
71 | $this->instance()->getWidth() - $width - $padding,
72 | ($this->instance()->getHeight() - $height) / 2,
73 | $width,
74 | $height
75 | ),
76 | self::RightBottom => array(
77 | $this->instance()->getWidth() - $width - $padding,
78 | $this->instance()->getHeight() - $height - $padding,
79 | $width,
80 | $height
81 | ),
82 | self::Bottom => array(
83 | ($this->instance()->getWidth() - $width) / 2,
84 | $this->instance()->getHeight() - $height - $padding,
85 | $width,
86 | $height
87 | ),
88 | self::LeftBottom => array(
89 | $padding,
90 | $this->instance()->getHeight() - $height - $padding,
91 | $width,
92 | $height
93 | ),
94 | default => array(
95 | $padding,
96 | $padding,
97 | $width,
98 | $height
99 | ),
100 | };
101 | }
102 |
103 | /**
104 | * 根据九宫格添加文字
105 | * @param string $text
106 | * @param int $direction
107 | * @param int $fontSize
108 | * @param string $color
109 | * @param string|int $fontFamily
110 | * @return static
111 | */
112 | public function addTextByDirection(string $text, int $direction = self::Top,
113 | int $fontSize = 16, string $color = '#000', string|int $fontFamily = 5) {
114 | $font = new Font($fontFamily, $fontSize, $color);
115 | $textBox = $this->instance()->fontSize($text, $font);
116 | list($x, $y) = $this->getPointByDirection($direction, $textBox->getWidth(), $textBox->getHeight());
117 | $this->instance()->text($text, $font, new Point($x, $y));
118 | return $this;
119 | }
120 |
121 | /**
122 | * 加文字
123 | * @param string $text
124 | * @param int $x
125 | * @param int $y
126 | * @param int $fontSize
127 | * @param string $color
128 | * @param int|string $fontFamily
129 | * @param int $angle 如果 $fontFamily 为 int,则不起作用
130 | * @return static
131 | */
132 | public function addText(string $text, int $x = 0, int $y = 0,
133 | int $fontSize = 16, string $color = '#000', string|int $fontFamily = 5,
134 | int $angle = 0) {
135 | $this->instance()->text($text, new Font($fontFamily, $fontSize, $color), new Point($x, $y), $angle);
136 | return $this;
137 | }
138 |
139 | /**
140 | * 根据九宫格添加图片
141 | * @param $image
142 | * @param int $direction
143 | * @param int $opacity
144 | * @return static
145 | */
146 | public function addImageByDirection($image, int $direction = self::Top, int $opacity = 50) {
147 | list($x, $y) = $this->getPointByDirection($direction);
148 | return $this->addImage($image, $x, $y, $opacity);
149 | }
150 |
151 | /**
152 | * 加水印图片
153 | * @param string|Image $image
154 | * @param int $x
155 | * @param int $y
156 | * @param int $opacity 透明度,对png图片不起作用
157 | * @return static
158 | */
159 | public function addImage($image, int $x = 0, int $y = 0, int $opacity = 50) {
160 | if ($image instanceof Image) {
161 | $image = $image->instance();
162 | } elseif (!$image instanceof ImageAdapter) {
163 | $image = ImageManager::create()->loadResource($image);
164 | }
165 | $this->instance()->paste($image, new Point($x, $y), $opacity);
166 | return $this;
167 | }
168 | }
--------------------------------------------------------------------------------