Text.php 38 KB

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