1<?php 2// $Header: /cvsroot/html2ps/box.text.php,v 1.56 2007/05/07 12:15:53 Konstantin Exp $ 3 4require_once(HTML2PS_DIR.'box.inline.simple.php'); 5 6// TODO: from my POV, it wll be better to pass the font- or CSS-controlling object to the constructor 7// instead of using globally visible functions in 'show'. 8 9class TextBox extends SimpleInlineBox { 10 var $words; 11 var $encodings; 12 var $hyphens; 13 var $_widths; 14 var $_word_widths; 15 var $_wrappable; 16 var $wrapped; 17 18 function TextBox() { 19 $this->SimpleInlineBox(); 20 21 $this->words = array(); 22 $this->encodings = array(); 23 $this->hyphens = array(); 24 $this->_word_widths = array(); 25 $this->_wrappable = array(); 26 $this->wrapped = null; 27 $this->_widths = array(); 28 29 $this->font_size = 0; 30 $this->ascender = 0; 31 $this->descender = 0; 32 $this->width = 0; 33 $this->height = 0; 34 } 35 36 /** 37 * Check if given subword contains soft hyphens and calculate 38 */ 39 function _make_wrappable(&$driver, $base_width, $font_name, $font_size, $subword_index) { 40 $hyphens = $this->hyphens[$subword_index]; 41 $wrappable = array(); 42 43 foreach ($hyphens as $hyphen) { 44 $subword_wrappable_index = $hyphen; 45 $subword_wrappable_width = $base_width + $driver->stringwidth(substr($this->words[$subword_index], 0, $subword_wrappable_index), 46 $font_name, 47 $this->encodings[$subword_index], 48 $font_size); 49 $subword_full_width = $subword_wrappable_width + $driver->stringwidth('-', 50 $font_name, 51 "iso-8859-1", 52 $font_size); 53 54 $wrappable[] = array($subword_index, $subword_wrappable_index, $subword_wrappable_width, $subword_full_width); 55 }; 56 return $wrappable; 57 } 58 59 function get_content() { 60 return join('', array_map(array($this, 'get_content_callback'), $this->words, $this->encodings)); 61 } 62 63 function get_content_callback($word, $encoding) { 64 $manager_encoding =& ManagerEncoding::get(); 65 return $manager_encoding->to_utf8($word, $encoding); 66 } 67 68 function get_height() { 69 return $this->height; 70 } 71 72 function put_height($value) { 73 $this->height = $value; 74 } 75 76 // Apply 'line-height' CSS property; modifies the default_baseline value 77 // (NOT baseline, as it is calculated - and is overwritten - in the close_line 78 // method of container box 79 // 80 // Note that underline position (or 'descender' in terms of PDFLIB) - 81 // so, simple that space of text box under the baseline - is scaled too 82 // when 'line-height' is applied 83 // 84 function _apply_line_height() { 85 $height = $this->get_height(); 86 $under = $height - $this->default_baseline; 87 88 $line_height = $this->get_css_property(CSS_LINE_HEIGHT); 89 90 if ($height > 0) { 91 $scale = $line_height->apply($this->ascender + $this->descender) / ($this->ascender + $this->descender); 92 } else { 93 $scale = 0; 94 }; 95 96 // Calculate the height delta of the text box 97 98 $delta = $height * ($scale-1); 99 $this->put_height(($this->ascender + $this->descender)*$scale); 100 $this->default_baseline = $this->default_baseline + $delta/2; 101 } 102 103 function _get_font_name(&$viewport, $subword_index) { 104 if (isset($this->_cache[CACHE_TYPEFACE][$subword_index])) { 105 return $this->_cache[CACHE_TYPEFACE][$subword_index]; 106 }; 107 108 $font_resolver =& $viewport->get_font_resolver(); 109 110 $font = $this->get_css_property(CSS_FONT); 111 112 $typeface = $font_resolver->get_typeface_name($font->family, 113 $font->weight, 114 $font->style, 115 $this->encodings[$subword_index]); 116 117 $this->_cache[CACHE_TYPEFACE][$subword_index] = $typeface; 118 119 return $typeface; 120 } 121 122 function add_subword($raw_subword, $encoding, $hyphens) { 123 $text_transform = $this->get_css_property(CSS_TEXT_TRANSFORM); 124 switch ($text_transform) { 125 case CSS_TEXT_TRANSFORM_CAPITALIZE: 126 $subword = ucwords($raw_subword); 127 break; 128 case CSS_TEXT_TRANSFORM_UPPERCASE: 129 $subword = strtoupper($raw_subword); 130 break; 131 case CSS_TEXT_TRANSFORM_LOWERCASE: 132 $subword = strtolower($raw_subword); 133 break; 134 case CSS_TEXT_TRANSFORM_NONE: 135 $subword = $raw_subword; 136 break; 137 } 138 139 $this->words[] = $subword; 140 $this->encodings[] = $encoding; 141 $this->hyphens[] = $hyphens; 142 } 143 144 function &create($text, $encoding, &$pipeline) { 145 $box =& TextBox::create_empty($pipeline); 146 $box->add_subword($text, $encoding, array()); 147 return $box; 148 } 149 150 function &create_empty(&$pipeline) { 151 $box =& new TextBox(); 152 $css_state = $pipeline->get_current_css_state(); 153 154 $box->readCSS($css_state); 155 $css_state = $pipeline->get_current_css_state(); 156 157 return $box; 158 } 159 160 function readCSS(&$state) { 161 parent::readCSS($state); 162 163 $this->_readCSSLengths($state, 164 array(CSS_TEXT_INDENT, 165 CSS_LETTER_SPACING)); 166 } 167 168 // Inherited from GenericFormattedBox 169 function get_descender() { 170 return $this->descender; 171 } 172 173 function get_ascender() { 174 return $this->ascender; 175 } 176 177 function get_baseline() { 178 return $this->baseline; 179 } 180 181 function get_min_width_natural(&$context) { 182 return $this->get_full_width(); 183 } 184 185 function get_min_width(&$context) { 186 return $this->get_full_width(); 187 } 188 189 function get_max_width(&$context) { 190 return $this->get_full_width(); 191 } 192 193 // Checks if current inline box should cause a line break inside the parent box 194 // 195 // @param $parent reference to a parent box 196 // @param $content flow context 197 // @return true if line break occurred; false otherwise 198 // 199 function maybe_line_break(&$parent, &$context) { 200 if (!$parent->line_break_allowed()) { 201 return false; 202 }; 203 204 $last =& $parent->last_in_line(); 205 if ($last) { 206 // Check if last box was a note call box. Punctuation marks 207 // after a note-call box should not be wrapped to new line, 208 // while "plain" words may be wrapped. 209 if ($last->is_note_call() && $this->is_punctuation()) { 210 return false; 211 }; 212 }; 213 214 // Calculate the x-coordinate of this box right edge 215 $right_x = $this->get_full_width() + $parent->_current_x; 216 217 $need_break = false; 218 219 // Check for right-floating boxes 220 // If upper-right corner of this inline box is inside of some float, wrap the line 221 $float = $context->point_in_floats($right_x, $parent->_current_y); 222 if ($float) { 223 $need_break = true; 224 }; 225 226 // No floats; check if we had run out the right edge of container 227 // TODO: nobr-before, nobr-after 228 if (($right_x > $parent->get_right()+EPSILON)) { 229 // Now check if parent line box contains any other boxes; 230 // if not, we should draw this box unless we have a floating box to the left 231 232 $first = $parent->get_first(); 233 234 $ti = $this->get_css_property(CSS_TEXT_INDENT); 235 $indent_offset = $ti->calculate($parent); 236 237 if ($parent->_current_x > $parent->get_left() + $indent_offset + EPSILON) { 238 $need_break = true; 239 }; 240 } 241 242 // As close-line will not change the current-Y parent coordinate if no 243 // items were in the line box, we need to offset this explicitly in this case 244 // 245 if ($parent->line_box_empty() && $need_break) { 246 $parent->_current_y -= $this->get_height(); 247 }; 248 249 if ($need_break) { 250 // Check if current box contains soft hyphens and use them, breaking word into parts 251 $size = count($this->_wrappable); 252 if ($size > 0) { 253 $width_delta = $right_x - $parent->get_right(); 254 if (!is_null($float)) { 255 $width_delta = $right_x - $float->get_left_margin(); 256 }; 257 258 $this->_find_soft_hyphen($parent, $width_delta); 259 }; 260 261 $parent->close_line($context); 262 263 // Check if parent inline boxes have left padding/margins and add them to current_x 264 $element = $this->parent; 265 while (!is_null($element) && is_a($element,"GenericInlineBox")) { 266 $parent->_current_x += $element->get_extra_left(); 267 $element = $element->parent; 268 }; 269 }; 270 271 return $need_break; 272 } 273 274 function _find_soft_hyphen(&$parent, $width_delta) { 275 /** 276 * Now we search for soft hyphen closest to the right margin 277 */ 278 $size = count($this->_wrappable); 279 for ($i=$size-1; $i>=0; $i--) { 280 $wrappable = $this->_wrappable[$i]; 281 if ($this->get_width() - $wrappable[3] > $width_delta) { 282 $this->save_wrapped($wrappable, $parent, $context); 283 $parent->append_line($this); 284 return; 285 }; 286 }; 287 } 288 289 function save_wrapped($wrappable, &$parent, &$context) { 290 $this->wrapped = array($wrappable, 291 $parent->_current_x + $this->get_extra_left(), 292 $parent->_current_y - $this->get_extra_top()); 293 } 294 295 function reflow(&$parent, &$context) { 296 // Check if we need a line break here (possilble several times in a row, if we 297 // have a long word and a floating box intersecting with this word 298 // 299 // To prevent infinite loop, we'll use a limit of 100 sequental line feeds 300 $i=0; 301 302 do { $i++; } while ($this->maybe_line_break($parent, $context) && $i < 100); 303 304 // Determine the baseline position and height of the text-box using line-height CSS property 305 $this->_apply_line_height(); 306 307 // set default baseline 308 $this->baseline = $this->default_baseline; 309 310 // append current box to parent line box 311 $parent->append_line($this); 312 313 // Determine coordinates of upper-left _margin_ corner 314 $this->guess_corner($parent); 315 316 // Offset parent current X coordinate 317 if (!is_null($this->wrapped)) { 318 $parent->_current_x += $this->get_full_width() - $this->wrapped[0][2]; 319 } else { 320 $parent->_current_x += $this->get_full_width(); 321 }; 322 323 // Extends parents height 324 $parent->extend_height($this->get_bottom()); 325 326 // Update the value of current collapsed margin; pure text (non-span) 327 // boxes always have zero margin 328 329 $context->pop_collapsed_margin(); 330 $context->push_collapsed_margin( 0 ); 331 } 332 333 function getWrappedWidthAndHyphen() { 334 return $this->wrapped[0][3]; 335 } 336 337 function getWrappedWidth() { 338 return $this->wrapped[0][2]; 339 } 340 341 function reflow_text(&$driver) { 342 $num_words = count($this->words); 343 344 /** 345 * Empty text box 346 */ 347 if ($num_words == 0) { 348 return true; 349 }; 350 351 /** 352 * A simple assumption is made: fonts used for different encodings 353 * have equal ascender/descender values (while they have the same 354 * typeface, style and weight). 355 */ 356 $font_name = $this->_get_font_name($driver, 0); 357 358 /** 359 * Get font vertical metrics 360 */ 361 $ascender = $driver->font_ascender($font_name, $this->encodings[0]); 362 if (is_null($ascender)) { 363 error_log("TextBox::reflow_text: cannot get font ascender"); 364 return null; 365 }; 366 367 $descender = $driver->font_descender($font_name, $this->encodings[0]); 368 if (is_null($descender)) { 369 error_log("TextBox::reflow_text: cannot get font descender"); 370 return null; 371 }; 372 373 /** 374 * Setup box size 375 */ 376 $font = $this->get_css_property(CSS_FONT_SIZE); 377 $font_size = $font->getPoints(); 378 379 // Both ascender and descender should make $font_size 380 // as it is not guaranteed that $ascender + $descender == 1, 381 // we should normalize the result 382 $koeff = $font_size / ($ascender + $descender); 383 $this->ascender = $ascender * $koeff; 384 $this->descender = $descender * $koeff; 385 386 $this->default_baseline = $this->ascender; 387 $this->height = $this->ascender + $this->descender; 388 389 /** 390 * Determine box width 391 */ 392 if ($font_size > 0) { 393 $width = 0; 394 395 for ($i=0; $i<$num_words; $i++) { 396 $font_name = $this->_get_font_name($driver, $i); 397 398 $current_width = $driver->stringwidth($this->words[$i], 399 $font_name, 400 $this->encodings[$i], 401 $font_size); 402 $this->_word_widths[] = $current_width; 403 404 // Add information about soft hyphens 405 $this->_wrappable = array_merge($this->_wrappable, $this->_make_wrappable($driver, $width, $font_name, $font_size, $i)); 406 407 $width += $current_width; 408 }; 409 410 $this->width = $width; 411 } else { 412 $this->width = 0; 413 }; 414 415 $letter_spacing = $this->get_css_property(CSS_LETTER_SPACING); 416 417 if ($letter_spacing->getPoints() != 0) { 418 $this->_widths = array(); 419 420 for ($i=0; $i<$num_words; $i++) { 421 $num_chars = strlen($this->words[$i]); 422 423 for ($j=0; $j<$num_chars; $j++) { 424 $this->_widths[] = $driver->stringwidth($this->words[$i]{$j}, 425 $font_name, 426 $this->encodings[$i], 427 $font_size); 428 }; 429 430 $this->width += $letter_spacing->getPoints()*$num_chars; 431 }; 432 }; 433 434 return true; 435 } 436 437 function show(&$driver) { 438 /** 439 * Check if font-size have been set to 0; in this case we should not draw this box at all 440 */ 441 $font_size = $this->get_css_property(CSS_FONT_SIZE); 442 if ($font_size->getPoints() == 0) { 443 return true; 444 } 445 446 // Check if current text box will be cut-off by the page edge 447 // Get Y coordinate of the top edge of the box 448 $top = $this->get_top_margin(); 449 // Get Y coordinate of the bottom edge of the box 450 $bottom = $this->get_bottom_margin(); 451 452 $top_inside = $top >= $driver->getPageBottom()-EPSILON; 453 $bottom_inside = $bottom >= $driver->getPageBottom()-EPSILON; 454 455 if (!$top_inside && !$bottom_inside) { 456 return true; 457 } 458 459 return $this->_showText($driver); 460 } 461 462 function _showText(&$driver) { 463 if (!is_null($this->wrapped)) { 464 return $this->_showTextWrapped($driver); 465 } else { 466 return $this->_showTextNormal($driver); 467 }; 468 } 469 470 function _showTextWrapped(&$driver) { 471 // draw generic box 472 parent::show($driver); 473 474 $font_size = $this->get_css_property(CSS_FONT_SIZE); 475 476 $decoration = $this->get_css_property(CSS_TEXT_DECORATION); 477 478 // draw text decoration 479 $driver->decoration($decoration['U'], 480 $decoration['O'], 481 $decoration['T']); 482 483 $letter_spacing = $this->get_css_property(CSS_LETTER_SPACING); 484 485 // Output text with the selected font 486 // note that we're using $default_baseline; 487 // the alignment offset - the difference between baseline and default_baseline values 488 // is taken into account inside the get_top/get_bottom functions 489 // 490 $current_char = 0; 491 492 $left = $this->wrapped[1]; 493 $top = $this->get_top() - $this->default_baseline; 494 $num_words = count($this->words); 495 496 /** 497 * First part of wrapped word (before hyphen) 498 */ 499 for ($i=0; $i<$this->wrapped[0][0]; $i++) { 500 // Activate font 501 $status = $driver->setfont($this->_get_font_name($driver, $i), 502 $this->encodings[$i], 503 $font_size->getPoints()); 504 if (is_null($status)) { 505 error_log("TextBox::show: setfont call failed"); 506 return null; 507 }; 508 509 $driver->show_xy($this->words[$i], 510 $left, 511 $this->wrapped[2] - $this->default_baseline); 512 $left += $this->_word_widths[$i]; 513 }; 514 515 $index = $this->wrapped[0][0]; 516 517 $status = $driver->setfont($this->_get_font_name($driver, $index), 518 $this->encodings[$index], 519 $font_size->getPoints()); 520 if (is_null($status)) { 521 error_log("TextBox::show: setfont call failed"); 522 return null; 523 }; 524 525 $driver->show_xy(substr($this->words[$index],0,$this->wrapped[0][1])."-", 526 $left, 527 $this->wrapped[2] - $this->default_baseline); 528 529 /** 530 * Second part of wrapped word (after hyphen) 531 */ 532 533 $left = $this->get_left(); 534 $top = $this->get_top(); 535 $driver->show_xy(substr($this->words[$index],$this->wrapped[0][1]), 536 $left, 537 $top - $this->default_baseline); 538 539 $size = count($this->words); 540 for ($i = $this->wrapped[0][0]+1; $i<$size; $i++) { 541 // Activate font 542 $status = $driver->setfont($this->_get_font_name($driver, $i), 543 $this->encodings[$i], 544 $font_size->getPoints()); 545 if (is_null($status)) { 546 error_log("TextBox::show: setfont call failed"); 547 return null; 548 }; 549 550 $driver->show_xy($this->words[$i], 551 $left, 552 $top - $this->default_baseline); 553 554 $left += $this->_word_widths[$i]; 555 }; 556 557 return true; 558 } 559 560 function _showTextNormal(&$driver) { 561 // draw generic box 562 parent::show($driver); 563 564 $font_size = $this->get_css_property(CSS_FONT_SIZE); 565 566 $decoration = $this->get_css_property(CSS_TEXT_DECORATION); 567 568 // draw text decoration 569 $driver->decoration($decoration['U'], 570 $decoration['O'], 571 $decoration['T']); 572 573 $letter_spacing = $this->get_css_property(CSS_LETTER_SPACING); 574 575 if ($letter_spacing->getPoints() == 0) { 576 // Output text with the selected font 577 // note that we're using $default_baseline; 578 // the alignment offset - the difference between baseline and default_baseline values 579 // is taken into account inside the get_top/get_bottom functions 580 // 581 $size = count($this->words); 582 $left = $this->get_left(); 583 584 for ($i=0; $i<$size; $i++) { 585 // Activate font 586 $status = $driver->setfont($this->_get_font_name($driver, $i), 587 $this->encodings[$i], 588 $font_size->getPoints()); 589 if (is_null($status)) { 590 error_log("TextBox::show: setfont call failed"); 591 return null; 592 }; 593 594 $driver->show_xy($this->words[$i], 595 $left, 596 $this->get_top() - $this->default_baseline); 597 598 $left += $this->_word_widths[$i]; 599 }; 600 } else { 601 $current_char = 0; 602 603 $left = $this->get_left(); 604 $top = $this->get_top() - $this->default_baseline; 605 $num_words = count($this->words); 606 607 for ($i=0; $i<$num_words; $i++) { 608 $num_chars = strlen($this->words[$i]); 609 610 for ($j=0; $j<$num_chars; $j++) { 611 $status = $driver->setfont($this->_get_font_name($driver, $i), 612 $this->encodings[$i], 613 $font_size->getPoints()); 614 615 $driver->show_xy($this->words[$i]{$j}, $left, $top); 616 $left += $this->_widths[$current_char] + $letter_spacing->getPoints(); 617 $current_char++; 618 }; 619 }; 620 }; 621 622 return true; 623 } 624 625 function show_fixed(&$driver) { 626 $font_size = $this->get_css_property(CSS_FONT_SIZE); 627 628 // Check if font-size have been set to 0; in this case we should not draw this box at all 629 if ($font_size->getPoints() == 0) { 630 return true; 631 } 632 633 return $this->_showText($driver); 634 } 635 636 function offset($dx, $dy) { 637 parent::offset($dx, $dy); 638 639 // Note that horizonal offset should be called explicitly from text-align routines 640 // otherwise wrapped part will be offset twice (as offset is called both for 641 // wrapped and non-wrapped parts). 642 if (!is_null($this->wrapped)) { 643 $this->offset_wrapped($dx, $dy); 644 }; 645 } 646 647 function offset_wrapped($dx, $dy) { 648 $this->wrapped[1] += $dx; 649 $this->wrapped[2] += $dy; 650 } 651 652 function reflow_whitespace(&$linebox_started, &$previous_whitespace) { 653 $linebox_started = true; 654 $previous_whitespace = false; 655 return; 656 } 657 658 function is_null() { return false; } 659} 660?>