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?>