1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @desc 图片类 |
4
|
|
|
* @author vicens<[email protected]> |
5
|
|
|
*/ |
6
|
|
|
|
7
|
|
|
namespace Vicens\Captcha; |
8
|
|
|
|
9
|
|
|
use Symfony\Component\HttpFoundation\Response; |
10
|
|
|
|
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Class Builder |
14
|
|
|
* @method width ($width) |
15
|
|
|
* @method height ($height) |
16
|
|
|
* @method textColor ($color) |
17
|
|
|
* @method backgroundColor ($color) |
18
|
|
|
* @method maxFrontLines ($maxFrontLines) |
19
|
|
|
* @method maxBehindLines ($maxBehindLines) |
20
|
|
|
* @method distortion ($distortion) |
21
|
|
|
* @method maxAngle ($maxAngle) |
22
|
|
|
* @method maxOffset ($maxOffset) |
23
|
|
|
* @method quality ($quality) |
24
|
|
|
* @package Vicens\Captcha |
25
|
|
|
*/ |
26
|
|
|
class Image |
27
|
|
|
{ |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* 已生成的图片 |
31
|
|
|
* @var resource |
32
|
|
|
*/ |
33
|
|
|
protected $image; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* 验证码 |
37
|
|
|
* @var string |
38
|
|
|
*/ |
39
|
|
|
protected $code; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* 验证码设置 |
43
|
|
|
* @var array |
44
|
|
|
*/ |
45
|
|
|
protected $config = array( |
46
|
|
|
/** |
47
|
|
|
* 默认验证码宽度 |
48
|
|
|
* @var int |
49
|
|
|
*/ |
50
|
|
|
'width' => 150, |
51
|
|
|
/** |
52
|
|
|
* 默认验证码高度 |
53
|
|
|
* @var int |
54
|
|
|
*/ |
55
|
|
|
'height' => 40, |
56
|
|
|
/** |
57
|
|
|
* 指定文字颜色 |
58
|
|
|
* @var string |
59
|
|
|
*/ |
60
|
|
|
'textColor' => null, |
61
|
|
|
/** |
62
|
|
|
* 文字字体文件 |
63
|
|
|
* @var string |
64
|
|
|
*/ |
65
|
|
|
'textFont' => null, |
66
|
|
|
/** |
67
|
|
|
* 指定图片背景色 |
68
|
|
|
* @var string |
69
|
|
|
*/ |
70
|
|
|
'backgroundColor' => null, |
71
|
|
|
/** |
72
|
|
|
* 开启失真模式 |
73
|
|
|
* @var bool |
74
|
|
|
*/ |
75
|
|
|
'distortion' => true, |
76
|
|
|
/** |
77
|
|
|
* 最大前景线条数 |
78
|
|
|
* @var int |
79
|
|
|
*/ |
80
|
|
|
'maxFrontLines' => null, |
81
|
|
|
/** |
82
|
|
|
* 最大背景线条数 |
83
|
|
|
* @val int |
84
|
|
|
*/ |
85
|
|
|
'maxBehindLines' => null, |
86
|
|
|
/** |
87
|
|
|
* 文字最大角度 |
88
|
|
|
* @var int |
89
|
|
|
*/ |
90
|
|
|
'maxAngle' => 8, |
91
|
|
|
/** |
92
|
|
|
* 文字最大偏移量 |
93
|
|
|
* @var int |
94
|
|
|
*/ |
95
|
|
|
'maxOffset' => 5, |
96
|
|
|
/** |
97
|
|
|
* 图片质量 |
98
|
|
|
* @var int |
99
|
|
|
*/ |
100
|
|
|
'quality' => 90 |
101
|
|
|
); |
102
|
|
|
|
103
|
|
|
|
104
|
|
|
public function __construct($code, array $config = array()) |
105
|
|
|
{ |
106
|
|
|
$this->code = $code; |
107
|
|
|
|
108
|
|
|
$this->setConfig($config); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* 设置验证码配置 |
113
|
|
|
* @param $config |
114
|
|
|
* @param null $value |
115
|
|
|
* @return $this |
116
|
|
|
*/ |
117
|
|
|
public function setConfig($config, $value = null) |
118
|
|
|
{ |
119
|
|
|
if (!is_array($config)) { |
120
|
|
|
$config = [$config => $value]; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
View Code Duplication |
foreach ($config as $key => $value) { |
|
|
|
|
124
|
|
|
if (array_key_exists($key, $this->config)) { |
125
|
|
|
$this->config[$key] = $value; |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
return $this; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* 获取配置 |
133
|
|
|
* @param $key |
134
|
|
|
* @return array|string|number |
135
|
|
|
*/ |
136
|
|
|
public function getConfig($key) |
137
|
|
|
{ |
138
|
|
|
if ($key) { |
139
|
|
|
return $this->config[$key]; |
140
|
|
|
} |
141
|
|
|
return $this->config; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* 获取图片 |
146
|
|
|
* @return resource |
147
|
|
|
*/ |
148
|
|
|
public function getImage() |
149
|
|
|
{ |
150
|
|
|
if (!$this->image) { |
151
|
|
|
|
152
|
|
|
$this->reset(); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
return $this->image; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* 重置图片 |
160
|
|
|
* @return $this |
161
|
|
|
*/ |
162
|
|
|
public function reset() |
163
|
|
|
{ |
164
|
|
|
|
165
|
|
|
$this->image = $this->build($this->code); |
166
|
|
|
|
167
|
|
|
return $this; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* 获取 base64的数据,用做image标签的src |
172
|
|
|
* @return string |
173
|
|
|
*/ |
174
|
|
|
public function getDataUrl() |
175
|
|
|
{ |
176
|
|
|
return 'data:image/jpeg;base64,' . base64_encode($this->getContent()); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* 输出为html |
181
|
|
|
* @param int $width |
182
|
|
|
* @param int $height |
183
|
|
|
* @return string |
184
|
|
|
*/ |
185
|
|
|
public function html($width = null, $height = null) |
186
|
|
|
{ |
187
|
|
|
$html = '<img src="' . $this->getDataUrl() . '"" alt="captcha"'; |
188
|
|
|
if (is_null($width)) { |
189
|
|
|
$html .= ' width="' . $width . '"'; |
190
|
|
|
} |
191
|
|
|
if (is_null($height)) { |
192
|
|
|
$html .= ' height="' . $height . '"'; |
193
|
|
|
} |
194
|
|
|
$html .= '>'; |
195
|
|
|
return $html; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* 返回Response响应 |
200
|
|
|
* @return Response |
201
|
|
|
*/ |
202
|
|
|
public function response() |
203
|
|
|
{ |
204
|
|
|
|
205
|
|
|
return Response::create($this->getContent(), 200, array('Content-type' => 'image/jpeg')); |
206
|
|
|
|
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* 获取图片资源 |
212
|
|
|
* @return mixed |
213
|
|
|
*/ |
214
|
|
|
public function getContext() |
215
|
|
|
{ |
216
|
|
|
return $this->getImage(); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* 保存为图片 |
221
|
|
|
* @param $filename |
222
|
|
|
* @return $this |
223
|
|
|
*/ |
224
|
|
|
public function save($filename) |
225
|
|
|
{ |
226
|
|
|
$this->output($filename); |
227
|
|
|
|
228
|
|
|
return $this; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* 直接输出 |
233
|
|
|
* @param null $filename |
234
|
|
|
* @return $this |
235
|
|
|
*/ |
236
|
|
|
public function output($filename = null) |
237
|
|
|
{ |
238
|
|
|
imagejpeg($this->getContext(), $filename, $this->getConfig('quality')); |
239
|
|
|
|
240
|
|
|
return $this; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* 获取输出内容 |
245
|
|
|
* @return string |
246
|
|
|
*/ |
247
|
|
|
public function getContent() |
248
|
|
|
{ |
249
|
|
|
ob_start(); |
250
|
|
|
$this->output(); |
251
|
|
|
return ob_get_clean(); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* 创建验证码图片 |
256
|
|
|
* @param $code |
257
|
|
|
* @return resource |
258
|
|
|
*/ |
259
|
|
|
protected function build($code) |
260
|
|
|
{ |
261
|
|
|
|
262
|
|
|
// 图片宽 |
263
|
|
|
$width = $this->getConfig('width'); |
264
|
|
|
// 图片高 |
265
|
|
|
$height = $this->getConfig('height'); |
266
|
|
|
// 背景颜色 |
267
|
|
|
$backgroundColor = $this->getConfig('backgroundColor'); |
268
|
|
|
|
269
|
|
|
|
270
|
|
|
//随机取一个字体 |
271
|
|
|
$font = $this->getTextFont(); |
272
|
|
|
|
273
|
|
|
//根据宽高创建一个背景画布 |
274
|
|
|
$image = imagecreatetruecolor($width, $height); |
275
|
|
|
|
276
|
|
|
if ($backgroundColor == null) { |
277
|
|
|
$backgroundColor = imagecolorallocate($image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(200, 255)); |
278
|
|
|
} else { |
279
|
|
|
$color = $backgroundColor; |
280
|
|
|
$backgroundColor = imagecolorallocate($image, $color[0], $color[1], $color[2]); |
281
|
|
|
} |
282
|
|
|
//填充背景色 |
283
|
|
|
imagefill($image, 0, 0, $backgroundColor); |
284
|
|
|
|
285
|
|
|
//绘制背景干扰线 |
286
|
|
|
$this->drawLines($image, $this->getConfig('maxBehindLines')); |
287
|
|
|
|
288
|
|
|
//写入验证码文字 |
289
|
|
|
$color = $this->renderText($image, $code, $font); |
290
|
|
|
|
291
|
|
|
//绘制前景干扰线 |
292
|
|
|
$this->drawLines($image, $this->getConfig('maxFrontLines'), $color); |
293
|
|
|
|
294
|
|
|
|
295
|
|
|
if ($this->getConfig('distortion')) { |
296
|
|
|
//创建失真 |
297
|
|
|
$image = $this->distort($image, $width, $height, $backgroundColor); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
//如果不指定字体颜色和背景颜色,则使用图像过滤器修饰 |
301
|
|
|
if (function_exists('imagefilter') && is_null($backgroundColor) && is_null($this->getConfig('textColor'))) { |
302
|
|
|
//颜色翻转 - 1/2几率 |
303
|
|
|
if (mt_rand(0, 1) == 0) { |
304
|
|
|
imagefilter($image, IMG_FILTER_NEGATE); |
305
|
|
|
} |
306
|
|
|
//用边缘检测来突出图像的边缘 - 1/11几率 |
307
|
|
|
if (mt_rand(0, 10) == 0) { |
308
|
|
|
imagefilter($image, IMG_FILTER_EDGEDETECT); |
309
|
|
|
} |
310
|
|
|
//改变图像的对比度 |
311
|
|
|
imagefilter($image, IMG_FILTER_CONTRAST, mt_rand(-50, 10)); |
312
|
|
|
|
313
|
|
|
if (mt_rand(0, 5) == 0) { |
314
|
|
|
//用高斯算法和指定颜色模糊图像 |
315
|
|
|
imagefilter($image, IMG_FILTER_COLORIZE, mt_rand(-80, 50), mt_rand(-80, 50), mt_rand(-80, 50)); |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
return $image; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* 获取一个字体 |
323
|
|
|
* @return string |
324
|
|
|
*/ |
325
|
|
|
protected function getTextFont() |
326
|
|
|
{ |
327
|
|
|
//指定字体 |
328
|
|
|
if ($this->config['textFont'] && file_exists($this->config['textFont'])) { |
329
|
|
|
return $this->config['textFont']; |
330
|
|
|
} |
331
|
|
|
//随机字体 |
332
|
|
|
return __DIR__ . '/../fonts/' . mt_rand(0, 5) . '.ttf'; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* 写入验证码到图片中 |
337
|
|
|
* @param $image |
338
|
|
|
* @param $phrase |
339
|
|
|
* @param $font |
340
|
|
|
* @return int |
341
|
|
|
*/ |
342
|
|
|
protected function renderText($image, $phrase, $font) |
343
|
|
|
{ |
344
|
|
|
$length = strlen($phrase); |
345
|
|
|
if ($length === 0) { |
346
|
|
|
return imagecolorallocate($image, 0, 0, 0); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
// 计算文字尺寸 |
350
|
|
|
$size = $this->config['width'] / $length - mt_rand(0, 3) - 1; |
351
|
|
|
$box = imagettfbbox($size, 0, $font, $phrase); |
352
|
|
|
$textWidth = $box[2] - $box[0]; |
353
|
|
|
$textHeight = $box[1] - $box[7]; |
354
|
|
|
$x = ($this->config['width'] - $textWidth) / 2; |
355
|
|
|
$y = ($this->config['height'] - $textHeight) / 2 + $size; |
356
|
|
|
|
357
|
|
|
if (!count($this->config['textColor'])) { |
358
|
|
|
$textColor = array(mt_rand(0, 150), mt_rand(0, 150), mt_rand(0, 150)); |
359
|
|
|
} else { |
360
|
|
|
$textColor = $this->config['textColor']; |
361
|
|
|
} |
362
|
|
|
$color = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]); |
363
|
|
|
|
364
|
|
|
// 循环写入字符,随机角度 |
365
|
|
|
for ($i = 0; $i < $length; $i++) { |
366
|
|
|
$box = imagettfbbox($size, 0, $font, $phrase[$i]); |
367
|
|
|
$w = $box[2] - $box[0]; |
368
|
|
|
$angle = mt_rand(-$this->config['maxAngle'], $this->config['maxAngle']); |
369
|
|
|
$offset = mt_rand(-$this->config['maxOffset'], $this->config['maxOffset']); |
370
|
|
|
imagettftext($image, $size, $angle, $x, $y + $offset, $color, $font, $phrase[$i]); |
371
|
|
|
$x += $w; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
return $color; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* 画线 |
379
|
|
|
* @param $image |
380
|
|
|
* @param $width |
381
|
|
|
* @param $height |
382
|
|
|
* @param null $color |
383
|
|
|
*/ |
384
|
|
|
protected function renderLine($image, $width, $height, $color = null) |
385
|
|
|
{ |
386
|
|
|
if ($color === null) { |
387
|
|
|
$color = imagecolorallocate($image, mt_rand(100, 255), mt_rand(100, 255), mt_rand(100, 255)); |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
if (mt_rand(0, 1)) { // 横向 |
391
|
|
|
$Xa = mt_rand(0, $width / 2); |
392
|
|
|
$Ya = mt_rand(0, $height); |
393
|
|
|
$Xb = mt_rand($width / 2, $width); |
394
|
|
|
$Yb = mt_rand(0, $height); |
395
|
|
|
} else { // 纵向 |
396
|
|
|
$Xa = mt_rand(0, $width); |
397
|
|
|
$Ya = mt_rand(0, $height / 2); |
398
|
|
|
$Xb = mt_rand(0, $width); |
399
|
|
|
$Yb = mt_rand($height / 2, $height); |
400
|
|
|
} |
401
|
|
|
imagesetthickness($image, mt_rand(1, 3)); |
402
|
|
|
imageline($image, $Xa, $Ya, $Xb, $Yb, $color); |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* 画线 |
407
|
|
|
* @param $image |
408
|
|
|
* @param $max |
409
|
|
|
* @param $color |
410
|
|
|
*/ |
411
|
|
|
protected function drawLines($image, $max, $color = null) |
412
|
|
|
{ |
413
|
|
|
$square = $this->config['width'] * $this->config['height']; |
414
|
|
|
$effects = mt_rand($square / 3000, $square / 2000); |
415
|
|
|
|
416
|
|
|
// 计算线条数 |
417
|
|
|
if ($max != null && $max > 0) { |
418
|
|
|
$effects = min($max, $effects); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
if ($max !== 0) { |
422
|
|
|
for ($e = 0; $e < $effects; $e++) { |
423
|
|
|
|
424
|
|
|
if ($color) { |
425
|
|
|
$this->renderLine($image, $this->config['width'], $this->config['height'], $color); |
426
|
|
|
} else { |
427
|
|
|
$this->renderLine($image, $this->config['width'], $this->config['height']); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
} |
431
|
|
|
} |
432
|
|
|
} |
433
|
|
|
|
434
|
|
|
/** |
435
|
|
|
* 创建失真 |
436
|
|
|
* @param $image |
437
|
|
|
* @param $width |
438
|
|
|
* @param $height |
439
|
|
|
* @param $bg |
440
|
|
|
* @return resource |
441
|
|
|
*/ |
442
|
|
|
protected function distort($image, $width, $height, $bg) |
443
|
|
|
{ |
444
|
|
|
$contents = imagecreatetruecolor($width, $height); |
445
|
|
|
$X = mt_rand(0, $width); |
446
|
|
|
$Y = mt_rand(0, $height); |
447
|
|
|
$phase = mt_rand(0, 10); |
448
|
|
|
$scale = 1.1 + mt_rand(0, 10000) / 30000; |
449
|
|
|
for ($x = 0; $x < $width; $x++) { |
450
|
|
|
for ($y = 0; $y < $height; $y++) { |
451
|
|
|
$Vx = $x - $X; |
452
|
|
|
$Vy = $y - $Y; |
453
|
|
|
$Vn = sqrt($Vx * $Vx + $Vy * $Vy); |
454
|
|
|
|
455
|
|
|
if ($Vn != 0) { |
456
|
|
|
$Vn2 = $Vn + 4 * sin($Vn / 30); |
457
|
|
|
$nX = $X + ($Vx * $Vn2 / $Vn); |
458
|
|
|
$nY = $Y + ($Vy * $Vn2 / $Vn); |
459
|
|
|
} else { |
460
|
|
|
$nX = $X; |
461
|
|
|
$nY = $Y; |
462
|
|
|
} |
463
|
|
|
$nY = $nY + $scale * sin($phase + $nX * 0.2); |
464
|
|
|
|
465
|
|
|
$p = $this->getColor($image, round($nX), round($nY), $bg); |
466
|
|
|
|
467
|
|
|
if ($p == 0) { |
468
|
|
|
$p = $bg; |
469
|
|
|
} |
470
|
|
|
|
471
|
|
|
imagesetpixel($contents, $x, $y, $p); |
472
|
|
|
} |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
return $contents; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
/** |
479
|
|
|
* 获取颜色 |
480
|
|
|
* @param $image |
481
|
|
|
* @param $x |
482
|
|
|
* @param $y |
483
|
|
|
* @param $background |
484
|
|
|
* @return int |
485
|
|
|
*/ |
486
|
|
|
protected function getColor($image, $x, $y, $background) |
487
|
|
|
{ |
488
|
|
|
$L = imagesx($image); |
489
|
|
|
$H = imagesy($image); |
490
|
|
|
if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) { |
491
|
|
|
return $background; |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
return imagecolorat($image, $x, $y); |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
/** |
498
|
|
|
* @param $name |
499
|
|
|
* @param $arguments |
500
|
|
|
* @return $this |
501
|
|
|
*/ |
502
|
|
|
public function __call($name, $arguments) |
503
|
|
|
{ |
504
|
|
|
if (array_key_exists($name, $this->config)) { |
505
|
|
|
$this->config[$name] = $arguments[0]; |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
return $this; |
509
|
|
|
} |
510
|
|
|
|
511
|
|
|
} |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.