1<?php
2// $Header: /cvsroot/html2ps/box.inline.php,v 1.53 2007/01/24 18:55:44 Konstantin Exp $
3
4require_once(HTML2PS_DIR.'encoding.inc.php');
5
6define('SYMBOL_SHY', code_to_utf8(0xAD));
7define('BROKEN_SYMBOL', chr(0xC2));
8
9class LineBox {
10  var $top;
11  var $right;
12  var $bottom;
13  var $left;
14
15  function LineBox() { }
16
17  function &copy() {
18    $box =& new LineBox;
19    $box->top    = $this->top;
20    $box->right  = $this->right;
21    $box->bottom = $this->bottom;
22    $box->left   = $this->left;
23    return $box;
24  }
25
26  function offset($dx, $dy) {
27    $this->top    += $dy;
28    $this->bottom += $dy;
29    $this->left   += $dx;
30    $this->right  += $dx;
31  }
32
33  function create(&$box) {
34    $lbox = new LineBox;
35    $lbox->top    = $box->get_top();
36    $lbox->right  = $box->get_right();
37    $lbox->bottom = $box->get_bottom();
38    $lbox->left   = $box->get_left();
39
40    // $lbox->bottom = $box->get_top() - $box->get_baseline() - $box->get_descender();
41    // $lbox->top    = $box->get_top() - $box->get_baseline() + $box->get_ascender();
42    return $lbox;
43  }
44
45  function extend(&$box) {
46    $base = $box->get_top() - $box->get_baseline();
47
48    $this->top    = max($this->top,    $base + $box->get_ascender());
49    $this->right  = max($this->right,  $box->get_right());
50    $this->bottom = min($this->bottom, $base - $box->get_descender());
51
52    // Left edge of the line box should never be modified
53  }
54
55  function fake_box(&$box) {
56    // Create the fake box object
57
58    $fake_state = new CSSState(CSS::get());
59    $fake_state->pushState();
60
61    $fake = null;
62    $fake_box = new BlockBox($fake);
63    $fake_box->readCSS($fake_state);
64
65    // Setup fake box size
66    $fake_box->put_left($this->left);
67    $fake_box->put_width($this->right - $this->left);
68    $fake_box->put_top($this->top - $box->baseline);
69    $fake_box->put_height($this->top - $this->bottom);
70
71    // Setup padding value
72    $fake_box->setCSSProperty(CSS_PADDING, $box->get_css_property(CSS_PADDING));
73
74    // Setup fake box border and background
75    $fake_box->setCSSProperty(CSS_BACKGROUND, $box->get_css_property(CSS_BACKGROUND));
76    $fake_box->setCSSProperty(CSS_BORDER, $box->get_css_property(CSS_BORDER));
77
78    return $fake_box;
79  }
80}
81
82class InlineBox extends GenericInlineBox {
83  var $_lines;
84
85  function InlineBox() {
86    // Call parent's constructor
87    $this->GenericInlineBox();
88
89    // Clear the list of line boxes inside this box
90    $this->_lines = array();
91  }
92
93  function &create(&$root, &$pipeline) {
94    // Create contents of this inline box
95    if ($root->node_type() == XML_TEXT_NODE) {
96      $css_state =& $pipeline->get_current_css_state();
97      $box = InlineBox::create_from_text($root->content,
98                                         $css_state->get_property(CSS_WHITE_SPACE),
99                                         $pipeline);
100      return $box;
101    } else {
102      $box =& new InlineBox();
103
104      $css_state =& $pipeline->get_current_css_state();
105
106      $box->readCSS($css_state);
107
108      // Initialize content
109      $child = $root->first_child();
110      while ($child) {
111        $child_box =& create_pdf_box($child, $pipeline);
112        $box->add_child($child_box);
113        $child = $child->next_sibling();
114      };
115
116      // Add fake whitespace box with zero size for the anchor spans
117      // We need this, as "reflow" functions will automatically remove empty inline boxes from the
118      // document tree
119      //
120      if ($box->is_null()) {
121        $css_state->pushState();
122        $css_state->set_property(CSS_FONT_SIZE, Value::fromData(0.01, UNIT_PT));
123
124        $whitespace = WhitespaceBox::create($pipeline);
125        $whitespace->readCSS($css_state);
126
127        $box->add_child($whitespace);
128
129        $css_state->popState();
130      };
131    }
132
133    return $box;
134  }
135
136  function &create_from_text($text, $white_space, &$pipeline) {
137    $box =& new InlineBox();
138    $box->readCSS($pipeline->get_current_css_state());
139
140    // Apply/inherit text-related CSS properties
141    $css_state =& $pipeline->get_current_css_state();
142    $css_state->pushDefaultTextState();
143
144    require_once(HTML2PS_DIR.'inline.content.builder.factory.php');
145    $inline_content_builder =& InlineContentBuilderFactory::get($white_space);
146    $inline_content_builder->build($box, $text, $pipeline);
147
148    // Clear the CSS stack
149    $css_state->popState();
150
151    return $box;
152  }
153
154  function &get_line_box($index) {
155    $line_box =& $this->_lines[$index];
156    return $line_box;
157  }
158
159  function get_line_box_count() {
160    return count($this->_lines);
161  }
162
163  // Inherited from GenericFormattedBox
164
165  function process_word($raw_content, &$pipeline) {
166    if ($raw_content === '') {
167      return false;
168    }
169
170    $ptr      = 0;
171    $word     = '';
172    $hyphens  = array();
173    $encoding = 'iso-8859-1';
174
175    $manager_encoding =& ManagerEncoding::get();
176    $text_box =& TextBox::create_empty($pipeline);
177
178    $len = strlen($raw_content);
179    while ($ptr < $len) {
180      $char = $manager_encoding->get_next_utf8_char($raw_content, $ptr);
181
182      // Check if current  char is a soft hyphen  character. It it is,
183      // remove it from the word  (as it should not be drawn normally)
184      // and store its location
185      if ($char == SYMBOL_SHY) {
186        $hyphens[] = strlen($word);
187      } else {
188        $mapping = $manager_encoding->get_mapping($char);
189
190        /**
191         * If this character is not found in predefined encoding vectors,
192         * we'll use "Custom" encoding and add single-character TextBox
193         *
194         * @TODO: handle characters without known glyph names
195         */
196        if (is_null($mapping)) {
197          /**
198           * No mapping to default encoding vectors found for this character
199           */
200
201          /**
202           * Add last word
203           */
204          if ($word !== '') {
205            $text_box->add_subword($word, $encoding, $hyphens);
206          };
207
208          /**
209           * Add current symbol
210           */
211          $custom_char = $manager_encoding->add_custom_char(utf8_to_code($char));
212          $text_box->add_subword($custom_char, $manager_encoding->get_current_custom_encoding_name(), $hyphens);
213
214          $word = '';
215        } else {
216          if (isset($mapping[$encoding])) {
217            $word .= $mapping[$encoding];
218          } else {
219            // This condition prevents empty text boxes from appearing; say, if word starts with a national
220            // character, an () - text box with no letters will be generated, in rare case causing a random line
221            // wraps, if container is narrow
222            if ($word !== '') {
223              $text_box->add_subword($word, $encoding, $hyphens);
224            };
225
226            reset($mapping);
227            list($encoding, $add) = each($mapping);
228
229            $word = $mapping[$encoding];
230            $hyphens = array();
231          };
232        };
233      };
234    };
235
236    if ($word !== '') {
237      $text_box->add_subword($word, $encoding, $hyphens);
238    };
239
240    $this->add_child($text_box);
241    return true;
242  }
243
244  function show(&$driver) {
245    if ($this->get_css_property(CSS_POSITION) == POSITION_RELATIVE) {
246      // Postpone
247      return true;
248    };
249
250    return $this->_show($driver);
251  }
252
253  function show_postponed(&$driver) {
254    return $this->_show($driver);
255  }
256
257  function _show(&$driver) {
258    // Show line boxes background and borders
259    $size = $this->get_line_box_count();
260    for ($i=0; $i<$size; $i++) {
261      $line_box = $this->get_line_box($i);
262      $fake_box = $line_box->fake_box($this);
263
264      $background = $this->get_css_property(CSS_BACKGROUND);
265      $border     = $this->get_css_property(CSS_BORDER);
266
267      $background->show($driver, $fake_box);
268      $border->show($driver, $fake_box);
269    };
270
271    // Show content
272    $size = count($this->content);
273    for ($i=0; $i < $size; $i++) {
274      if (is_null($this->content[$i]->show($driver))) {
275        return null;
276      };
277    }
278
279    return true;
280  }
281
282  // Initialize next line box inside this inline
283  //
284  // Adds the next element to _lines array inside the current object and initializes it with the
285  // $box parameters
286  //
287  // @param $box child box which will be first in this line box
288  // @param $line_no number of line box
289  //
290  function init_line(&$box, &$line_no) {
291    $line_box = LineBox::create($box);
292    $this->_lines[$line_no] = $line_box;
293  }
294
295  // Extends the existing line box to include the given child
296  // OR starts new line box, if current child is to the left of the box right edge
297  // (which should not happen white the line box is filled)
298  //
299  // @param $box child box which will be first in this line box
300  // @param $line_no number of line box
301  //
302  function extend_line(&$box, $line_no) {
303    if (!isset($this->_lines[$line_no])) {
304      // New line box started
305      $this->init_line($box, $line_no);
306
307      return $line_no;
308    };
309
310    // Check if this box starts a new line
311    if ($box->get_left() < $this->_lines[$line_no]->right) {
312      $line_no++;
313      $this->init_line($box, $line_no);
314      return $line_no;
315    };
316
317    $this->_lines[$line_no]->extend($box);
318
319    return $line_no;
320  }
321
322  function merge_line(&$box, $line_no) {
323    $start_line = 0;
324
325    if ($line_no > 0 && count($box->_lines) > 0) {
326      if ($this->_lines[$line_no-1]->right + EPSILON > $box->_lines[0]->left) {
327        $this->_lines[$line_no-1]->right  = max($box->_lines[0]->right,  $this->_lines[$line_no-1]->right);
328        $this->_lines[$line_no-1]->top    = max($box->_lines[0]->top,    $this->_lines[$line_no-1]->top);
329        $this->_lines[$line_no-1]->bottom = min($box->_lines[0]->bottom, $this->_lines[$line_no-1]->bottom);
330        $start_line = 1;
331      };
332    };
333
334    $size = count($box->_lines);
335    for ($i=$start_line; $i<$size; $i++) {
336      $this->_lines[] = $box->_lines[$i]->copy();
337    };
338
339    return count($this->_lines);
340  }
341
342  function reflow_static(&$parent, &$context) {
343    GenericFormattedBox::reflow($parent, $context);
344
345    // Note that inline boxes (actually SPANS)
346    // are never added to the parent's line boxes
347
348    // Move current box to the parent's current coordinates
349    // Note that span box will start at the far left of the parent, NOT on its current X!
350    // Also, note that inline box can have margins, padding and borders!
351
352    $this->put_left($parent->get_left());
353    $this->put_top($parent->get_top() - $this->get_extra_top());
354
355    // first line of the SPAN will be offset to its parent current-x
356    // PLUS the left padding of current span!
357    $parent->_current_x += $this->get_extra_left();
358    $this->_current_x = $parent->_current_x;
359
360    // Note that the same operation IS NOT applied to parent current-y!
361    // The padding space is just extended to the top possibly OVERLAPPING the above boxes.
362
363    $this->width = 0;
364
365    // Reflow contents
366    $size = count($this->content);
367    for ($i=0; $i<$size; $i++) {
368      $child =& $this->content[$i];
369
370      // Add current element into _parent_ line box and reflow it
371      $child->reflow($parent, $context);
372
373      // In general, if inline box centained whitespace box only,
374      // it could be removed during reflow function call;
375      // let's check it and skip to next child
376      //
377      // if no children left AT ALL (so this box is empty), just exit
378
379      // Track the real height of the inline box; it will be used by other functions
380      // (say, functions calculating content height)
381
382      $this->extend_height($child->get_bottom_margin());
383    };
384
385    // Apply right extra space value (padding + border + margin)
386    $parent->_current_x += $this->get_extra_right();
387
388    // Margins of inline boxes are not collapsed
389
390    if ($this->get_first_data()) {
391      $context->pop_collapsed_margin();
392      $context->push_collapsed_margin( 0 );
393    };
394  }
395
396  function reflow_inline() {
397    $line_no = 0;
398
399    $size = count($this->content);
400    for ($i=0; $i<$size; $i++) {
401      $child =& $this->content[$i];
402      $child->reflow_inline();
403
404      if (!$child->is_null()) {
405        if (is_a($child,'InlineBox')) {
406          $line_no = $this->merge_line($child, $line_no);
407        } else {
408          $line_no = $this->extend_line($child, $line_no);
409        };
410      };
411    };
412  }
413
414  function reflow_whitespace(&$linebox_started, &$previous_whitespace) {
415    /**
416     * Anchors could have no content at all (like <a name="test"></a>).
417     * We should not remove such anchors, as this will break internal links
418     * in the document.
419     */
420    $dest = $this->get_css_property(CSS_HTML2PS_LINK_DESTINATION);
421    if (!is_null($dest)) {
422      return;
423    };
424
425    $size = count($this->content);
426    for ($i=0; $i<$size; $i++) {
427      $child =& $this->content[$i];
428      $child->reflow_whitespace($linebox_started, $previous_whitespace);
429    };
430
431    if ($this->is_null()) {
432      $this->parent->remove($this);
433    };
434  }
435
436  function get_extra_line_left() {
437    return $this->get_extra_left() + ($this->parent ? $this->parent->get_extra_line_left() : 0);
438  }
439
440  function get_extra_line_right() {
441    return $this->get_extra_right() + ($this->parent ? $this->parent->get_extra_line_right() : 0);
442  }
443
444  /**
445   * As "nowrap" properties applied to block-level boxes only, we may use simplified version of
446   * 'get_min_width' here
447   */
448  function get_min_width(&$context) {
449    if (isset($this->_cache[CACHE_MIN_WIDTH])) {
450      return $this->_cache[CACHE_MIN_WIDTH];
451    }
452
453    $content_size = count($this->content);
454
455    /**
456     * If box does not have any content, its minimal width is determined by extra horizontal space
457     */
458    if ($content_size == 0) {
459      return $this->_get_hor_extra();
460    };
461
462    $minw = $this->content[0]->get_min_width($context);
463
464    for ($i=1; $i<$content_size; $i++) {
465      $item = $this->content[$i];
466      if (!$item->out_of_flow()) {
467        $minw = max($minw, $item->get_min_width($context));
468      };
469    }
470
471    // Apply width constraint to min width. Return maximal value
472    $wc = $this->get_css_property(CSS_WIDTH);
473    $min_width = max($minw, $wc->apply($minw, $this->parent->get_width())) + $this->_get_hor_extra();
474
475    $this->_cache[CACHE_MIN_WIDTH] = $min_width;
476    return $min_width;
477  }
478
479  // Restore default behaviour, as this class is a ContainerBox descendant
480  function get_max_width_natural(&$context, $limit=10E6) {
481    return $this->get_max_width($context, $limit);
482  }
483
484  function offset($dx, $dy) {
485    $size = count($this->_lines);
486    for ($i=0; $i<$size; $i++) {
487      $this->_lines[$i]->offset($dx, $dy);
488    };
489    GenericInlineBox::offset($dx, $dy);
490  }
491
492  /**
493   * Deprecated
494   */
495  function getLineBoxCount() {
496    return $this->get_line_box_count();
497  }
498
499  function &getLineBox($index) {
500    return $this->get_line_box($index);
501  }
502};
503
504?>