Passed
Push — master ( a2ba4c...af1bf0 )
by Vicens
03:10
created

Captcha::drawLines()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.6737
c 0
b 0
f 0
cc 6
eloc 11
nc 8
nop 3
1
<?php
2
/**
3
 * @description 验证码生成类
4
 * @author vicens <[email protected]>
5
 */
6
7
namespace Vicens\Captcha;
8
9
use Symfony\Component\HttpFoundation\Session\Session;
10
11
/**
12
 * Class Captcha
13
 * @method Captcha length(int $length)  设置验证码字符长度
14
 * @method Captcha charset(string $charset) 设置验证码的字符集
15
 * @method Captcha width(int $width) 设置验证码宽度
16
 * @method Captcha height(int $height) 设置验证码高度
17
 * @method Captcha textColor(string $color) 设置文本颜色
18
 * @method Captcha textFont(string $font) 设置文本字体
19
 * @method Captcha backgroundColor(string $color) 设置背景颜色
20
 * @method Captcha distortion(boolean $distortion) 是否开启失真模式
21
 * @method Captcha maxFrontLines(int $maxFrontLines) 设置最大前景线条数
22
 * @method Captcha minFrontLines(int $minFrontLines) 设置最小前景线条数
23
 * @method Captcha maxAngle(int $maxAngle) 文字最大倾斜角度
24
 * @method Captcha maxOffset(int $maxOffset) 文字最大偏移
25
 *
26
 * @package Vicens\Captcha
27
 */
28
class Captcha
29
{
30
31
    protected $config = [
32
        /**
33
         * 默认验证码长度
34
         * @var int
35
         */
36
        'length' => 4,
37
        /**
38
         * 验证码字符集
39
         * @var string
40
         */
41
        'charset' => 'abcdefghijklmnpqrstuvwxyz123456789',
42
        /**
43
         * 默认验证码宽度
44
         * @var int
45
         */
46
        'width' => 150,
47
        /**
48
         * 默认验证码高度
49
         * @var int
50
         */
51
        'height' => 40,
52
        /**
53
         * 指定文字颜色
54
         * @var string
55
         */
56
        'textColor' => null,
57
        /**
58
         * 文字字体文件
59
         * @var string
60
         */
61
        'textFont' => null,
62
        /**
63
         * 指定图片背景色
64
         * @var string
65
         */
66
        'backgroundColor' => null,
67
        /**
68
         * 开启失真模式
69
         * @var bool
70
         */
71
        'distortion' => true,
72
        /**
73
         * 最大前景线条数
74
         * @var int
75
         */
76
        'maxFrontLines' => null,
77
        /**
78
         * 最大背景线条数
79
         * @val int
80
         */
81
        'maxBehindLines' => null,
82
        /**
83
         * 文字最大角度
84
         * @var int
85
         */
86
        'maxAngle' => 8,
87
        /**
88
         * 文字最大偏移量
89
         * @var int
90
         */
91
        'maxOffset' => 5
92
    ];
93
94
    /**
95
     * 存储驱动
96
     * @var Session
97
     */
98
    protected $store;
99
100
101
    public function __construct(array $config = [], Session $store = null)
102
    {
103
104
        if ($store === null) {
105
            $store = new Session();
106
        }
107
108
        $this->store = $store;
109
110
        $this->setConfig($config);
111
    }
112
113
    /**
114
     * 设置验证码配置
115
     *
116
     * @param array|string $config 配置数组或配置项key
117
     * @param mixed $value 配置项值
118
     * @return $this
119
     */
120
    public function setConfig($config, $value = null)
121
    {
122
123
        if (!is_array($config)) {
124
            $config = [$config => $value];
125
        }
126
127
        foreach ($config as $key => $value) {
128
            if (array_key_exists($key, $this->config)) {
129
                $this->config[$key] = $value;
130
            }
131
        }
132
        return $this;
133
    }
134
135
    /**
136
     * 获取配置
137
     *
138
     * @param string|null $key 配置项key
139
     * @return string|number|array
140
     */
141
    public function getConfig($key = null)
142
    {
143
        if ($key !== null) {
144
            return $this->config[$key];
145
        }
146
147
        return $this->config;
148
    }
149
150
    /**
151
     * 生成验证码
152
     *
153
     * @return Image
154
     */
155
    public function make()
156
    {
157
158
        $code = $this->generate();
159
160
        $this->store($code);
161
162
        $image = $this->build($code);
163
164
        return new Image($image);
165
    }
166
167
    /**
168
     * 仅测试正确性, 不删除验证码
169
     *
170
     * @param string $input
171
     * @return bool
172
     */
173
    public function test($input)
174
    {
175
176
        if (!($this->has() && $input)) {
177
            return false;
178
        }
179
180
        //返回验证结果
181
        return password_verify(strtolower($input), $this->get());
182
    }
183
184
    /**
185
     * 检测正确性,并删除验证码
186
     *
187
     * @param string $input
188
     * @return bool
189
     */
190
    public function check($input)
191
    {
192
        $result = $this->test($input);
193
194
        $this->remove();
195
196
        return $result;
197
    }
198
199
    /**
200
     * 生成验证码
201
     *
202
     * @return string
203
     */
204
    protected function generate()
205
    {
206
        $characters = str_split($this->getConfig('charset'));
207
        $length = $this->getConfig('length');
208
209
        $code = '';
210
        for ($i = 0; $i < $length; $i++) {
211
            $code .= $characters[rand(0, count($characters) - 1)];
212
        }
213
214
        return $code;
215
    }
216
217
218
    /**
219
     * 加密字符串
220
     *
221
     * @param string $value
222
     * @return bool|string
223
     */
224
    protected function hash($value)
225
    {
226
        $hash = password_hash($value, PASSWORD_BCRYPT, array('cost' => 10));
227
228
        if ($hash === false) {
229
            throw new \RuntimeException('Bcrypt hashing not supported.');
230
        }
231
232
        return $hash;
233
    }
234
235
    /**
236
     * 返回存储到session中的键全名
237
     *
238
     * @return string
239
     */
240
    protected function getStoreName()
241
    {
242
        return 'captcha';
243
    }
244
245
    /**
246
     * 是否有存储的验证码
247
     *
248
     * @return bool
249
     */
250
    protected function has()
251
    {
252
        return $this->store->has($this->getStoreName());
253
    }
254
255
    /**
256
     * 存储验证码
257
     *
258
     * @param $code
259
     */
260
    protected function store($code)
261
    {
262
        $this->store->set($this->getStoreName(), $this->hash(strtolower($code)));
263
    }
264
265
    /**
266
     * 从存储中获取验证码
267
     *
268
     * @return mixed
269
     */
270
    protected function get()
271
    {
272
        return $this->store->get($this->getStoreName());
273
    }
274
275
    /**
276
     * 从存储中删除验证码
277
     *
278
     * @return mixed
279
     */
280
    protected function remove()
281
    {
282
        return $this->store->remove($this->getStoreName());
283
    }
284
285
    /**
286
     * 创建验证码图片
287
     *
288
     * @param string $code
289
     * @return resource
290
     */
291
    protected function build($code)
292
    {
293
294
        // 图片宽
295
        $width = $this->getConfig('width');
296
        // 图片高
297
        $height = $this->getConfig('height');
298
        // 背景颜色
299
        $backgroundColor = $this->getConfig('backgroundColor');
300
301
302
        //随机取一个字体
303
        $font = $this->getTextFont();
304
305
        //根据宽高创建一个背景画布
306
        $image = imagecreatetruecolor($width, $height);
307
308
        if ($backgroundColor === null) {
309
            $backgroundColor = imagecolorallocate($image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(200, 255));
310
        } else {
311
            $color = $backgroundColor;
312
            $backgroundColor = imagecolorallocate($image, $color[0], $color[1], $color[2]);
313
        }
314
        //填充背景色
315
        imagefill($image, 0, 0, $backgroundColor);
316
317
        //绘制背景干扰线
318
        $this->drawLines($image, $this->getConfig('maxBehindLines'));
319
320
        //写入验证码文字
321
        $color = $this->renderText($image, $code, $font);
322
323
        //绘制前景干扰线
324
        $this->drawLines($image, $this->getConfig('maxFrontLines'), $color);
325
326
327
        if ($this->getConfig('distortion')) {
328
            //创建失真
329
            $image = $this->distort($image, $width, $height, $backgroundColor);
330
        }
331
332
        //如果不指定字体颜色和背景颜色,则使用图像过滤器修饰
333
        if (function_exists('imagefilter') && is_null($backgroundColor) && is_null($this->getConfig('textColor'))) {
334
            //颜色翻转 - 1/2几率
335
            if (mt_rand(0, 1) == 0) {
336
                imagefilter($image, IMG_FILTER_NEGATE);
337
            }
338
            //用边缘检测来突出图像的边缘 - 1/11几率
339
            if (mt_rand(0, 10) == 0) {
340
                imagefilter($image, IMG_FILTER_EDGEDETECT);
341
            }
342
            //改变图像的对比度
343
            imagefilter($image, IMG_FILTER_CONTRAST, mt_rand(-50, 10));
344
345
            if (mt_rand(0, 5) == 0) {
346
                //用高斯算法和指定颜色模糊图像
347
                imagefilter($image, IMG_FILTER_COLORIZE, mt_rand(-80, 50), mt_rand(-80, 50), mt_rand(-80, 50));
348
            }
349
        }
350
        return $image;
351
    }
352
353
    /**
354
     * 获取一个字体
355
     *
356
     * @return string
357
     */
358
    protected function getTextFont()
359
    {
360
        //指定字体
361
        if ($this->getConfig('textFont') && file_exists($this->getConfig('textFont'))) {
362
            return $this->getConfig('textFont');
363
        }
364
        //随机字体
365
        return __DIR__ . '/../fonts/' . mt_rand(0, 5) . '.ttf';
366
    }
367
368
    /**
369
     * 写入验证码到图片中
370
     *
371
     * @param resource $image
372
     * @param string $phrase
373
     * @param string $font
374
     * @return int
375
     */
376
    protected function renderText($image, $phrase, $font)
377
    {
378
        $length = strlen($phrase);
379
        if ($length === 0) {
380
            return imagecolorallocate($image, 0, 0, 0);
381
        }
382
383
        // 计算文字尺寸
384
        $size = $this->getConfig('width') / $length - mt_rand(0, 3) - 1;
385
        $box = imagettfbbox($size, 0, $font, $phrase);
386
        $textWidth = $box[2] - $box[0];
387
        $textHeight = $box[1] - $box[7];
388
        $x = ($this->getConfig('width') - $textWidth) / 2;
389
        $y = ($this->getConfig('height') - $textHeight) / 2 + $size;
390
391
        if (!count($this->getConfig('textColor'))) {
392
            $textColor = array(mt_rand(0, 150), mt_rand(0, 150), mt_rand(0, 150));
393
        } else {
394
            $textColor = $this->getConfig('textColor');
395
        }
396
        $color = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]);
397
398
        // 循环写入字符,随机角度
399
        for ($i = 0; $i < $length; $i++) {
400
            $box = imagettfbbox($size, 0, $font, $phrase[$i]);
401
            $w = $box[2] - $box[0];
402
            $angle = mt_rand(-$this->getConfig('maxAngle'), $this->getConfig('maxAngle'));
403
            $offset = mt_rand(-$this->getConfig('maxOffset'), $this->getConfig('maxOffset'));
404
            imagettftext($image, $size, $angle, $x, $y + $offset, $color, $font, $phrase[$i]);
405
            $x += $w;
406
        }
407
408
        return $color;
409
    }
410
411
    /**
412
     * 画线
413
     *
414
     * @param resource $image
415
     * @param int $width
416
     * @param int $height
417
     * @param int|null $color
418
     */
419
    protected function renderLine($image, $width, $height, $color = null)
420
    {
421
        if ($color === null) {
422
            $color = imagecolorallocate($image, mt_rand(100, 255), mt_rand(100, 255), mt_rand(100, 255));
423
        }
424
425
        if (mt_rand(0, 1)) { // 横向
426
            $Xa = mt_rand(0, $width / 2);
427
            $Ya = mt_rand(0, $height);
428
            $Xb = mt_rand($width / 2, $width);
429
            $Yb = mt_rand(0, $height);
430
        } else { // 纵向
431
            $Xa = mt_rand(0, $width);
432
            $Ya = mt_rand(0, $height / 2);
433
            $Xb = mt_rand(0, $width);
434
            $Yb = mt_rand($height / 2, $height);
435
        }
436
        imagesetthickness($image, mt_rand(1, 3));
437
        imageline($image, $Xa, $Ya, $Xb, $Yb, $color);
438
    }
439
440
    /**
441
     * 画线
442
     *
443
     * @param resource $image
444
     * @param int $max
445
     * @param int|null $color
446
     */
447
    protected function drawLines($image, $max, $color = null)
448
    {
449
        $square = $this->getConfig('width') * $this->getConfig('height');
450
        $effects = mt_rand($square / 3000, $square / 2000);
451
452
        // 计算线条数
453
        if ($max != null && $max > 0) {
454
            $effects = min($max, $effects);
455
        }
456
457
        if ($max !== 0) {
458
            for ($e = 0; $e < $effects; $e++) {
459
460
                if ($color) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $color of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
461
                    $this->renderLine($image, $this->getConfig('width'), $this->getConfig('height'), $color);
462
                } else {
463
                    $this->renderLine($image, $this->getConfig('width'), $this->getConfig('height'));
464
                }
465
466
            }
467
        }
468
    }
469
470
    /**
471
     * 创建失真
472
     *
473
     * @param resource $image
474
     * @param int $width
475
     * @param int $height
476
     * @param int $bg
477
     * @return resource
478
     */
479
    protected function distort($image, $width, $height, $bg)
480
    {
481
        $contents = imagecreatetruecolor($width, $height);
482
        $X = mt_rand(0, $width);
483
        $Y = mt_rand(0, $height);
484
        $phase = mt_rand(0, 10);
485
        $scale = 1.1 + mt_rand(0, 10000) / 30000;
486
        for ($x = 0; $x < $width; $x++) {
487
            for ($y = 0; $y < $height; $y++) {
488
                $Vx = $x - $X;
489
                $Vy = $y - $Y;
490
                $Vn = sqrt($Vx * $Vx + $Vy * $Vy);
491
492
                if ($Vn != 0) {
493
                    $Vn2 = $Vn + 4 * sin($Vn / 30);
494
                    $nX = $X + ($Vx * $Vn2 / $Vn);
495
                    $nY = $Y + ($Vy * $Vn2 / $Vn);
496
                } else {
497
                    $nX = $X;
498
                    $nY = $Y;
499
                }
500
                $nY = $nY + $scale * sin($phase + $nX * 0.2);
501
502
                $p = $this->getColor($image, round($nX), round($nY), $bg);
503
504
                if ($p == 0) {
505
                    $p = $bg;
506
                }
507
508
                imagesetpixel($contents, $x, $y, $p);
509
            }
510
        }
511
512
        return $contents;
513
    }
514
515
    /**
516
     * 获取颜色
517
     *
518
     * @param resource $image
519
     * @param int $x
520
     * @param int $y
521
     * @param int $background
522
     * @return int
523
     */
524
    protected function getColor($image, $x, $y, $background)
525
    {
526
        $L = imagesx($image);
527
        $H = imagesy($image);
528
        if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) {
529
            return $background;
530
        }
531
532
        return imagecolorat($image, $x, $y);
533
    }
534
535
    /**
536
     * @param string $name
537
     * @param array $arguments
538
     * @return $this
539
     */
540
    public function __call($name, $arguments)
541
    {
542
        if (array_key_exists($name, $this->config)) {
543
            $this->config[$name] = $arguments[0];
544
        }
545
546
        return $this;
547
    }
548
}