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