Completed
Push — master ( dca322...47e653 )
by Julito
08:54
created

Image_Text::init()   F

Complexity

Conditions 27
Paths 1388

Size

Total Lines 121
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
eloc 77
nc 1388
nop 0
dl 0
loc 121
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Image_Text.
4
 *
5
 * This is the main file of the Image_Text package. This file has to be included for
6
 * usage of Image_Text.
7
 *
8
 * This is a simple example script, showing Image_Text's facilities.
9
 *
10
 * PHP version 5
11
 *
12
 * @category  Image
13
 * @package   Image_Text
14
 * @author    Tobias Schlitt <[email protected]>
15
 * @copyright 1997-2005 The PHP Group
16
 * @license   http://www.php.net/license/3_01.txt PHP License
17
 * @version   CVS: $Id$
18
 * @link      http://pear.php.net/package/Image_Text
19
 * @since     File available since Release 0.0.1
20
 */
21
/**
22
 * Image_Text - Advanced text manipulations in images.
23
 *
24
 * Image_Text provides advanced text manipulation facilities for GD2 image generation
25
 * with PHP. Simply add text clippings to your images, let the class automatically
26
 * determine lines, rotate text boxes around their center or top left corner. These
27
 * are only a couple of features Image_Text provides.
28
 *
29
 * @category  Image
30
 * @package   Image_Text
31
 * @author    Tobias Schlitt <[email protected]>
32
 * @copyright 1997-2005 The PHP Group
33
 * @license   http://www.php.net/license/3_01.txt PHP License
34
 * @version   Release: @package_version@
35
 * @link      http://pear.php.net/package/Image_Text
36
 * @since     0.0.1
37
 */
38
class Image_Text
39
{
40
    /**
41
     * Regex to match HTML style hex triples.
42
     */
43
    const IMAGE_TEXT_REGEX_HTMLCOLOR
44
        = "/^[#|]([a-f0-9]{2})?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i";
45
46
    /**
47
     * Defines horizontal alignment to the left of the text box. (This is standard.)
48
     */
49
    const IMAGE_TEXT_ALIGN_LEFT = "left";
50
51
    /**
52
     * Defines horizontal alignment to the center of the text box.
53
     */
54
    const IMAGE_TEXT_ALIGN_RIGHT = "right";
55
56
    /**
57
     * Defines horizontal alignment to the center of the text box.
58
     */
59
    const IMAGE_TEXT_ALIGN_CENTER = "center";
60
61
    /**
62
     * Defines vertical alignment to the to the top of the text box. (This is
63
     * standard.)
64
     */
65
    const IMAGE_TEXT_ALIGN_TOP = "top";
66
67
    /**
68
     * Defines vertical alignment to the to the middle of the text box.
69
     */
70
    const IMAGE_TEXT_ALIGN_MIDDLE = "middle";
71
72
    /**
73
     * Defines vertical alignment to the to the bottom of the text box.
74
     */
75
    const IMAGE_TEXT_ALIGN_BOTTOM = "bottom";
76
77
    /**
78
     * TODO: This constant is useless until now, since justified alignment does not
79
     * work yet
80
     */
81
    const IMAGE_TEXT_ALIGN_JUSTIFY = "justify";
82
83
    /**
84
     * Options array. these options can be set through the constructor or the set()
85
     * method.
86
     *
87
     * Possible options to set are:
88
     * <pre>
89
     *   'x'                | This sets the top left coordinates (using x/y) or the
90
     *   'y'                | center point coordinates (using cx/cy) for your text
91
     *   'cx'               | box. The values from cx/cy will overwrite x/y.
92
     *   'cy'               |
93
     *
94
     *   'canvas'           | You can set different values as a canvas:
95
     *                      |   - A gd image resource.
96
     *                      |   - An array with 'width' and 'height'.
97
     *                      |   - Nothing (the canvas will be measured after the real
98
     *                      |     text size).
99
     *
100
     *   'antialias'        | This is usually true. Set it to false to switch
101
     *                      | antialiasing off.
102
     *
103
     *   'width'            | The width and height for your text box.
104
     *   'height'           |
105
     *
106
     *   'halign'           | Alignment of your text inside the textbox. Use
107
     *   'valign'           | alignment constants to define vertical and horizontal
108
     *                      | alignment.
109
     *
110
     *   'angle'            | The angle to rotate your text box.
111
     *
112
     *   'color'            | An array of color values. Colors will be rotated in the
113
     *                      | mode you choose (linewise or paragraphwise). Can be in
114
     *                      | the following formats:
115
     *                      |   - String representing HTML style hex couples
116
     *                      |     (+ unusual alpha couple in the first place,
117
     *                      |      optional).
118
     *                      |   - Array of int values using 'r', 'g', 'b' and
119
     *                      |     optionally 'a' as keys.
120
     *
121
     *   'color_mode'       | The color rotation mode for your color sets. Does only
122
     *                      | apply if you defined multiple colors. Use 'line' or
123
     *                      | 'paragraph'.
124
     *
125
     *   'background_color' | defines the background color. NULL sets it transparent
126
     *   'enable_alpha'     | if alpha channel should be enabled. Automatically
127
     *                      | enabled when background_color is set to NULL
128
     *
129
     *   'font_path'        | Location of the font to use. The path only gives the
130
     *                      | directory path (ending with a /).
131
     *   'font_file'        | The fontfile is given in the 'font_file' option.
132
     *
133
     *   'font_size'        | The font size to render text in (will be overwriten, if
134
     *                      | you use automeasurize).
135
     *
136
     *   'line_spacing'     | Measure for the line spacing to use. Default is 0.5.
137
     *
138
     *   'min_font_size'    | Automeasurize settings. Try to keep this area as small
139
     *   'max_font_size'    | as possible to get better performance.
140
     *
141
     *   'image_type'       | The type of image (use image type constants). Is
142
     *                      | default set to PNG.
143
     *
144
     *   'dest_file'        | The destination to (optionally) save your file.
145
     * </pre>
146
     *
147
     * @var array
148
     * @see Image_Text::set()
149
     */
150
151
    private $_options = array(
152
        // orientation
153
        'x' => 0,
154
        'y' => 0,
155
156
        // surface
157
        'canvas' => null,
158
        'antialias' => true,
159
160
        // text clipping
161
        'width' => 0,
162
        'height' => 0,
163
164
        // text alignment inside the clipping
165
        'halign' => self::IMAGE_TEXT_ALIGN_LEFT,
166
        'valign' => self::IMAGE_TEXT_ALIGN_TOP,
167
168
        // angle to rotate the text clipping
169
        'angle' => 0,
170
171
        // color settings
172
        'color' => array('#000000'),
173
174
        'color_mode' => 'line',
175
176
        'background_color' => '#000000',
177
        'enable_alpha' => false,
178
179
        // font settings
180
        'font_path' => "./",
181
        'font_file' => null,
182
        'font_size' => 2,
183
        'line_spacing' => 0.5,
184
185
        // automasurizing settings
186
        'min_font_size' => 1,
187
        'max_font_size' => 100,
188
189
        //max. lines to render
190
        'max_lines' => 100,
191
192
        // misc settings
193
        'image_type' => IMAGETYPE_PNG,
194
        'dest_file' => ''
195
    );
196
197
    /**
198
     * Contains option names, which can cause re-initialization force.
199
     *
200
     * @var array
201
     */
202
    private $_reInits = array(
203
        'width', 'height', 'canvas', 'angle', 'font_file', 'font_path', 'font_size'
204
    );
205
206
    /**
207
     * The text you want to render.
208
     *
209
     * @var string
210
     */
211
    private $_text;
212
213
    /**
214
     * Resource ID of the image canvas.
215
     *
216
     * @var resource
217
     */
218
    private $_img;
219
220
    /**
221
     * Tokens (each word).
222
     *
223
     * @var array
224
     */
225
    private $_tokens = array();
226
227
    /**
228
     * Fullpath to the font.
229
     *
230
     * @var string
231
     */
232
    private $_font;
233
234
    /**
235
     * Contains the bbox of each rendered lines.
236
     *
237
     * @var array
238
     */
239
    private $_bbox = array();
240
241
    /**
242
     * Defines in which mode the canvas has be set.
243
     *
244
     * @var array
245
     */
246
    private $_mode = '';
247
248
    /**
249
     * Color indices returned by imagecolorallocatealpha.
250
     *
251
     * @var array
252
     */
253
    private $_colors = array();
254
255
    /**
256
     * Width and height of the (rendered) text.
257
     *
258
     * @var array
259
     */
260
    private $_realTextSize = array('width' => false, 'height' => false);
261
262
    /**
263
     * Measurized lines.
264
     *
265
     * @var array
266
     */
267
    private $_lines = false;
268
269
    /**
270
     * Fontsize for which the last measuring process was done.
271
     *
272
     * @var array
273
     */
274
    private $_measurizedSize = false;
275
276
    /**
277
     * Is the text object initialized?
278
     *
279
     * @var bool
280
     */
281
    private $_init = false;
282
283
    /**
284
     * Constructor
285
     *
286
     * Set the text and options. This initializes a new Image_Text object. You must
287
     * set your text here. Optionally you can set all options here using the $options
288
     * parameter. If you finished switching all options you have to call the init()
289
     * method first before doing anything further! See Image_Text::set() for further
290
     * information.
291
     *
292
     * @param string $text    Text to print.
293
     * @param array  $options Options.
294
     *
295
     * @see Image_Text::set(), Image_Text::construct(), Image_Text::init()
296
     */
297
    public function __construct($text, $options = null)
298
    {
299
        $this->set('text', $text);
300
        if (!empty($options)) {
301
            $this->_options = array_merge($this->_options, $options);
302
        }
303
    }
304
305
    /**
306
     * Construct and initialize an Image_Text in one step.
307
     * This method is called statically and creates plus initializes an Image_Text
308
     * object. Beware: You will have to recall init() if you set an option afterwards
309
     * manually.
310
     *
311
     * @param string $text    Text to print.
312
     * @param array  $options Options.
313
     *
314
     * @return Image_Text
315
     * @see Image_Text::set(), Image_Text::Image_Text(), Image_Text::init()
316
     */
317
    public static function construct($text, $options)
318
    {
319
        $itext = new Image_Text($text, $options);
320
        $itext->init();
321
        return $itext;
322
    }
323
324
    /**
325
     * Set options
326
     *
327
     * Set a single or multiple options. It may happen that you have to reinitialize
328
     * the Image_Text object after changing options. For possible options, please
329
     * take a look at the class options array!
330
     *
331
     * @param array|string $option A single option name or the options array.
332
     * @param mixed        $value  Option value if $option is string.
333
     *
334
     * @return void
335
     * @see    Image_Text::Image_Text()
336
     * @throws Image_Text_Exception setting the value failed
337
     */
338
    public function set($option, $value = null)
339
    {
340
        $reInits = array_flip($this->_reInits);
341
        if (!is_array($option)) {
342
            if (!isset($value)) {
343
                throw new Image_Text_Exception('No value given.');
344
            }
345
            $option = array($option => $value);
346
        }
347
        foreach ($option as $opt => $val) {
348
            switch ($opt) {
349
            case 'color':
350
                $this->setColors($val);
351
                break;
352
            case 'text':
353
                if (is_array($val)) {
354
                    $this->_text = implode('\n', $val);
355
                } else {
356
                    $this->_text = $val;
357
                }
358
                break;
359
            default:
360
                $this->_options[$opt] = $val;
361
                break;
362
            }
363
            if (isset($reInits[$opt])) {
364
                $this->_init = false;
365
            }
366
        }
367
    }
368
369
    /**
370
     * Set the color-set
371
     *
372
     * Using this method you can set multiple colors for your text. Use a simple
373
     * numeric array to determine their order and give it to this function. Multiple
374
     * colors will be cycled by the options specified 'color_mode' option. The given
375
     * array will overwrite the existing color settings!
376
     *
377
     * The following colors syntaxes are understood by this method:
378
     * <ul>
379
     * <li>"#ffff00" hexadecimal format (HTML style), with and without #.</li>
380
     * <li>"#08ffff00" hexadecimal format (HTML style) with alpha channel (08),
381
     * with and without #.</li>
382
     * <li>array with 'r','g','b' and (optionally) 'a' keys, using int values.</li>
383
     * </ul>
384
     * A single color or an array of colors are allowed here.
385
     *
386
     * @param array|string $colors Single color or array of colors.
387
     *
388
     * @return void
389
     * @see Image_Text::setColor(), Image_Text::set()
390
     * @throws Image_Text_Exception
391
     */
392
    public function setColors($colors)
393
    {
394
        $i = 0;
395
        if (is_array($colors) && (is_string($colors[0]) || is_array($colors[0]))) {
396
            foreach ($colors as $color) {
397
                $this->setColor($color, $i++);
398
            }
399
        } else {
400
            $this->setColor($colors, $i);
401
        }
402
    }
403
404
    /**
405
     * Set a color
406
     *
407
     * This method is used to set a color at a specific color ID inside the color
408
     * cycle.
409
     *
410
     * The following colors syntaxes are understood by this method:
411
     * <ul>
412
     * <li>"#ffff00" hexadecimal format (HTML style), with and without #.</li>
413
     * <li>"#08ffff00" hexadecimal format (HTML style) with alpha channel (08), with
414
     * and without #.</li>
415
     * <li>array with 'r','g','b' and (optionally) 'a' keys, using int values.</li>
416
     * </ul>
417
     *
418
     * @param array|string $color Color value.
419
     * @param int          $id    ID (in the color array) to set color to.
420
     *
421
     * @return void
422
     * @see Image_Text::setColors(), Image_Text::set()
423
     * @throws Image_Text_Exception
424
     */
425
426
    function setColor($color, $id = 0)
427
    {
428
        if (is_array($color)) {
429
            if (isset($color['r']) && isset($color['g']) && isset($color['b'])) {
430
                $color['a'] = isset($color['a']) ? $color['a'] : 0;
431
                $this->_options['colors'][$id] = $color;
432
            } else if (isset($color[0]) && isset($color[1]) && isset($color[2])) {
433
                $color['r'] = $color[0];
434
                $color['g'] = $color[1];
435
                $color['b'] = $color[2];
436
                $color['a'] = isset($color[3]) ? $color[3] : 0;
437
                $this->_options['colors'][$id] = $color;
438
            } else {
439
                throw new Image_Text_Exception(
440
                    'Use keys 1,2,3 (optionally) 4 or r,g,b and (optionally) a.'
441
                );
442
            }
443
        } elseif (is_string($color)) {
444
            $color = $this->convertString2RGB($color);
445
            if ($color) {
446
                $this->_options['color'][$id] = $color;
447
            } else {
448
                throw new Image_Text_Exception('Invalid color.');
449
            }
450
        }
451
        if ($this->_img) {
452
            $aaFactor = ($this->_options['antialias']) ? 1 : -1;
453
            if (function_exists('imagecolorallocatealpha') && isset($color['a'])) {
454
                $this->_colors[$id] = $aaFactor *
455
                    imagecolorallocatealpha(
456
                        $this->_img,
457
                        $color['r'], $color['g'], $color['b'], $color['a']
458
                    );
459
            } else {
460
                $this->_colors[$id] = $aaFactor *
461
                    imagecolorallocate(
462
                        $this->_img,
463
                        $color['r'], $color['g'], $color['b']
464
                    );
465
            }
466
            if ($this->_colors[$id] == 0 && $aaFactor == -1) {
467
                // correction for black with antialiasing OFF
468
                // since color cannot be negative zero
469
                $this->_colors[$id] = -256;
470
            }
471
        }
472
    }
473
474
    /**
475
     * Initialize the Image_Text object.
476
     *
477
     * This method has to be called after setting the options for your Image_Text
478
     * object. It initializes the canvas, normalizes some data and checks important
479
     * options. Be sure to check the initialization after you switched some options.
480
     * The set() method may force you to reinitialize the object.
481
     *
482
     * @return void
483
     * @see Image_Text::set()
484
     * @throws Image_Text_Exception
485
     */
486
    public function init()
487
    {
488
        // Does the fontfile exist and is readable?
489
        $fontFile = rtrim($this->_options['font_path'], '/\\');
490
        $fontFile .= defined('OS_WINDOWS') && OS_WINDOWS ? '\\' : '/';
491
        $fontFile .= $this->_options['font_file'];
492
        $fontFile = realpath($fontFile);
493
494
        if (empty($fontFile)) {
495
            throw new Image_Text_Exception('You must supply a font file.');
496
        } elseif (!file_exists($fontFile)) {
497
            throw new Image_Text_Exception('Font file was not found.');
498
        } elseif (!is_readable($fontFile)) {
499
            throw new Image_Text_Exception('Font file is not readable.');
500
        } elseif (!imagettfbbox(1, 1, $fontFile, 1)) {
501
            throw new Image_Text_Exception('Font file is not valid.');
502
        }
503
        $this->_font = $fontFile;
504
505
        // Is the font size to small?
506
        if ($this->_options['width'] < 1) {
507
            throw new Image_Text_Exception('Width too small. Has to be > 1.');
508
        }
509
510
        // Check and create canvas
511
        $image_canvas = false;
512
        switch (true) {
513
514
        case (empty($this->_options['canvas'])):
515
            // Create new image from width && height of the clipping
516
            $this->_img = imagecreatetruecolor(
517
                $this->_options['width'], $this->_options['height']
518
            );
519
            if (!$this->_img) {
520
                throw new Image_Text_Exception('Could not create image canvas.');
521
            }
522
            break;
523
524
        case (is_resource($this->_options['canvas']) &&
525
            get_resource_type($this->_options['canvas']) == 'gd'):
526
            // The canvas is an image resource
527
            $image_canvas = true;
528
            $this->_img = $this->_options['canvas'];
529
            break;
530
531
        case (is_array($this->_options['canvas']) &&
532
            isset($this->_options['canvas']['width']) &&
533
            isset($this->_options['canvas']['height'])):
534
535
            // Canvas must be a width and height measure
536
            $this->_img = imagecreatetruecolor(
537
                $this->_options['canvas']['width'],
538
                $this->_options['canvas']['height']
539
            );
540
            break;
541
542
        case (is_array($this->_options['canvas']) &&
543
            isset($this->_options['canvas']['size']) &&
544
            ($this->_options['canvas']['size'] = 'auto')):
545
546
        case (is_string($this->_options['canvas']) &&
547
            ($this->_options['canvas'] = 'auto')):
548
            $this->_mode = 'auto';
549
            break;
550
551
        default:
552
            throw new Image_Text_Exception('Could not create image canvas.');
553
        }
554
555
        if ($this->_img) {
556
            $this->_options['canvas'] = array();
557
            $this->_options['canvas']['width'] = imagesx($this->_img);
558
            $this->_options['canvas']['height'] = imagesy($this->_img);
559
        }
560
561
        if ($this->_options['enable_alpha']) {
562
            imagesavealpha($this->_img, true);
563
            imagealphablending($this->_img, false);
564
        }
565
566
        if ($this->_options['background_color'] === null) {
567
            $this->_options['enable_alpha'] = true;
568
            imagesavealpha($this->_img, true);
569
            imagealphablending($this->_img, false);
570
            $colBg = imagecolorallocatealpha($this->_img, 255, 255, 255, 127);
571
        } else {
572
            $arBg = $this->convertString2RGB($this->_options['background_color']);
573
            if ($arBg === false) {
574
                throw new Image_Text_Exception('Background color is invalid.');
575
            }
576
            $colBg = imagecolorallocatealpha(
577
                $this->_img, $arBg['r'], $arBg['g'], $arBg['b'], $arBg['a']
578
            );
579
        }
580
        if ($image_canvas === false) {
581
            imagefilledrectangle(
582
                $this->_img,
583
                0, 0,
584
                $this->_options['canvas']['width'] - 1,
585
                $this->_options['canvas']['height'] - 1,
586
                $colBg
587
            );
588
        }
589
590
        // Save and repair angle
591
        $angle = $this->_options['angle'];
592
        while ($angle < 0) {
593
            $angle += 360;
594
        }
595
        if ($angle > 359) {
596
            $angle = $angle % 360;
597
        }
598
        $this->_options['angle'] = $angle;
599
600
        // Set the color values
601
        $this->setColors($this->_options['color']);
602
603
        $this->_lines = null;
604
605
        // Initialization is complete
606
        $this->_init = true;
607
    }
608
609
    /**
610
     * Auto measurize text
611
     *
612
     * Automatically determines the greatest possible font size to fit the text into
613
     * the text box. This method may be very resource intensive on your webserver. A
614
     * good tweaking point are the $start and $end parameters, which specify the
615
     * range of font sizes to search through. Anyway, the results should be cached if
616
     * possible. You can optionally set $start and $end here as a parameter or the
617
     * settings of the options array are used.
618
     *
619
     * @param int|bool $start Fontsize to start testing with.
620
     * @param int|bool $end   Fontsize to end testing with.
621
     *
622
     * @return int Fontsize measured
623
     * @see  Image_Text::measurize()
624
     * @throws Image_Text_Exception
625
     * @todo Beware of initialize
626
     */
627
    public function autoMeasurize($start = false, $end = false)
628
    {
629
        if (!$this->_init) {
630
            throw new Image_Text_Exception('Not initialized. Call ->init() first!');
631
        }
632
633
        $start = (empty($start)) ? $this->_options['min_font_size'] : $start;
634
        $end = (empty($end)) ? $this->_options['max_font_size'] : $end;
635
636
        // Run through all possible font sizes until a measurize fails
637
        // Not the optimal way. This can be tweaked!
638
        for ($i = $start; $i <= $end; $i++) {
639
            $this->_options['font_size'] = $i;
640
            $res = $this->measurize();
641
642
            if ($res === false) {
643
                if ($start == $i) {
644
                    $this->_options['font_size'] = -1;
645
                    throw new Image_Text_Exception("No possible font size found");
646
                }
647
                $this->_options['font_size'] -= 1;
648
                $this->_measurizedSize = $this->_options['font_size'];
649
                break;
650
            }
651
            // Always the last couple of lines is stored here.
652
            $this->_lines = $res;
653
        }
654
        return $this->_options['font_size'];
655
    }
656
657
    /**
658
     * Measurize text into the text box
659
     *
660
     * This method makes your text fit into the defined textbox by measurizing the
661
     * lines for your given font-size. You can do this manually before rendering (or
662
     * use even Image_Text::autoMeasurize()) or the renderer will do measurizing
663
     * automatically.
664
     *
665
     * @param bool $force Optionally, default is false, set true to force
666
     *                    measurizing.
667
     *
668
     * @return array Array of measured lines.
669
     * @see    Image_Text::autoMeasurize()
670
     * @throws Image_Text_Exception
671
     */
672
    public function measurize($force = false)
673
    {
674
        if (!$this->_init) {
675
            throw new Image_Text_Exception('Not initialized. Call ->init() first!');
676
        }
677
        $this->_processText();
678
679
        // Precaching options
680
        $font = $this->_font;
681
        $size = $this->_options['font_size'];
682
683
        $space = (1 + $this->_options['line_spacing'])
684
            * $this->_options['font_size'];
685
686
        $max_lines = (int)$this->_options['max_lines'];
687
688
        if (($max_lines < 1) && !$force) {
689
            return false;
690
        }
691
692
        $block_width = $this->_options['width'];
693
        $block_height = $this->_options['height'];
694
695
        $colors_cnt = sizeof($this->_colors);
696
697
        $text_line = '';
698
699
        $lines_cnt = 0;
700
701
        $lines = array();
702
703
        $text_height = 0;
704
        $text_width = 0;
705
706
        $i = 0;
707
        $para_cnt = 0;
708
        $width = 0;
709
710
        $beginning_of_line = true;
711
712
        // Run through tokens and order them in lines
713
        foreach ($this->_tokens as $token) {
714
            // Handle new paragraphs
715
            if ($token == "\n") {
716
                $bounds = imagettfbbox($size, 0, $font, $text_line);
717
                if ((++$lines_cnt >= $max_lines) && !$force) {
718
                    return false;
719
                }
720
                if ($this->_options['color_mode'] == 'paragraph') {
721
                    $c = $this->_colors[$para_cnt % $colors_cnt];
722
                    $i++;
723
                } else {
724
                    $c = $this->_colors[$i++ % $colors_cnt];
725
                }
726
                $lines[] = array(
727
                    'string' => $text_line,
728
                    'width' => $bounds[2] - $bounds[0],
729
                    'height' => $bounds[1] - $bounds[7],
730
                    'bottom_margin' => $bounds[1],
731
                    'left_margin' => $bounds[0],
732
                    'color' => $c
733
                );
734
                $text_width = max($text_width, ($bounds[2] - $bounds[0]));
735
                $text_height += (int)$space;
736
                if (($text_height > $block_height) && !$force) {
737
                    return false;
738
                }
739
                $para_cnt++;
740
                $text_line = '';
741
                $beginning_of_line = true;
742
                continue;
743
            }
744
745
            // Usual lining up
746
747
            if ($beginning_of_line) {
748
                $text_line = '';
749
                $text_line_next = $token;
750
                $beginning_of_line = false;
751
            } else {
752
                $text_line_next = $text_line . ' ' . $token;
753
            }
754
            $bounds = imagettfbbox($size, 0, $font, $text_line_next);
755
            $prev_width = isset($prev_width) ? $width : 0;
756
            $width = $bounds[2] - $bounds[0];
757
758
            // Handling of automatic new lines
759
            if ($width > $block_width) {
760
                if ((++$lines_cnt >= $max_lines) && !$force) {
761
                    return false;
762
                }
763
                if ($this->_options['color_mode'] == 'line') {
764
                    $c = $this->_colors[$i++ % $colors_cnt];
765
                } else {
766
                    $c = $this->_colors[$para_cnt % $colors_cnt];
767
                    $i++;
768
                }
769
770
                $lines[] = array(
771
                    'string' => $text_line,
772
                    'width' => $prev_width,
773
                    'height' => $bounds[1] - $bounds[7],
774
                    'bottom_margin' => $bounds[1],
775
                    'left_margin' => $bounds[0],
776
                    'color' => $c
777
                );
778
                $text_width = max($text_width, ($bounds[2] - $bounds[0]));
779
                $text_height += (int)$space;
780
                if (($text_height > $block_height) && !$force) {
781
                    return false;
782
                }
783
784
                $text_line = $token;
785
                $bounds = imagettfbbox($size, 0, $font, $text_line);
786
                $width = $bounds[2] - $bounds[0];
787
                $beginning_of_line = false;
788
            } else {
789
                $text_line = $text_line_next;
790
            }
791
        }
792
        // Store remaining line
793
        $bounds = imagettfbbox($size, 0, $font, $text_line);
794
        $i++;
795
        if ($this->_options['color_mode'] == 'line') {
796
            $c = $this->_colors[$i % $colors_cnt];
797
        } else {
798
            $c = $this->_colors[$para_cnt % $colors_cnt];
799
        }
800
        $lines[] = array(
801
            'string' => $text_line,
802
            'width' => $bounds[2] - $bounds[0],
803
            'height' => $bounds[1] - $bounds[7],
804
            'bottom_margin' => $bounds[1],
805
            'left_margin' => $bounds[0],
806
            'color' => $c
807
        );
808
809
        // add last line height, but without the line-spacing
810
        $text_height += $this->_options['font_size'];
811
812
        $text_width = max($text_width, ($bounds[2] - $bounds[0]));
813
814
        if (($text_height > $block_height) && !$force) {
815
            return false;
816
        }
817
818
        $this->_realTextSize = array(
819
            'width' => $text_width, 'height' => $text_height
820
        );
821
        $this->_measurizedSize = $this->_options['font_size'];
822
823
        return $lines;
824
    }
825
826
    /**
827
     * Render the text in the canvas using the given options.
828
     *
829
     * This renders the measurized text or automatically measures it first. The
830
     * $force parameter can be used to switch of measurizing problems (this may cause
831
     * your text being rendered outside a given text box or destroy your image
832
     * completely).
833
     *
834
     * @param bool $force Optional, initially false, set true to silence measurize
835
     *                    errors.
836
     *
837
     * @return void
838
     * @throws Image_Text_Exception
839
     */
840
    public function render($force = false)
841
    {
842
        if (!$this->_init) {
843
            throw new Image_Text_Exception('Not initialized. Call ->init() first!');
844
        }
845
846
        if (!$this->_tokens) {
847
            $this->_processText();
848
        }
849
850
        if (empty($this->_lines)
851
            || ($this->_measurizedSize != $this->_options['font_size'])
852
        ) {
853
            $this->_lines = $this->measurize($force);
854
        }
855
        $lines = $this->_lines;
856
857
        if ($this->_mode === 'auto') {
858
            $this->_img = imagecreatetruecolor(
859
                $this->_realTextSize['width'],
860
                $this->_realTextSize['height']
861
            );
862
            if (!$this->_img) {
863
                throw new Image_Text_Exception('Could not create image canvas.');
864
            }
865
            $this->_mode = '';
866
            $this->setColors($this->_options['color']);
867
        }
868
869
        $block_width = $this->_options['width'];
870
871
        $max_lines = $this->_options['max_lines'];
872
873
        $angle = $this->_options['angle'];
874
        $radians = round(deg2rad($angle), 3);
875
876
        $font = $this->_font;
877
        $size = $this->_options['font_size'];
878
879
        $line_spacing = $this->_options['line_spacing'];
880
881
        $align = $this->_options['halign'];
882
883
        $offset = $this->_getOffset();
884
885
        $start_x = $offset['x'];
886
        $start_y = $offset['y'];
887
888
        $sinR = sin($radians);
889
        $cosR = cos($radians);
890
891
        switch ($this->_options['valign']) {
892
        case self::IMAGE_TEXT_ALIGN_TOP:
893
            $valign_space = 0;
894
            break;
895
        case self::IMAGE_TEXT_ALIGN_MIDDLE:
896
            $valign_space = ($this->_options['height']
897
                    - $this->_realTextSize['height']) / 2;
898
            break;
899
        case self::IMAGE_TEXT_ALIGN_BOTTOM:
900
            $valign_space = $this->_options['height']
901
                - $this->_realTextSize['height'];
902
            break;
903
        default:
904
            $valign_space = 0;
905
        }
906
907
        $space = (1 + $line_spacing) * $size;
908
909
        // Adjustment of align + translation of top-left-corner to bottom-left-corner
910
        // of first line
911
        $new_posx = $start_x + ($sinR * ($valign_space + $size));
912
        $new_posy = $start_y + ($cosR * ($valign_space + $size));
913
914
        $lines_cnt = min($max_lines, sizeof($lines));
915
916
        $bboxes = array();
917
        // Go thorugh lines for rendering
918
        for ($i = 0; $i < $lines_cnt; $i++) {
919
920
            // Calc the new start X and Y (only for line>0)
921
            // the distance between the line above is used
922
            if ($i > 0) {
923
                $new_posx += $sinR * $space;
924
                $new_posy += $cosR * $space;
925
            }
926
927
            // Calc the position of the 1st letter. We can then get the left and
928
            // bottom margins 'i' is really not the same than 'j' or 'g'.
929
            $left_margin = $lines[$i]['left_margin'];
930
            $line_width = $lines[$i]['width'];
931
932
            // Calc the position using the block width, the current line width and
933
            // obviously the angle. That gives us the offset to slide the line.
934
            switch ($align) {
935
            case self::IMAGE_TEXT_ALIGN_LEFT:
936
                $hyp = 0;
937
                break;
938
            case self::IMAGE_TEXT_ALIGN_RIGHT:
939
                $hyp = $block_width - $line_width - $left_margin;
940
                break;
941
            case self::IMAGE_TEXT_ALIGN_CENTER:
942
                $hyp = ($block_width - $line_width) / 2 - $left_margin;
943
                break;
944
            default:
945
                $hyp = 0;
946
                break;
947
            }
948
949
            $posx = $new_posx + $cosR * $hyp;
950
            $posy = $new_posy - $sinR * $hyp;
951
952
            $c = $lines[$i]['color'];
953
954
            // Render textline
955
            $bboxes[] = imagettftext(
956
                $this->_img, $size, $angle, $posx, $posy,
957
                $c, $font, $lines[$i]['string']
958
            );
959
        }
960
        $this->_bbox = $bboxes;
961
    }
962
963
    /**
964
     * Return the image ressource.
965
     *
966
     * Get the image canvas.
967
     *
968
     * @return resource Used image resource
969
     */
970
    public function getImg()
971
    {
972
        return $this->_img;
973
    }
974
975
    /**
976
     * Display the image (send it to the browser).
977
     *
978
     * This will output the image to the users browser. You can use the standard
979
     * IMAGETYPE_* constants to determine which image type will be generated.
980
     * Optionally you can save your image to a destination you set in the options.
981
     *
982
     * @param bool $save Save or not the image on printout.
983
     * @param bool $free Free the image on exit.
984
     *
985
     * @return  bool         True on success
986
     * @see Image_Text::save()
987
     * @throws Image_Text_Exception
988
     */
989
    public function display($save = false, $free = false)
990
    {
991
        if (!headers_sent()) {
992
            header(
993
                "Content-type: " .
994
                image_type_to_mime_type($this->_options['image_type'])
995
            );
996
        } else {
997
            throw new Image_Text_Exception('Header already sent.');
998
        }
999
        switch ($this->_options['image_type']) {
1000
        case IMAGETYPE_PNG:
1001
            $imgout = 'imagepng';
1002
            break;
1003
        case IMAGETYPE_JPEG:
1004
            $imgout = 'imagejpeg';
1005
            break;
1006
        case IMAGETYPE_BMP:
1007
            $imgout = 'imagebmp';
1008
            break;
1009
        default:
1010
            throw new Image_Text_Exception('Unsupported image type.');
1011
        }
1012
        if ($save) {
1013
            $imgout($this->_img);
1014
            $this->save();
1015
        } else {
1016
            $imgout($this->_img);
1017
        }
1018
1019
        if ($free) {
1020
            $res = imagedestroy($this->_img);
1021
            if (!$res) {
1022
                throw new Image_Text_Exception('Destroying image failed.');
1023
            }
1024
        }
1025
    }
1026
1027
    /**
1028
     * Save image canvas.
1029
     *
1030
     * Saves the image to a given destination. You can leave out the destination file
1031
     * path, if you have the option for that set correctly. Saving is possible with
1032
     * the display() method, too.
1033
     *
1034
     * @param bool|string $destFile The destination to save to (optional, uses
1035
     *                              options value else).
1036
     *
1037
     * @see Image_Text::display()
1038
     * @return void
1039
     * @throws Image_Text_Exception
1040
     */
1041
    public function save($destFile = false)
1042
    {
1043
        if (!$destFile) {
1044
            $destFile = $this->_options['dest_file'];
1045
        }
1046
        if (!$destFile) {
1047
            throw new Image_Text_Exception("Invalid desitination file.");
1048
        }
1049
1050
        switch ($this->_options['image_type']) {
1051
        case IMAGETYPE_PNG:
1052
            $imgout = 'imagepng';
1053
            break;
1054
        case IMAGETYPE_JPEG:
1055
            $imgout = 'imagejpeg';
1056
            break;
1057
        case IMAGETYPE_BMP:
1058
            $imgout = 'imagebmp';
1059
            break;
1060
        default:
1061
            throw new Image_Text_Exception('Unsupported image type.');
1062
            break;
1063
        }
1064
1065
        $res = $imgout($this->_img, $destFile);
1066
        if (!$res) {
1067
            throw new Image_Text_Exception('Saving file failed.');
1068
        }
1069
    }
1070
1071
    /**
1072
     * Get completely translated offset for text rendering.
1073
     *
1074
     * Get completely translated offset for text rendering. Important for usage of
1075
     * center coords and angles.
1076
     *
1077
     * @return array Array of x/y coordinates.
1078
     */
1079
    private function _getOffset()
1080
    {
1081
        // Presaving data
1082
        $width = $this->_options['width'];
1083
        $height = $this->_options['height'];
1084
        $angle = $this->_options['angle'];
1085
        $x = $this->_options['x'];
1086
        $y = $this->_options['y'];
1087
        // Using center coordinates
1088
        if (!empty($this->_options['cx']) && !empty($this->_options['cy'])) {
1089
            $cx = $this->_options['cx'];
1090
            $cy = $this->_options['cy'];
1091
            // Calculation top left corner
1092
            $x = $cx - ($width / 2);
1093
            $y = $cy - ($height / 2);
1094
            // Calculating movement to keep the center point on himslf after rotation
1095
            if ($angle) {
1096
                $ang = deg2rad($angle);
1097
                // Vector from the top left cornern ponting to the middle point
1098
                $vA = array(($cx - $x), ($cy - $y));
1099
                // Matrix to rotate vector
1100
                // sinus and cosinus
1101
                $sin = round(sin($ang), 14);
1102
                $cos = round(cos($ang), 14);
1103
                // matrix
1104
                $mRot = array(
1105
                    $cos, (-$sin),
1106
                    $sin, $cos
1107
                );
1108
                // Multiply vector with matrix to get the rotated vector
1109
                // This results in the location of the center point after rotation
1110
                $vB = array(
1111
                    ($mRot[0] * $vA[0] + $mRot[2] * $vA[0]),
1112
                    ($mRot[1] * $vA[1] + $mRot[3] * $vA[1])
1113
                );
1114
                // To get the movement vector, we subtract the original middle
1115
                $vC = array(
1116
                    ($vA[0] - $vB[0]),
1117
                    ($vA[1] - $vB[1])
1118
                );
1119
                // Finally we move the top left corner coords there
1120
                $x += $vC[0];
1121
                $y += $vC[1];
1122
            }
1123
        }
1124
        return array('x' => (int)round($x, 0), 'y' => (int)round($y, 0));
1125
    }
1126
1127
    /**
1128
     * Convert a color to an array.
1129
     *
1130
     * The following colors syntax must be used:
1131
     * "#08ffff00" hexadecimal format with alpha channel (08)
1132
     *
1133
     * @param string $scolor string of colorcode.
1134
     *
1135
     * @see Image_Text::IMAGE_TEXT_REGEX_HTMLCOLOR
1136
     * @return bool|array false if string can't be converted to array
1137
     */
1138
    public static function convertString2RGB($scolor)
1139
    {
1140
        if (preg_match(self::IMAGE_TEXT_REGEX_HTMLCOLOR, $scolor, $matches)) {
1141
            return array(
1142
                'r' => hexdec($matches[2]),
1143
                'g' => hexdec($matches[3]),
1144
                'b' => hexdec($matches[4]),
1145
                'a' => hexdec(!empty($matches[1]) ? $matches[1] : 0),
1146
            );
1147
        }
1148
        return false;
1149
    }
1150
1151
    /**
1152
     * Extract the tokens from the text.
1153
     *
1154
     * @return void
1155
     */
1156
    private function _processText()
1157
    {
1158
        if (!isset($this->_text)) {
1159
            return;
1160
        }
1161
        $this->_tokens = array();
1162
1163
        // Normalize linebreak to "\n"
1164
        $this->_text = preg_replace("[\r\n]", "\n", $this->_text);
1165
1166
        // Get each paragraph
1167
        $paras = explode("\n", $this->_text);
1168
1169
        // loop though the paragraphs
1170
        // and get each word (token)
1171
        foreach ($paras as $para) {
1172
            $words = explode(' ', $para);
1173
            foreach ($words as $word) {
1174
                $this->_tokens[] = $word;
1175
            }
1176
            // add a "\n" to mark the end of a paragraph
1177
            $this->_tokens[] = "\n";
1178
        }
1179
        // we do not need an end paragraph as the last token
1180
        array_pop($this->_tokens);
1181
    }
1182
}
1183