Text.php 38 KB

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