fix image text
[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 __construct($text, $options = null)
373     {
374                 $this->set('text', $text);
375
376         if (!empty($options)) {
377              $this->options = array_merge($this->options, $options);
378          }
379           
380     }
381
382     /**
383      * Construct and initialize an Image_Text in one step.
384      * This method is called statically and creates plus initializes an Image_Text object.
385      * Beware: You will have to recall init() if you set an option afterwards manually.
386      *
387      * @param   string  $text       Text to print.
388      * @param   array   $options    Options.
389      * @access public
390      * @static
391      * @see Image_Text::set(), Image_Text::Image_Text(), Image_Text::init()
392      */
393
394     function &construct ( $text, $options ) {
395         $itext = new Image_Text($text, $options);
396         $res = $itext->init();
397         if (PEAR::isError($res)) {
398             return $res;
399         }
400         return $itext;
401     }
402
403     /**
404      * Set options
405      *
406      * Set a single or multiple options. It may happen that you have to reinitialize the Image_Text
407      * object after changing options. For possible options, please take a look at the class options
408      * array!
409      *
410      *
411      * @param   mixed   $option     A single option name or the options array.
412      * @param   mixed   $value      Option value if $option is string.
413      * @return  bool                True on success, otherwise PEAR::Error.
414      * @access public
415      * @see Image_Text::Image_Text()
416      */
417
418     function set($option, $value=null)
419     {
420         $reInits = array_flip($this->_reInits);
421         if (!is_array($option)) {
422             if (!isset($value)) {
423                 return PEAR::raiseError('No value given.');
424             }
425             $option = array($option => $value);
426         }
427         foreach ($option as $opt => $val) {
428             switch ($opt) {
429              case 'color':
430                 $this->setColors($val);
431                 break;
432              case 'text':
433                 if (is_array($val)) {
434                     $this->_text = implode('\n', $val);
435                 } else {
436                     $this->_text = $val;
437                 }
438                 break;
439              default:
440                 $this->options[$opt] = $val;
441                 break;
442             }
443             if (isset($reInits[$opt])) {
444                 $this->_init = false;
445             }
446         }
447         return true;
448     }
449
450     /**
451      * Set the color-set
452      *
453      * Using this method you can set multiple colors for your text.
454      * Use a simple numeric array to determine their order and give
455      * it to this function. Multiple colors will be
456      * cycled by the options specified 'color_mode' option. The given array
457      * will overwrite the existing color settings!
458      *
459      * The following colors syntaxes are understood by this method:
460      * - "#ffff00" hexadecimal format (HTML style), with and without #.
461      * - "#08ffff00" hexadecimal format (HTML style) with alpha channel (08), with and without #.
462      * - array with 'r','g','b' and (optionally) 'a' keys, using int values.
463      * - a GD color special color (tiled,...).
464      *
465      * A single color or an array of colors are allowed here.
466      *
467      * @param   mixed  $colors       Single color or array of colors.
468      * @return  bool                 True on success, otherwise PEAR::Error.
469      * @access  public
470      * @see Image_Text::setColor(), Image_Text::set()
471      */
472
473     function setColors($colors)
474     {
475         $i = 0;
476         if (is_array($colors) &&
477             (is_string($colors[0]) || is_array($colors[0]))
478            ) {
479             foreach ($colors as $color) {
480                 $res = $this->setColor($color,$i++);
481                 if (PEAR::isError($res)) {
482                     return $res;
483                 }
484             }
485         } else {
486             return $this->setColor($colors, $i);
487         }
488         return true;
489     }
490
491     /**
492      * Set a color
493      *
494      * This method is used to set a color at a specific color ID inside the
495      * color cycle.
496      *
497      * The following colors syntaxes are understood by this method:
498      * - "#ffff00" hexadecimal format (HTML style), with and without #.
499      * - "#08ffff00" hexadecimal format (HTML style) with alpha channel (08), with and without #.
500      * - array with 'r','g','b' and (optionally) 'a' keys, using int values.
501      *
502      * @param   mixed    $color        Color value.
503      * @param   mixed    $id           ID (in the color array) to set color to.
504      * @return  bool                True on success, otherwise PEAR::Error.
505      * @access  public
506      * @see Image_Text::setColors(), Image_Text::set()
507      */
508
509     function setColor($color, $id=0)
510     {
511         if(is_array($color)) {
512             if (isset($color['r']) && isset($color['g']) && isset($color['b'])) {
513                 $color['a'] = isset($color['a']) ? $color['a'] : 0;
514                 $this->options['colors'][$id] = $color;
515             } else if (isset($color[0]) && isset($color[1]) && isset($color[2])) {
516                 $color['r'] = $color[0];
517                 $color['g'] = $color[1];
518                 $color['b'] = $color[2];
519                 $color['a'] = isset($color[3]) ? $color[3] : 0;
520                 $this->options['colors'][$id] = $color;
521             } else {
522                 return PEAR::raiseError('Use keys 1,2,3 (optionally) 4 or r,g,b and (optionally) a.');
523             }
524         } elseif (is_string($color)) {
525             $color = $this->_convertString2RGB($color);
526             if ($color) {
527                 $this->options['color'][$id] = $color;
528             } else {
529                 return PEAR::raiseError('Invalid color.');
530             }
531         }
532         if ($this->_img) {
533             $aaFactor = ($this->options['antialias']) ? 1 : -1;
534             if (function_exists('imagecolorallocatealpha') && isset($color['a'])) {
535                 $this->colors[$id] = $aaFactor * imagecolorallocatealpha($this->_img,
536                                 $color['r'],$color['g'],$color['b'],$color['a']);
537             } else {
538                 $this->colors[$id] = $aaFactor * imagecolorallocate($this->_img,
539                                 $color['r'],$color['g'],$color['b']);
540             }
541             if ($this->colors[$id] == 0 && $aaFactor == -1) {
542                 // correction for black with antialiasing OFF
543                 // since color cannot be negative zero
544                 $this->colors[$id] = -256;
545             }
546         }
547         return true;
548     }
549
550     /**
551      * Initialiaze the Image_Text object.
552      *
553      * This method has to be called after setting the options for your Image_Text object.
554      * It initializes the canvas, normalizes some data and checks important options.
555      * Be shure to check the initialization after you switched some options. The
556      * set() method may force you to reinitialize the object.
557      *
558      * @access  public
559      * @return  bool  True on success, otherwise PEAR::Error.
560      * @see Image_Text::set()
561      */
562
563     function init()
564     {
565         // Does the fontfile exist and is readable?
566         // todo: with some versions of the GD-library it's also possible to leave font_path empty, add strip ".ttf" from
567         //        the fontname; the fontfile will then be automatically searched for in library-defined directories
568         //        however this does not yet work if at this point we check for the existance of the fontfile
569          
570         $font_file = rtrim($this->options['font_path'], '/\\');
571         $font_file.= (OS_WINDOWS) ? '\\' : '/';
572         $font_file.= $this->options['font_file'];
573         $font_file = realpath($font_file);
574         if (empty($font_file) || !is_file($font_file) || !is_readable($font_file)) {
575             return PEAR::staticRaiseError('Fontfile not found or not readable: ' . $font_file);
576         } else {
577             $this->_font = $font_file;
578         }
579
580         // Is the font size to small?
581         if ($this->options['width'] < 1) {
582             return PEAR::raiseError('Width too small. Has to be > 1.');
583         }
584
585         // Check and create canvas
586         $image_canvas = false;
587         switch (true) {
588             case (empty($this->options['canvas'])):
589
590                 // Create new image from width && height of the clipping
591                 $this->_img = imagecreatetruecolor(
592                             $this->options['width'], $this->options['height']);
593                 if (!$this->_img) {
594                     return PEAR::raiseError('Could not create image canvas.');
595                 }
596                 break;
597
598             case (is_resource($this->options['canvas']) &&
599                     get_resource_type($this->options['canvas'])=='gd'):
600                 // The canvas is an image resource
601                 $image_canvas = true;
602                 $this->_img = $this->options['canvas'];
603                 break;
604
605             case (is_array($this->options['canvas']) &&
606                     isset($this->options['canvas']['width']) &&
607                     isset($this->options['canvas']['height'])):
608
609                 // Canvas must be a width and height measure
610                 $this->_img = imagecreatetruecolor(
611                     $this->options['canvas']['width'],
612                     $this->options['canvas']['height']
613                 );
614                 break;
615
616
617             case (is_array($this->options['canvas']) &&
618                     isset($this->options['canvas']['size']) &&
619                     ($this->options['canvas']['size'] = 'auto')):
620
621             case (is_string($this->options['canvas']) &&
622                      ($this->options['canvas'] = 'auto')):
623                 $this->_mode = 'auto';
624                 break;
625
626             default:
627                 return PEAR::raiseError('Could not create image canvas.');
628
629         }
630
631
632
633         if ($this->_img) {
634             $this->options['canvas'] = array();
635             $this->options['canvas']['width']  = imagesx($this->_img);
636             $this->options['canvas']['height'] = imagesy($this->_img);
637         }
638
639         if ($this->options['enable_alpha']) {
640             imagesavealpha($this->_img, true);
641             imagealphablending($this->_img, false);
642         }
643
644         if ($this->options['background_color'] === null) {
645             $this->options['enable_alpha'] = true;
646             imagesavealpha($this->_img, true);
647             imagealphablending($this->_img, false);
648             $colBg = imagecolorallocatealpha($this->_img, 255, 255, 255, 127);
649         } else {
650             $arBg  = $this->_convertString2RGB($this->options['background_color']);
651             if ($arBg === false) {
652                 return PEAR::raiseError('Background color is invalid.');
653             }
654             $colBg = imagecolorallocatealpha($this->_img, $arBg['r'], $arBg['g'], $arBg['b'], $arBg['a']);
655         }
656         if ($image_canvas === false) {
657             imagefilledrectangle(
658                 $this->_img,
659                 0, 0,
660                 $this->options['canvas']['width'] - 1, $this->options['canvas']['height'] - 1,
661                 $colBg
662             );
663         }
664
665
666         // Save and repair angle
667         $angle = $this->options['angle'];
668         while ($angle < 0) {
669             $angle += 360;
670         }
671         if ($angle > 359) {
672             $angle = $angle % 360;
673         }
674         $this->options['angle'] = $angle;
675
676         // Set the color values
677         $res = $this->setColors($this->options['color']);
678         if (PEAR::isError($res)) {
679             return $res;
680         }
681
682         $this->_lines = null;
683
684         // Initialization is complete
685         $this->_init = true;
686         return true;
687     }
688
689     /**
690      * Auto measurize text
691      *
692      * Automatically determines the greatest possible font size to
693      * fit the text into the text box. This method may be very resource
694      * intensive on your webserver. A good tweaking point are the $start
695      * and $end parameters, which specify the range of font sizes to search
696      * through. Anyway, the results should be cached if possible. You can
697      * optionally set $start and $end here as a parameter or the settings of
698      * the options array are used.
699      *
700      * @access public
701      * @param  int      $start  Fontsize to start testing with.
702      * @param  int      $end    Fontsize to end testing with.
703      * @return int              Fontsize measured or PEAR::Error.
704      * @see Image_Text::measurize()
705      */
706
707     function autoMeasurize($start=false, $end=false)
708     {
709         if (!$this->_init) {
710             return PEAR::raiseError('Not initialized. Call ->init() first!');
711         }
712
713         $start = (empty($start)) ? $this->options['min_font_size'] : $start;
714         $end = (empty($end)) ? $this->options['max_font_size'] : $end;
715
716         $res = false;
717         // Run through all possible font sizes until a measurize fails
718         // Not the optimal way. This can be tweaked!
719         for ($i = $start; $i <= $end; $i++) {
720             $this->options['font_size'] = $i;
721             $res = $this->measurize();
722
723             if ($res === false) {
724                 if ($start == $i) {
725                     $this->options['font_size'] = -1;
726                     return PEAR::raiseError("No possible font size found");
727                 }
728                 $this->options['font_size'] -= 1;
729                 $this->_measurizedSize = $this->options['font_size'];
730                 break;
731             }
732             // Always the last couple of lines is stored here.
733             $this->_lines = $res;
734         }
735         return $this->options['font_size'];
736     }
737
738     /**
739      * Measurize text into the text box
740      *
741      * This method makes your text fit into the defined textbox by measurizing the
742      * lines for your given font-size. You can do this manually before rendering (or use
743      * even Image_Text::autoMeasurize()) or the renderer will do measurizing
744      * automatically.
745      *
746      * @access public
747      * @param  bool  $force  Optionally, default is false, set true to force measurizing.
748      * @return array         Array of measured lines or PEAR::Error.
749      * @see Image_Text::autoMeasurize()
750      */
751
752     function measurize($force=false)
753     {
754         if (!$this->_init) {
755             return PEAR::raiseError('Not initialized. Call ->init() first!');
756         }
757         $this->_processText();
758
759         // Precaching options
760         $font = $this->_font;
761         $size = $this->options['font_size'];
762
763         $line_spacing = $this->options['line_spacing'];
764         $space = (1 + $this->options['line_spacing']) * $this->options['font_size'];
765
766         $max_lines = (int)$this->options['max_lines'];
767
768         if (($max_lines<1) && !$force) {
769             return false;
770         }
771
772         $block_width = $this->options['width'];
773         $block_height = $this->options['height'];
774
775         $colors_cnt = sizeof($this->colors);
776         $c = $this->colors[0];
777
778         $text_line = '';
779
780         $lines_cnt = 0;
781         $tokens_cnt = sizeof($this->_tokens);
782
783         $lines = array();
784
785         $text_height = 0;
786         $text_width = 0;
787
788         $i = 0;
789         $para_cnt = 0;
790
791         $beginning_of_line = true;
792
793         // Run through tokens and order them in lines
794         foreach($this->_tokens as $token) {
795             // Handle new paragraphs
796             if ($token=="\n") {
797                 $bounds = imagettfbbox($size, 0, $font, $text_line);
798                 if ((++$lines_cnt>=$max_lines) && !$force) {
799                     return false;
800                 }
801                 if ($this->options['color_mode']=='paragraph') {
802                     $c = $this->colors[$para_cnt%$colors_cnt];
803                     $i++;
804                 } else {
805                     $c = $this->colors[$i++%$colors_cnt];
806                 }
807                 $lines[]  = array(
808                                 'string'        => $text_line,
809                                 'width'         => $bounds[2]-$bounds[0],
810                                 'height'        => $bounds[1]-$bounds[7],
811                                 'bottom_margin' => $bounds[1],
812                                 'left_margin'   => $bounds[0],
813                                 'color'         => $c
814                             );
815                 $text_width = max($text_width, ($bounds[2]-$bounds[0]));
816                 $text_height += (int)$space;
817                 if (($text_height > $block_height) && !$force) {
818                     return false;
819                 }
820                 $para_cnt++;
821                 $text_line = '';
822                 $beginning_of_line = true;
823                 continue;
824             }
825
826             // Usual lining up
827
828             if ($beginning_of_line) {
829                 $text_line = '';
830                 $text_line_next = $token;
831                 $beginning_of_line = false;
832             } else {
833                 $text_line_next = $text_line.' '.$token;
834             }
835             $bounds = imagettfbbox($size, 0, $font, $text_line_next);
836             $prev_width = isset($prev_width)?$width:0;
837             $width = $bounds[2]-$bounds[0];
838
839             // Handling of automatic new lines
840             if ($width>$block_width) {
841                 if ((++$lines_cnt>=$max_lines) && !$force) {
842                     return false;
843                 }
844                 if ($this->options['color_mode']=='line') {
845                     $c = $this->colors[$i++%$colors_cnt];
846                 } else {
847                     $c = $this->colors[$para_cnt%$colors_cnt];
848                     $i++;
849                 }
850
851                 $lines[]  = array(
852                                 'string'    => $text_line,
853                                 'width'     => $prev_width,
854                                 'height'    => $bounds[1]-$bounds[7],
855                                 'bottom_margin' => $bounds[1],
856                                 'left_margin'   => $bounds[0],
857                                 'color'         => $c
858                             );
859                 $text_width = max($text_width, ($bounds[2]-$bounds[0]));
860                 $text_height += (int)$space;
861                 if (($text_height > $block_height) && !$force) {
862                     return false;
863                 }
864
865                 $text_line      = $token;
866                 $bounds = imagettfbbox($size, 0, $font, $text_line);
867                 $width = $bounds[2]-$bounds[0];
868                 $beginning_of_line = false;
869             } else {
870                 $text_line = $text_line_next;
871             }
872         }
873         // Store remaining line
874         $bounds = imagettfbbox($size, 0, $font,$text_line);
875         if ($this->options['color_mode']=='line') {
876             $c = $this->colors[$i++%$colors_cnt];
877         } else {
878             $c = $this->colors[$para_cnt%$colors_cnt];
879             $i++;
880         }
881         $lines[]  = array(
882                         'string'=> $text_line,
883                         'width' => $bounds[2]-$bounds[0],
884                         'height'=> $bounds[1]-$bounds[7],
885                         'bottom_margin' => $bounds[1],
886                         'left_margin'   => $bounds[0],
887                         'color'         => $c
888                     );
889
890         // add last line height, but without the line-spacing
891         $text_height += $this->options['font_size'];
892
893         $text_width = max($text_width, ($bounds[2]-$bounds[0]));
894
895         if (($text_height > $block_height) && !$force) {
896             return false;
897         }
898
899         $this->_realTextSize = array('width' => $text_width, 'height' => $text_height);
900         $this->_measurizedSize = $this->options['font_size'];
901
902         return $lines;
903     }
904
905     /**
906      * Render the text in the canvas using the given options.
907      *
908      * This renders the measurized text or automatically measures it first. The $force parameter
909      * can be used to switch of measurizing problems (this may cause your text being rendered
910      * outside a given text box or destroy your image completely).
911      *
912      * @access public
913      * @param  bool     $force  Optional, initially false, set true to silence measurize errors.
914      * @return bool             True on success, otherwise PEAR::Error.
915      */
916
917     function render($force=false)
918     {
919         if (!$this->_init) {
920             return PEAR::raiseError('Not initialized. Call ->init() first!');
921         }
922
923         if (!$this->_tokens) {
924             $this->_processText();
925         }
926
927         if (empty($this->_lines) || ($this->_measurizedSize != $this->options['font_size'])) {
928             $this->_lines = $this->measurize( $force );
929         }
930         $lines = $this->_lines;
931
932         if (PEAR::isError($this->_lines)) {
933             return $this->_lines;
934         }
935
936         if ($this->_mode === 'auto') {
937             $this->_img = imagecreatetruecolor(
938                         $this->_realTextSize['width'],
939                         $this->_realTextSize['height']
940                     );
941             if (!$this->_img) {
942                 return PEAR::raiseError('Could not create image cabvas.');
943             }
944             $this->_mode = '';
945             $this->setColors($this->_options['color']);
946         }
947
948         $block_width = $this->options['width'];
949         $block_height = $this->options['height'];
950
951         $max_lines = $this->options['max_lines'];
952
953         $angle = $this->options['angle'];
954         $radians = round(deg2rad($angle), 3);
955
956         $font = $this->_font;
957         $size = $this->options['font_size'];
958
959         $line_spacing = $this->options['line_spacing'];
960
961         $align = $this->options['halign'];
962
963         $im = $this->_img;
964
965         $offset = $this->_getOffset();
966
967         $start_x = $offset['x'];
968         $start_y = $offset['y'];
969
970         $end_x = $start_x + $block_width;
971         $end_y = $start_y + $block_height;
972
973         $sinR = sin($radians);
974         $cosR = cos($radians);
975
976         switch ($this->options['valign']) {
977             case IMAGE_TEXT_ALIGN_TOP:
978                 $valign_space = 0;
979                 break;
980             case IMAGE_TEXT_ALIGN_MIDDLE:
981                 $valign_space = ($this->options['height'] - $this->_realTextSize['height']) / 2;
982                 break;
983             case IMAGE_TEXT_ALIGN_BOTTOM:
984                 $valign_space = $this->options['height'] - $this->_realTextSize['height'];
985                 break;
986             default:
987                 $valign_space = 0;
988         }
989
990         $space = (1 + $line_spacing) * $size;
991
992         // Adjustment of align + translation of top-left-corner to bottom-left-corner of first line
993         $new_posx = $start_x + ($sinR * ($valign_space + $size));
994         $new_posy = $start_y + ($cosR * ($valign_space + $size));
995
996         $lines_cnt = min($max_lines,sizeof($lines));
997
998         // Go thorugh lines for rendering
999         for($i=0; $i<$lines_cnt; $i++){
1000
1001             // Calc the new start X and Y (only for line>0)
1002             // the distance between the line above is used
1003             if($i > 0){
1004                 $new_posx += $sinR * $space;
1005                 $new_posy += $cosR * $space;
1006             }
1007
1008             // Calc the position of the 1st letter. We can then get the left and bottom margins
1009             // 'i' is really not the same than 'j' or 'g'.
1010             $bottom_margin  = $lines[$i]['bottom_margin'];
1011             $left_margin    = $lines[$i]['left_margin'];
1012             $line_width     = $lines[$i]['width'];
1013
1014             // Calc the position using the block width, the current line width and obviously
1015             // the angle. That gives us the offset to slide the line.
1016             switch($align) {
1017                 case IMAGE_TEXT_ALIGN_LEFT:
1018                     $hyp = 0;
1019                     break;
1020                 case IMAGE_TEXT_ALIGN_RIGHT:
1021                     $hyp = $block_width - $line_width - $left_margin;
1022                     break;
1023                 case IMAGE_TEXT_ALIGN_CENTER:
1024                     $hyp = ($block_width-$line_width)/2 - $left_margin;
1025                     break;
1026                 default:
1027                     $hyp = 0;
1028                     break;
1029             }
1030
1031             $posx = $new_posx + $cosR * $hyp;
1032             $posy = $new_posy - $sinR * $hyp;
1033
1034             $c = $lines[$i]['color'];
1035
1036             // Render textline
1037             $bboxes[] = imagettftext ($im, $size, $angle, $posx, $posy, $c, $font, $lines[$i]['string']);
1038         }
1039         $this->bbox = $bboxes;
1040         return true;
1041     }
1042
1043     /**
1044      * Return the image ressource.
1045      *
1046      * Get the image canvas.
1047      *
1048      * @access public
1049      * @return resource Used image resource
1050      */
1051
1052     function &getImg()
1053     {
1054         return $this->_img;
1055     }
1056
1057     /**
1058      * Display the image (send it to the browser).
1059      *
1060      * This will output the image to the users browser. You can use the standard IMAGETYPE_*
1061      * constants to determine which image type will be generated. Optionally you can save your
1062      * image to a destination you set in the options.
1063      *
1064      * @param   bool  $save  Save or not the image on printout.
1065      * @param   bool  $free  Free the image on exit.
1066      * @return  bool         True on success, otherwise PEAR::Error.
1067      * @access public
1068      * @see Image_Text::save()
1069      */
1070
1071     function display($save=false, $free=false)
1072     {
1073         if (!headers_sent()) {
1074             header("Content-type: " .image_type_to_mime_type($this->options['image_type']));
1075         } else {
1076             PEAR::raiseError('Header already sent.');
1077         }
1078         switch ($this->options['image_type']) {
1079             case IMAGETYPE_PNG:
1080                 $imgout = 'imagepng';
1081                 break;
1082             case IMAGETYPE_JPEG:
1083                 $imgout = 'imagejpeg';
1084                 break;
1085             case IMAGETYPE_BMP:
1086                 $imgout = 'imagebmp';
1087                 break;
1088             default:
1089                 return PEAR::raiseError('Unsupported image type.');
1090                 break;
1091         }
1092         if ($save) {
1093             $imgout($this->_img);
1094             $res = $this->save();
1095             if (PEAR::isError($res)) {
1096                 return $res;
1097             }
1098         } else {
1099            $imgout($this->_img);
1100         }
1101
1102         if ($free) {
1103             $res = imagedestroy($this->image);
1104             if (!$res) {
1105                 PEAR::raiseError('Destroying image failed.');
1106             }
1107         }
1108         return true;
1109     }
1110
1111     /**
1112      * Save image canvas.
1113      *
1114      * Saves the image to a given destination. You can leave out the destination file path,
1115      * if you have the option for that set correctly. Saving is possible with the display()
1116      * method, too.
1117      *
1118      * @param   string  $destFile   The destination to save to (optional, uses options value else).
1119      * @return  bool                True on success, otherwise PEAR::Error.
1120      * @see Image_Text::display()
1121      */
1122
1123     function save($dest_file=false)
1124     {
1125         if (!$dest_file) {
1126             $dest_file = $this->options['dest_file'];
1127         }
1128         if (!$dest_file) {
1129             return PEAR::raiseError("Invalid desitination file.");
1130         }
1131
1132         switch ($this->options['image_type']) {
1133             case IMAGETYPE_PNG:
1134                 $imgout = 'imagepng';
1135                 break;
1136             case IMAGETYPE_JPEG:
1137                 $imgout = 'imagejpeg';
1138                 break;
1139             case IMAGETYPE_BMP:
1140                 $imgout = 'imagebmp';
1141                 break;
1142             default:
1143                 return PEAR::raiseError('Unsupported image type.');
1144                 break;
1145         }
1146
1147         $res = $imgout($this->_img, $dest_file);
1148         if (!$res) {
1149             PEAR::raiseError('Saving file failed.');
1150         }
1151         return true;
1152     }
1153
1154     /**
1155      * Get completely translated offset for text rendering.
1156      *
1157      * Get completely translated offset for text rendering. Important
1158      * for usage of center coords and angles
1159      *
1160      * @access private
1161      * @return array    Array of x/y coordinates.
1162      */
1163
1164     function _getOffset()
1165     {
1166         // Presaving data
1167         $width = $this->options['width'];
1168         $height = $this->options['height'];
1169         $angle = $this->options['angle'];
1170         $x = $this->options['x'];
1171         $y = $this->options['y'];
1172         // Using center coordinates
1173         if (!empty($this->options['cx']) && !empty($this->options['cy'])) {
1174             $cx = $this->options['cx'];
1175             $cy = $this->options['cy'];
1176             // Calculation top left corner
1177             $x = $cx - ($width / 2);
1178             $y = $cy - ($height / 2);
1179             // Calculating movement to keep the center point on himslf after rotation
1180             if ($angle) {
1181                 $ang = deg2rad($angle);
1182                 // Vector from the top left cornern ponting to the middle point
1183                 $vA = array( ($cx - $x), ($cy - $y) );
1184                 // Matrix to rotate vector
1185                 // sinus and cosinus
1186                 $sin = round(sin($ang), 14);
1187                 $cos = round(cos($ang), 14);
1188                 // matrix
1189                 $mRot = array(
1190                     $cos, (-$sin),
1191                     $sin, $cos
1192                 );
1193                 // Multiply vector with matrix to get the rotated vector
1194                 // This results in the location of the center point after rotation
1195                 $vB = array (
1196                     ($mRot[0] * $vA[0] + $mRot[2] * $vA[0]),
1197                     ($mRot[1] * $vA[1] + $mRot[3] * $vA[1])
1198                 );
1199                 // To get the movement vector, we subtract the original middle
1200                 $vC = array (
1201                     ($vA[0] - $vB[0]),
1202                     ($vA[1] - $vB[1])
1203                 );
1204                 // Finally we move the top left corner coords there
1205                 $x += $vC[0];
1206                 $y += $vC[1];
1207             }
1208         }
1209         return array ('x' => (int)round($x, 0), 'y' => (int)round($y, 0));
1210     }
1211
1212     /**
1213      * Convert a color to an array.
1214      *
1215      * The following colors syntax must be used:
1216      * "#08ffff00" hexadecimal format with alpha channel (08)
1217      * array with 'r','g','b','a'(optionnal) keys
1218      * A GD color special color (tiled,...)
1219      * Only one color is allowed
1220      * If $id is given, the color index $id is used
1221      *
1222      * @param   mixed  $colors       Array of colors.
1223      * @param   mixed  $id           Array of colors.
1224      * @access private
1225      */
1226     function _convertString2RGB($scolor)
1227     {
1228         if (preg_match(IMAGE_TEXT_REGEX_HTMLCOLOR, $scolor, $matches)) {
1229             return array(
1230                            'r' => hexdec($matches[2]),
1231                            'g' => hexdec($matches[3]),
1232                            'b' => hexdec($matches[4]),
1233                            'a' => hexdec(!empty($matches[1])?$matches[1]:0),
1234                            );
1235         }
1236         return false;
1237     }
1238
1239     /**
1240      * Extract the tokens from the text.
1241      *
1242      * @access private
1243      */
1244     function _processText()
1245     {
1246         if (!isset($this->_text)) {
1247             return false;
1248         }
1249         $this->_tokens = array();
1250
1251         // Normalize linebreak to "\n"
1252         $this->_text = preg_replace("[\r\n]", "\n", $this->_text);
1253
1254         // Get each paragraph
1255         $paras = explode("\n",$this->_text);
1256
1257         // loop though the paragraphs
1258         // and get each word (token)
1259         foreach($paras as $para) {
1260             $words = explode(' ',$para);
1261             foreach($words as $word) {
1262                 $this->_tokens[] = $word;
1263             }
1264             // add a "\n" to mark the end of a paragraph
1265             $this->_tokens[] = "\n";
1266         }
1267         // we do not need an end paragraph as the last token
1268         array_pop($this->_tokens);
1269     }
1270 }
1271
1272