1<?php
2// $Header: /cvsroot/html2ps/box.table.php,v 1.59 2007/04/01 12:11:24 Konstantin Exp $
3
4class CellSpan {
5  var $row;
6  var $column;
7  var $size;
8}
9
10/**
11 * It is assumed that every row contains at least one cell
12 */
13class TableBox extends GenericContainerBox {
14  var $cwc;
15  var $_cached_min_widths;
16
17  function TableBox() {
18    $this->GenericContainerBox();
19
20    // List of column width constraints
21    $this->cwc = array();
22
23    $this->_cached_min_widths = null;
24  }
25
26  function readCSS(&$state) {
27    parent::readCSS($state);
28
29    $this->_readCSS($state,
30                    array(CSS_BORDER_COLLAPSE,
31                          CSS_TABLE_LAYOUT));
32
33    $this->_readCSSLengths($state,
34                           array(CSS_HTML2PS_CELLPADDING,
35                                 CSS_HTML2PS_CELLSPACING));
36  }
37
38  function &cell($r, $c) {
39    return $this->content[$r]->content[$c];
40  }
41
42  function rows_count() {
43    return count($this->content);
44  }
45
46  // NOTE: assumes that rows are already normalized!
47  function cols_count() {
48    return count($this->content[0]->content);
49  }
50
51  // FIXME: just a stub
52  function append_line(&$e) {}
53
54  function &create(&$root, &$pipeline) {
55    $box =& new TableBox();
56    $box->readCSS($pipeline->get_current_css_state());
57
58    // This row should not inherit any table specific properties!
59    // 'overflow' for example
60    //
61    $css_state =& $pipeline->get_current_css_state();
62    $css_state->pushDefaultState();
63
64    $row =& new TableRowBox($root);
65    $row->readCSS($css_state);
66
67    $box->add_child($row);
68
69    $css_state->popState();
70
71    // Setup cellspacing / cellpadding values
72    if ($box->get_css_property(CSS_BORDER_COLLAPSE) == BORDER_COLLAPSE) {
73      $handler =& CSS::get_handler(CSS_PADDING);
74      $box->setCSSProperty(CSS_PADDING, $handler->default_value());
75    };
76
77    // Set text-align to 'left'; all browsers I've ever seen prevent inheriting of
78    // 'text-align' property by the tables.
79    // Say, in the following example the text inside the table cell will be aligned left,
80    // instead of inheriting 'center' value.
81    //
82    // <div style="text-align: center; background-color: green;">
83    // <table width="100" bgcolor="red">
84    // <tr><td>TEST
85    // </table>
86    // </div>
87    $handler =& CSS::get_handler(CSS_TEXT_ALIGN);
88    $handler->css('left', $pipeline);
89
90    // Parse table contents
91    $child = $root->first_child();
92    $col_index = 0;
93    while ($child) {
94      if ($child->node_type() === XML_ELEMENT_NODE) {
95        if ($child->tagname() === 'colgroup') {
96          // COLGROUP tags do not generate boxes; they contain information on the columns
97          //
98          $col_index = $box->parse_colgroup_tag($child, $col_index);
99        } else {
100          $child_box =& create_pdf_box($child, $pipeline);
101          $box->add_child($child_box);
102        };
103      };
104
105      $child = $child->next_sibling();
106    };
107
108    $box->normalize($pipeline);
109    $box->normalize_cwc();
110    $box->normalize_rhc();
111    $box->normalize_parent();
112
113    return $box;
114  }
115
116  // Parse the data in COL node;
117  // currently only 'width' attribute is parsed
118  //
119  // @param $root reference to a COL dom node
120  // @param $index index of column corresponding to this node
121  function parse_col(&$root, $index) {
122    if ($root->has_attribute('width')) {
123      // The value if 'width' attrubute is "multi-length";
124      // it means that it could be:
125      // 1. absolute value (10)
126      // 2. percentage value (10%)
127      // 3. relative value (3* or just *)
128      //
129
130      // TODO: support for relative values
131
132      $value = $root->get_attribute('width');
133      if (is_percentage($value)) {
134        $this->cwc[$index] = new WCFraction(((int)$value) / 100);
135      } else {
136        $this->cwc[$index] = new WCConstant(px2pt((int)$value));
137      };
138    };
139  }
140
141  // Traverse the COLGROUP node and save the column-specific information
142  //
143  // @param $root COLGROUP node
144  // @param $start_index index of the first column in this column group
145  // @return index of column after the last processed
146  //
147  function parse_colgroup_tag(&$root, $start_index) {
148    $index = $start_index;
149
150    // COLGROUP may contain zero or more COLs
151    //
152    $child = $root->first_child();
153    while ($child) {
154      if ($child->tagname() === 'col') {
155        $this->parse_col($child, $index);
156        $index ++;
157      };
158      $child = $child->next_sibling();
159    };
160
161    return $index;
162  }
163
164  function normalize_parent() {
165    for ($i=0; $i<count($this->content); $i++) {
166      $this->content[$i]->parent =& $this;
167
168      for ($j=0; $j<count($this->content[$i]->content); $j++) {
169        $this->content[$i]->content[$j]->parent =& $this;
170
171        // Set the column number for the cell to further reference
172        $this->content[$i]->content[$j]->column = $j;
173
174        // Set the column number for the cell to further reference
175        $this->content[$i]->content[$j]->row    = $i;
176      }
177    }
178  }
179
180  // Normalize row height constraints
181  //
182  // no return value
183  //
184  function normalize_rhc() {
185    // Initialize the constraint array with the empty constraints
186    $this->rhc = array();
187    for ($i=0, $size = count($this->content); $i < $size; $i++) {
188      $this->rhc[$i] = new HCConstraint(null, null, null);
189    };
190
191    // Scan all cells
192    for ($i=0, $num_rows = count($this->content); $i < $num_rows; $i++) {
193      $row =& $this->content[$i];
194
195      for ($j=0, $num_cells = count($row->content); $j < $num_cells; $j++) {
196        $cell = $row->content[$j];
197
198        // Ignore cells with rowspans
199        if ($cell->rowspan > 1) { continue; }
200
201        // Put current cell width constraint as a columns with constraint
202        $this->rhc[$i] = merge_height_constraint($this->rhc[$i], $cell->get_height_constraint());
203
204        // Now reset the cell width constraint; cell width should be affected by ceolumn constraint only
205        $hc = new HCConstraint(null, null, null);
206        $cell->put_height_constraint($hc);
207      };
208    };
209  }
210
211  // Normalize column width constraints
212  // Note that cwc array may be partially prefilled by a GOLGROUP/COL-generated constraints!
213  //
214  function normalize_cwc() {
215    // Note we've called 'normalize' method prior to 'normalize_cwc',
216    // so we already have all rows of equal length
217    //
218    for ($i=0, $num_cols = count($this->content[0]->content); $i < $num_cols; $i++) {
219      // Check if there's already COL-generated constraint for this column
220      //
221      if (!isset($this->cwc[$i])) {
222        $this->cwc[$i] = new WCNone;
223      };
224    }
225
226    // For each column (we should have table already normalized - so lengths of all rows are equal)
227    for ($i=0, $num_cols = count($this->content[0]->content); $i < $num_cols; $i++) {
228
229      // For each row
230      for ($j=0, $num_rows = count($this->content); $j < $num_rows; $j++) {
231        $cell =& $this->content[$j]->content[$i];
232
233        // Ignore cells with colspans
234        if ($cell->colspan > 1) { continue; }
235
236        // Put current cell width constraint as a columns with constraint
237        $this->cwc[$i] = merge_width_constraint($this->cwc[$i], $cell->get_css_property(CSS_WIDTH));
238
239        // Now reset the cell width constraint; cell width should be affected by ceolumn constraint only
240        $cell->setCSSProperty(CSS_WIDTH, new WCNone);
241      }
242    }
243
244    // Now fix the overconstrained columns; first of all, sum of all percentage-constrained
245    // columns should be less or equal than 100%. If sum is greater, the last column
246    // percentage is reduced in order to get 100% as a result.
247    $rest = 1;
248    for ($i=0, $num_cols = count($this->content[0]->content); $i < $num_cols; $i++) {
249      // Get current CWC
250      $wc =& $this->cwc[$i];
251
252      if ($wc->isFraction()) {
253        $wc->fraction = min($rest, $wc->fraction);
254        $rest -= $wc->fraction;
255      };
256    };
257
258    /**
259     * Now, let's process cells spanninig several columns.
260     */
261
262    /**
263     * Let's check if there's any colspanning cells filling the whole table width and
264     * containing non-100% percentage constraint
265     */
266
267    // For each row
268    for ($j=0; $j<count($this->content); $j++) {
269      /**
270       * Check if the first cell in this row satisfies the above condition
271       */
272
273      $cell =& $this->content[$j]->content[0];
274
275      /**
276       * Note that there should be '>='; '==' is not enough, as sometimes cell is declared to span
277       * more columns than there are in the table
278       */
279      $cell_wc = $cell->get_css_property(CSS_WIDTH);
280      if (!$cell->is_fake() &&
281          $cell_wc->isFraction() &&
282          $cell->colspan >= count($this->content[$j])) {
283
284        /**
285         * Clear the constraint; anyway, it should be replaced with 100% in this case, as
286         * this cell is the only cell in the row
287         */
288
289        $wc = new WCNone;
290        $cell->setCSSProperty(CSS_WIDTH, $wc);
291      };
292    };
293  }
294
295  /**
296   * Normalize table by adding fake cells for colspans and rowspans
297   * Also, if there is any empty rows (without cells), add at least one fake cell
298   */
299  function normalize(&$pipeline) {
300    /**
301     * Fix empty rows by adding a fake cell
302     */
303    for ($i=0; $i<count($this->content); $i++) {
304      $row =& $this->content[$i];
305      if (count($row->content) == 0) {
306        $this->content[$i]->add_fake_cell_before(0, $pipeline);
307      };
308    };
309
310    /**
311     * first, normalize colspans
312     */
313    for ($i=0; $i<count($this->content); $i++) {
314      $this->content[$i]->normalize($pipeline);
315    };
316
317    /**
318     * second, normalize rowspans
319     *
320     * We should scan table column-by-column searching for row-spanned cells;
321     * consider the following example:
322     *
323     * <table>
324     * <tr>
325     * <td>A1</td>
326     * <td rowspan="3">B1</td>
327     * <td>C1</td>
328     * </tr>
329     *
330     * <tr>
331     * <td rowspan="2">A2</td>
332     * <td>C2</td>
333     * </tr>
334     *
335     * <tr>
336     * <td>C3</td>
337     * </tr>
338     * </table>
339     */
340
341    $i_col = 0;
342    do {
343      $flag = false;
344      for ($i_row=0; $i_row<count($this->content); $i_row++) {
345        $row =& $this->content[$i_row];
346        if ($i_col < count($row->content)) {
347          $flag = true;
348
349          // Check if this rowspan runs off the last row
350          $row->content[$i_col]->rowspan = min($row->content[$i_col]->rowspan,
351                                               count($this->content) - $i_row);
352
353          if ($row->content[$i_col]->rowspan > 1) {
354
355            // Note that min($i_row + $row->content[$i_col]->rowspan, count($this->content)) is
356            // required, as we cannot be sure that table actually contains the number
357            // of rows used in rowspan
358            //
359            for ($k=$i_row+1; $k<min($i_row + $row->content[$i_col]->rowspan, count($this->content)); $k++) {
360
361              // Note that if rowspanned cell have a colspan, we should insert SEVERAL fake cells!
362              //
363              for ($cs = 0; $cs < $row->content[$i_col]->colspan; $cs++) {
364                $this->content[$k]->add_fake_cell_before($i_col, $pipeline);
365              };
366            };
367          };
368        };
369      };
370
371      $i_col ++;
372    } while ($flag);
373
374    // third, make all rows equal in length by padding with fake-cells
375    $length = 0;
376    for ($i=0; $i<count($this->content); $i++) {
377      $length = max($length, count($this->content[$i]->content));
378    }
379    for ($i=0; $i<count($this->content); $i++) {
380      $row =& $this->content[$i];
381      while ($length > count($row->content)) {
382        $row->append_fake_cell($pipeline);
383      }
384    }
385  }
386
387  // Overrides default 'add_child' in GenericFormattedBox
388  function add_child(&$item) {
389    // Check if we're trying to add table cell to current table directly, without any table-rows
390    if ($item->isCell()) {
391      // Add cell to the last row
392      $last_row =& $this->content[count($this->content)-1];
393      $last_row->add_child($item);
394
395    } elseif ($item->isTableRow()) {
396      // If previous row is empty, remove it (get rid of automatically generated table row in constructor)
397      if (count($this->content) > 0) {
398        if (count($this->content[count($this->content)-1]->content) == 0) {
399          array_pop($this->content);
400        }
401      };
402
403      // Just add passed row
404      $this->content[] =& $item;
405    } elseif ($item->isTableSection()) {
406      // Add table section rows to current table, then drop section box
407      for ($i=0, $size = count($item->content); $i < $size; $i++) {
408        $this->add_child($item->content[$i]);
409      }
410    };
411  }
412
413  // Table-specific functions
414
415  // PREDICATES
416  function is_constrained_column($index) {
417    return !is_a($this->get_cwc($index),"wcnone");
418  }
419
420  // ROWSPANS
421  function table_have_rowspan($x,$y) {
422    return $this->content[$y]->content[$x]->rowspan;
423  }
424
425  function table_fit_rowspans($heights) {
426    $spans = $this->get_rowspans();
427
428    // Scan all cells spanning several rows
429    foreach ($spans as $span) {
430      $cell =& $this->content[$span->row]->content[$span->column];
431
432      // now check if cell height is less than sum of spanned rows heights
433      $row_heights = array_slice($heights, $span->row, $span->size);
434
435      // Vertical-align current cell
436      // calculate (approximate) row baseline
437      $baseline = $this->content[$span->row]->get_row_baseline();
438
439      // apply vertical-align
440      $vertical_align = $cell->get_css_property(CSS_VERTICAL_ALIGN);
441
442      $va_fun = CSSVerticalAlign::value2pdf($vertical_align);
443      $va_fun->apply_cell($cell, array_sum($row_heights), $baseline);
444
445      if (array_sum($row_heights) > $cell->get_full_height()) {
446        // Make cell fill all available vertical space
447        $cell->put_full_height(array_sum($row_heights));
448      };
449    }
450  }
451
452  function get_rowspans() {
453    $spans = array();
454
455    for ($i=0; $i<count($this->content); $i++) {
456      $spans = array_merge($spans, $this->content[$i]->get_rowspans($i));
457    };
458
459    return $spans;
460  }
461
462  // ROW-RELATED
463
464  /**
465   * Calculate set of row heights
466   *
467   * At the moment (*), we have a sum of total content heights of percentage constraned rows in
468   * $ch variable, and a "free" (e.g. table height - sum of all non-percentage constrained heights) height
469   * in the $h variable. Obviously, percentage-constrained rows should be expanded to fill the free space
470   *
471   * On the other size, there should be a maximal value to expand them to; for example, if sum of
472   * percentage constraints is 33%, then all these rows should fill only 1/3 of the table height,
473   * whatever the content height of other rows is. In this case, other (non-constrained) rows
474   * should be expanded to fill space left.
475   *
476   * In the latter case, if there's no non-constrained rows, the additional space should be filled by
477   * "plain" rows without any constraints
478   *
479   * @param $minheight the minimal allowed height of the row; as we'll need to expand rows later
480   * and rows containing totally empty cells will have zero height
481   * @return array of row heights in media points
482   */
483  function _row_heights($minheight) {
484    $heights = array();
485    $cheights = array();
486    $height = $this->get_height();
487
488    // Calculate "content" and "constrained" heights of table rows
489
490    for ($i=0; $i<count($this->content); $i++) {
491      $heights[] = max($minheight, $this->content[$i]->row_height());
492
493      // Apply row height constraint
494      // we need to specify box which parent will serve as a base for height calculation;
495
496      $hc = $this->get_rhc($i);
497      $cheights[] = $hc->apply($heights[$i], $this->content[$i], null);
498    };
499
500    // Collapse "constrained" heights of percentage-constrained rows, if they're
501    // taking more that available space
502
503    $flags = $this->get_non_percentage_constrained_height_flags();
504    $h = $height;
505    $ch = 0;
506    for ($i=0; $i<count($heights); $i++) {
507      if ($flags[$i]) { $h -= $cheights[$i]; } else { $ch += $cheights[$i]; };
508    };
509    // (*) see note in the function description
510    if ($ch > 0) {
511      $scale = $h / $ch;
512
513      if ($scale < 1) {
514        for ($i=0; $i<count($heights); $i++) {
515          if (!$flags[$i]) { $cheights[$i] *= $scale; };
516        };
517      };
518    };
519
520    // Expand non-constrained rows, if there's free space still
521
522    $flags = $this->get_non_constrained_height_flags();
523    $h = $height;
524    $ch = 0;
525    for ($i=0; $i<count($cheights); $i++) {
526      if (!$flags[$i]) { $h -= $cheights[$i]; } else { $ch += $cheights[$i]; };
527    };
528    // (*) see note in the function description
529    if ($ch > 0) {
530      $scale = $h / $ch;
531
532      if ($scale < 1) {
533        for ($i=0; $i<count($heights); $i++) {
534          if ($flags[$i]) { $cheights[$i] *= $scale; };
535        };
536      };
537    };
538
539    // Expand percentage-constrained rows, if there's free space still
540
541    $flags = $this->get_non_percentage_constrained_height_flags();
542    $h = $height;
543    $ch = 0;
544    for ($i=0; $i<count($cheights); $i++) {
545      if ($flags[$i]) { $h -= $cheights[$i]; } else { $ch += $cheights[$i]; };
546    };
547    // (*) see note in the function description
548    if ($ch > 0) {
549      $scale = $h / $ch;
550
551      if ($scale < 1) {
552        for ($i=0; $i<count($heights); $i++) {
553          if (!$flags[$i]) { $cheights[$i] *= $scale; };
554        };
555      };
556    };
557
558    // Get the actual row height
559    for ($i=0; $i<count($heights); $i++) {
560      $heights[$i] = max($heights[$i], $cheights[$i]);
561    };
562
563    return $heights;
564  }
565
566  function table_resize_rows(&$heights) {
567    $row_top = $this->get_top();
568
569    $size = count($heights);
570    for ($i=0; $i<$size; $i++) {
571      $this->content[$i]->table_resize_row($heights[$i], $row_top);
572      $row_top -= $heights[$i];
573    }
574
575    // Set table height to sum of row heights
576    $this->put_height(array_sum($heights));
577  }
578
579  //   // Calculate given table row height
580  //   //
581  //   // @param  $index zero-based row index
582  //   // @return value of row height (in media points)
583  //   //
584  //   function table_row_height($index) {
585  //     // Select row
586  //     $row =& $this->content[$index];
587
588  //     // Calculate height of each cell contained in this row
589  //     $height = 0;
590  //     for ($i=0; $i<count($row->content); $i++) {
591  //       if ($this->table_have_rowspan($i, $index) <= 1) {
592  //         $height = max($height, $row->content[$i]->get_full_height());
593  //       }
594  //     }
595
596  //     return $height;
597  //   }
598
599  //   function get_row_baseline($index) {
600  //     // Get current row
601  //     $row =& $this->content[$index];
602  //     // Calculate maximal baseline for each cell contained
603  //     $baseline = 0;
604  //     for ($i = 0; $i < count($row->content); $i++) {
605  //       // Cell baseline is the baseline of its first line box inside this cell
606  //       if (count($row->content[$i]->content) > 0) {
607  //         $baseline = max($baseline, $row->content[$i]->content[0]->baseline);
608  //       };
609  //     };
610  //     return $baseline;
611  //   }
612
613  // Width constraints
614  function get_cwc($col) {
615    return $this->cwc[$col];
616  }
617
618  // Get height constraint for the given row
619  //
620  // @param $row number of row (zero-based)
621  //
622  // @return HCConstraint object
623  //
624  function get_rhc($row) {
625    return $this->rhc[$row];
626  }
627
628  // Width calculation
629  //
630  // Note that if table have no width constraint AND some columns are percentage constrained,
631  // then the width of the table can be determined based on the minimal column width;
632  // e.g. if some column have minimal width of 10px and 10% width constraint,
633  // then table will have minimal width of 100px. If there's several percentage-constrained columns,
634  // then we choose from the generated values the maximal one
635  //
636  // Of course, all of the above can be applied ONLY to table without width constraint;
637  // of theres any w.c. applied to the table, it will have greater than column constraints
638  //
639  // We must take constrained table width into account; if there's a width constraint,
640  // then we must choose the maximal value between the constrained width and sum of minimal
641  // columns widths - so, expanding the constrained width in case it is not enough to fit
642  // the table contents
643  //
644  // @param $context referene to a flow context object
645  // @return minimal box width (including the padding/margin/border width! NOT content width)
646  //
647  function get_min_width(&$context) {
648    $widths = $this->get_table_columns_min_widths($context);
649    $maxw = $this->get_table_columns_max_widths($context);
650
651    // Expand some columns to fit colspanning cells
652    $widths = $this->_table_apply_colspans($widths, $context, 'get_min_width', $widths, $maxw);
653
654    $width = array_sum($widths);
655    $base_width = $width;
656
657    $wc = $this->get_css_property(CSS_WIDTH);
658    if (!$wc->isNull()) {
659      // Check if constrained table width should be expanded to fit the table contents
660      //
661      $width = max($width, $wc->apply(0, $this->parent->get_available_width($context)));
662    } else {
663      // Now check if there's any percentage column width constraints (note that
664      // if we've get here, than the table width is not constrained). Calculate
665      // the table width basing on these values and select the maximal value
666      //
667      for ($i=0; $i<$this->cols_count(); $i++) {
668        $cwc = $this->get_cwc($i);
669
670        $width = max($width,
671                     min($cwc->apply_inverse($widths[$i], $base_width),
672                         $this->parent->get_available_width($context) - $this->_get_hor_extra()));
673      };
674    };
675
676    return $width + $this->_get_hor_extra();
677  }
678
679  function get_min_width_natural(&$context) {
680    return $this->get_min_width($context);
681  }
682
683  function get_max_width(&$context) {
684    $wc = $this->get_css_property(CSS_WIDTH);
685
686    if ($wc->isConstant()) {
687      return $wc->apply(0, $this->parent->get_available_width($context));
688    } else {
689      $widths = $this->get_table_columns_max_widths($context);
690      $minwc = $this->get_table_columns_min_widths($context);
691
692      $widths = $this->_table_apply_colspans($widths, $context, 'get_max_width', $minwc, $widths);
693
694      $width = array_sum($widths);
695      $base_width = $width;
696
697      // Now check if there's any percentage column width constraints (note that
698      // if we've get here, than the table width is not constrained). Calculate
699      // the table width based on these values and select the maximal value
700      //
701      for ($i=0; $i<$this->cols_count(); $i++) {
702        $cwc = $this->get_cwc($i);
703
704        $width = max($width,
705                     min($cwc->apply_inverse($widths[$i], $base_width),
706                         $this->parent->get_available_width($context) - $this->_get_hor_extra()));
707      };
708
709      return $width + $this->_get_hor_extra();
710    }
711  }
712
713  function get_max_width_natural(&$context) {
714    return $this->get_max_width($context);
715  }
716
717  function get_width() {
718    $wc  = $this->get_css_property(CSS_WIDTH);
719    $pwc = $this->parent->get_css_property(CSS_WIDTH);
720
721    if (!$this->parent->isCell() ||
722        !$pwc->isNull() ||
723        !$wc->isFraction()) {
724      $width = $wc->apply($this->width, $this->parent->width);
725    } else {
726      $width = $this->width;
727    };
728
729    // Note that table 'padding' property for is handled differently
730    // by different browsers; for example, IE 6 ignores it completely,
731    // while FF 1.5 subtracts horizontal padding value from constrained
732    // table width. We emulate FF behavior here
733    return $width -
734      $this->get_padding_left() -
735      $this->get_padding_right();
736  }
737
738  function table_column_widths(&$context) {
739    $table_layout = $this->get_css_property(CSS_TABLE_LAYOUT);
740    switch ($table_layout) {
741    case TABLE_LAYOUT_FIXED:
742//       require_once(HTML2PS_DIR.'strategy.table.layout.fixed.php');
743//       $strategy =& new StrategyTableLayoutFixed();
744//       break;
745    case TABLE_LAYOUT_AUTO:
746    default:
747      require_once(HTML2PS_DIR.'strategy.table.layout.auto.php');
748      $strategy =& new StrategyTableLayoutAuto();
749      break;
750    };
751
752    return $strategy->apply($this, $context);
753  }
754
755  // Extend some columns widths (if needed) to fit colspanned cell contents
756  //
757  function _table_apply_colspans($widths, &$context, $width_fun, $minwc, $maxwc) {
758    $colspans = $this->get_colspans();
759
760    foreach ($colspans as $colspan) {
761      $cell = $this->content[$colspan->row]->content[$colspan->column];
762
763      // apply colspans to the corresponsing colspanned-cell dimension
764      //
765      $cell_width = $cell->$width_fun($context);
766
767      // Apply cell constraint width, if any AND if table width is constrained
768      // if table width is not constrained, we should not do this, as current value
769      // of $table->get_width is maximal width (parent width), not the actual
770      // width of the table
771      $wc = $this->get_css_property(CSS_WIDTH);
772      if (!$wc->isNull()) {
773        $cell_wc = $cell->get_css_property(CSS_WIDTH);
774        $cell_width = $cell_wc->apply($cell_width, $this->get_width());
775
776        // On the other side, constrained with cannot be less than cell minimal width
777        $cell_width = max($cell_width, $cell->get_min_width($context));
778      };
779
780      // now select the pre-calculated widths of columns covered by this cell
781      // select the list of resizable columns covered by this cell
782      $spanned_widths = array();
783      $spanned_resizable = array();
784
785      for ($i=$colspan->column; $i < $colspan->column+$colspan->size; $i++) {
786        $spanned_widths[] = $widths[$i];
787        $spanned_resizable[] = ($minwc[$i] != $maxwc[$i]);
788      }
789
790      // Sometimes we may encounter the colspan over the empty columns (I mean ALL columns are empty); in this case
791      // we need to make these columns reizable in order to fit colspanned cell contents
792      //
793      if (array_sum($spanned_widths) == 0) {
794        for ($i=0; $i<count($spanned_widths); $i++) {
795          $spanned_widths[$i] = EPSILON;
796          $spanned_resizable[$i] = true;
797        };
798      };
799
800      // The same problem may arise when all colspanned columns are not resizable; in this case we'll force all
801      // of them to be resized
802      $any_resizable = false;
803      for ($i=0; $i<count($spanned_widths); $i++) {
804        $any_resizable |= $spanned_resizable[$i];
805      };
806      if (!$any_resizable) {
807        for ($i=0; $i<count($spanned_widths); $i++) {
808          $spanned_resizable[$i] = true;
809        };
810      }
811
812      // Expand resizable columns
813      //
814      $spanned_widths = expand_to_with_flags($cell_width,$spanned_widths,$spanned_resizable);
815
816      // Store modified widths
817      array_splice($widths, $colspan->column, $colspan->size, $spanned_widths);
818    };
819
820    return $widths;
821  }
822
823  function get_table_columns_max_widths(&$context) {
824    $widths = array();
825
826    for ($i=0; $i<count($this->content[0]->content); $i++) {
827      $widths[] = 0;
828    };
829
830    for ($i=0; $i<count($this->content); $i++) {
831      // Calculate column widths for a current row
832      $roww = $this->content[$i]->get_table_columns_max_widths($context);
833      for ($j=0; $j<count($roww); $j++) {
834        //        $widths[$j] = max($roww[$j], isset($widths[$j]) ? $widths[$j] : 0);
835        $widths[$j] = max($roww[$j], $widths[$j]);
836      }
837    }
838
839    // Use column width constraints - column should not be wider its constrained width
840    for ($i=0; $i<count($widths); $i++) {
841      $cwc = $this->get_cwc($i);
842
843      // Newertheless, percentage constraints should not be applied IF table
844      // does not have constrained width
845      //
846      if (!is_a($cwc,"wcfraction")) {
847        $widths[$i] = $cwc->apply($widths[$i], $this->get_width());
848      };
849    }
850
851    // TODO: colspans
852
853    return $widths;
854  }
855
856  /**
857   * Optimization: calculated widths are cached
858   */
859  function get_table_columns_min_widths(&$context) {
860    if (!is_null($this->_cached_min_widths)) {
861      return $this->_cached_min_widths;
862    };
863
864    $widths = array();
865
866    for ($i=0; $i<count($this->content[0]->content); $i++) {
867      $widths[] = 0;
868    };
869
870    $content_size = count($this->content);
871    for ($i=0; $i<$content_size; $i++) {
872      // Calculate column widths for a current row
873      $roww = $this->content[$i]->get_table_columns_min_widths($context);
874
875      $row_size = count($roww);
876      for ($j=0; $j<$row_size; $j++) {
877        $widths[$j] = max($roww[$j], $widths[$j]);
878      }
879    }
880
881    $this->_cached_min_widths = $widths;
882    return $widths;
883  }
884
885  function get_colspans() {
886    $colspans = array();
887
888    for ($i=0; $i<count($this->content); $i++) {
889      $colspans = array_merge($colspans, $this->content[$i]->get_colspans($i));
890    };
891
892    return $colspans;
893  }
894
895  function check_constrained_colspan($col) {
896    for ($i=0; $i<$this->rows_count(); $i++) {
897      $cell =& $this->cell($i, $col);
898      $cell_wc = $cell->get_css_property(CSS_WIDTH);
899
900      if ($cell->colspan > 1 &&
901          !$cell_wc->isNull()) {
902        return true;
903      };
904    };
905    return false;
906  }
907
908  // Tries to change minimal constrained width so that columns will fit into the given
909  // table width
910  //
911  // Note that every width constraint have its own priority; first, the unconstrained columns are collapsed,
912  // then - percentage constrained and after all - columns having fixed width
913  //
914  // @param $width table width
915  // @param $minw array of unconstrained minimal widths
916  // @param $minwc array of constrained minimal widths
917  // @return list of normalized minimal constrained widths
918  //
919  function normalize_min_widths($width, $minw, $minwc) {
920    // Check if sum of constrained widths is too big
921    // Note that we compare sum of constrained width with the MAXIMAL value of table width and
922    // sum of uncostrained minimal width; it will prevent from unneeded collapsing of table cells
923    // if table content will expand its width anyway
924    //
925    $twidth = max($width, array_sum($minw));
926
927    // compare with sum of minimal constrained widths
928    //
929    if (array_sum($minwc) > $twidth) {
930      $delta = array_sum($minwc) - $twidth;
931
932      // Calculate the amount of difference between minimal and constrained minimal width for each columns
933      $diff = array();
934      for ($i=0; $i<count($minw); $i++) {
935        // Do no modify width of columns taking part in constrained colspans
936        if (!$this->check_constrained_colspan($i)) {
937          $diff[$i] = $minwc[$i] - $minw[$i];
938        } else {
939          $diff[$i] = 0;
940        };
941      }
942
943      // If no difference is found, we can collapse no columns
944      // otherwise scale some columns...
945      $cwdelta = array_sum($diff);
946
947      if ($cwdelta > 0) {
948        for ($i=0; $i<count($minw); $i++) {
949          //          $minwc[$i] = max(0,- ($minwc[$i] - $minw[$i]) / $cwdelta * $delta + $minwc[$i]);
950          $minwc[$i] = max(0, -$diff[$i] / $cwdelta * $delta + $minwc[$i]);
951        }
952      }
953    }
954
955    return $minwc;
956  }
957
958  function table_have_colspan($x, $y) {
959    return $this->content[$y]->content[$x]->colspan;
960  }
961
962  // Flow-control
963  function reflow(&$parent, &$context) {
964    if ($this->get_css_property(CSS_FLOAT) === FLOAT_NONE) {
965      $status = $this->reflow_static_normal($parent, $context);
966    } else {
967      $status = $this->reflow_static_float($parent, $context);
968    }
969
970    return $status;
971  }
972
973  function reflow_absolute(&$context) {
974    GenericFormattedBox::reflow($parent, $context);
975
976    // Calculate margin values if they have been set as a percentage
977    $this->_calc_percentage_margins($parent);
978
979    // Calculate width value if it had been set as a percentage
980    $this->_calc_percentage_width($parent, $context);
981
982    $wc = $this->get_css_property(CSS_WIDTH);
983    if (!$wc->isNull()) {
984      $col_width = $this->get_table_columns_min_widths($context);
985      $maxw      = $this->get_table_columns_max_widths($context);
986      $col_width = $this->_table_apply_colspans($col_width, $context, 'get_min_width', $col_width, $maxw);
987
988      if (array_sum($col_width) > $this->get_width()) {
989        $wc = new WCConstant(array_sum($col_width));
990      };
991    };
992
993    $position_strategy =& new StrategyPositionAbsolute();
994    $position_strategy->apply($this);
995
996    $this->reflow_content($context);
997  }
998
999  /**
1000   * TODO: unlike block elements, table unconstrained width is determined
1001   * with its content, so it may be smaller than parent available width!
1002   */
1003  function reflow_static_normal(&$parent, &$context) {
1004    GenericFormattedBox::reflow($parent, $context);
1005
1006    // Calculate margin values if they have been set as a percentage
1007    $this->_calc_percentage_margins($parent);
1008
1009    // Calculate width value if it had been set as a percentage
1010    $this->_calc_percentage_width($parent, $context);
1011
1012    $wc = $this->get_css_property(CSS_WIDTH);
1013    if (!$wc->isNull()) {
1014      $col_width = $this->get_table_columns_min_widths($context);
1015      $maxw      = $this->get_table_columns_max_widths($context);
1016      $col_width = $this->_table_apply_colspans($col_width, $context, 'get_min_width', $col_width, $maxw);
1017
1018      if (array_sum($col_width) > $this->get_width()) {
1019        $wc = new WCConstant(array_sum($col_width));
1020      };
1021    };
1022
1023    // As table width can be deterimined by its contents, we may calculate auto values
1024    // only AFTER the contents have been reflown; thus, we'll offset the table
1025    // as a whole by a value of left margin AFTER the content reflow
1026
1027    // Do margin collapsing
1028    $y = $this->collapse_margin($parent, $context);
1029
1030    // At this moment we have top parent/child collapsed margin at the top of context object
1031    // margin stack
1032
1033    $y = $this->apply_clear($y, $context);
1034
1035    // Store calculated Y coordinate as current Y in the parent box
1036    $parent->_current_y = $y;
1037
1038    // Terminate current parent line-box
1039    $parent->close_line($context);
1040
1041    // And add current box to the parent's line-box (alone)
1042    $parent->append_line($this);
1043
1044    // Determine upper-left _content_ corner position of current box
1045    // Also see note above regarding margins
1046    $border = $this->get_css_property(CSS_BORDER);
1047    $padding = $this->get_css_property(CSS_PADDING);
1048
1049    $this->put_left($parent->_current_x +
1050                    $border->left->get_width() +
1051                    $padding->left->value);
1052
1053    // Note that top margin already used above during maring collapsing
1054    $this->put_top($parent->_current_y - $border->top->get_width()  - $padding->top->value);
1055
1056    /**
1057     * By default, child block box will fill all available parent width;
1058     * note that actual width will be smaller because of non-zero padding, border and margins
1059     */
1060    $this->put_full_width($parent->get_available_width($context));
1061
1062    // Reflow contents
1063    $this->reflow_content($context);
1064
1065    // Update the collapsed margin value with current box bottom margin
1066    $margin = $this->get_css_property(CSS_MARGIN);
1067
1068    $context->pop_collapsed_margin();
1069    $context->pop_collapsed_margin();
1070    $context->push_collapsed_margin($margin->bottom->value);
1071
1072    // Calculate margins and/or width is 'auto' values have been specified
1073    $this->_calc_auto_width_margins($parent);
1074    $this->offset($margin->left->value, 0);
1075
1076    // Extend parent's height to fit current box
1077    $parent->extend_height($this->get_bottom_margin());
1078    // Terminate parent's line box
1079    $parent->close_line($context);
1080  }
1081
1082  // Get a list of boolean values indicating if table rows are height constrained
1083  //
1084  // @return array containing 'true' value at index I if I-th row is not height-constrained
1085  // and 'false' otherwise
1086  //
1087  function get_non_constrained_flags() {
1088    $flags = array();
1089
1090    for ($i=0; $i<count($this->content); $i++) {
1091      $hc = $this->get_rhc($i);
1092      $flags[$i] =
1093        (is_null($hc->constant)) &&
1094        (is_null($hc->min)) &&
1095        (is_null($hc->max));
1096    };
1097
1098    return $flags;
1099  }
1100
1101  // Get a list of boolean values indicating if table rows are height constrained using percentage values
1102  //
1103  // @return array containing 'true' value at index I if I-th row is not height-constrained
1104  // and 'false' otherwise
1105  //
1106  function get_non_percentage_constrained_height_flags() {
1107    $flags = array();
1108
1109    for ($i=0; $i<count($this->content); $i++) {
1110      $hc = $this->get_rhc($i);
1111      $flags[$i] =
1112        (!is_null($hc->constant) ? !$hc->constant[1] : true) &&
1113        (!is_null($hc->min)      ? !$hc->min[1]      : true) &&
1114        (!is_null($hc->max)      ? !$hc->max[1]      : true);
1115    };
1116
1117    return $flags;
1118  }
1119
1120  function get_non_constrained_height_flags() {
1121    $flags = array();
1122
1123    for ($i=0; $i<count($this->content); $i++) {
1124      $hc = $this->get_rhc($i);
1125
1126      $flags[$i] = $hc->is_null();
1127    };
1128
1129    return $flags;
1130  }
1131
1132  // Get a list of boolean values indicating if table columns are height constrained
1133  //
1134  // @return array containing 'true' value at index I if I-th columns is not width-constrained
1135  // and 'false' otherwise
1136  //
1137  function get_non_constrained_width_flags() {
1138    $flags = array();
1139
1140    for ($i=0; $i<$this->cols_count(); $i++) {
1141      $wc = $this->get_cwc($i);
1142      $flags[$i] = is_a($wc,"wcnone");
1143    };
1144
1145    return $flags;
1146  }
1147
1148  function get_non_constant_constrained_width_flags() {
1149    $flags = array();
1150
1151    for ($i=0; $i<$this->cols_count(); $i++) {
1152      $wc = $this->get_cwc($i);
1153      $flags[$i] = !is_a($wc,"WCConstant");
1154    };
1155
1156    return $flags;
1157  }
1158
1159  function check_if_column_image_constrained($col) {
1160    for ($i=0; $i<$this->rows_count(); $i++) {
1161      $cell =& $this->cell($i, $col);
1162      for ($j=0; $j<count($cell->content); $j++) {
1163        if (!$cell->content[$j]->is_null() &&
1164            !is_a($cell->content[$j], "GenericImgBox")) {
1165          return false;
1166        };
1167      };
1168    };
1169    return true;
1170  }
1171
1172  function get_non_image_constrained_width_flags() {
1173    $flags = array();
1174
1175    for ($i=0; $i<$this->cols_count(); $i++) {
1176      $flags[$i] = !$this->check_if_column_image_constrained($i);
1177    };
1178
1179    return $flags;
1180  }
1181
1182  // Get a list of boolean values indicating if table rows are NOT constant constrained
1183  //
1184  // @return array containing 'true' value at index I if I-th row is height-constrained
1185  // and 'false' otherwise
1186  //
1187  function get_non_constant_constrained_flags() {
1188    $flags = array();
1189
1190    for ($i=0; $i<count($this->content); $i++) {
1191      $hc = $this->get_rhc($i);
1192      $flags[$i] = is_null($hc->constant);
1193    };
1194
1195    return $flags;
1196  }
1197
1198  function reflow_content(&$context) {
1199    // Reflow content
1200
1201    // Reset current Y value
1202    //
1203    $this->_current_y = $this->get_top();
1204
1205    // Determine the base table width
1206    // if width constraint exists, the actual table width will not be changed anyway
1207    //
1208    $this->put_width(min($this->get_max_width($context), $this->get_width()));
1209
1210    // Calculate widths of table columns
1211    $columns = $this->table_column_widths($context);
1212
1213    // Collapse table to minimum width (if width is not constrained)
1214    $real_width = array_sum($columns);
1215    $this->put_width($real_width);
1216
1217    // If width is constrained, and is less than calculated, update the width constraint
1218    //
1219    //     if ($this->get_width() < $real_width) {
1220    //       // $this->put_width_constraint(new WCConstant($real_width));
1221    //     };
1222
1223    // Flow cells horizontally in each table row
1224    for ($i=0; $i<count($this->content); $i++) {
1225      // Row flow started
1226      // Reset current X coordinate to the far left of the table
1227      $this->_current_x = $this->get_left();
1228
1229      // Flow each cell in the row
1230      $span = 0;
1231      for ($j=0; $j<count($this->content[$i]->content); $j++) {
1232        // Skip cells covered by colspans (fake cells, anyway)
1233        if ($span == 0) {
1234          // Flow current cell
1235          // Any colspans here?
1236          $span = $this->table_have_colspan($j, $i);
1237
1238          // Get sum of width for the current cell (or several cells in colspan)
1239          // In most cases, $span == 1 here (just a single cell)
1240          $cw = array_sum(array_slice($columns, $j, $span));
1241
1242          // store calculated width of the current cell
1243          $cell =& $this->content[$i]->content[$j];
1244          $cell->put_full_width($cw);
1245          $cell->setCSSProperty(CSS_WIDTH,
1246                                new WCConstant($cw -
1247                                               $cell->_get_hor_extra()));
1248
1249          // TODO: check for rowspans
1250
1251          // Flow cell
1252          $this->content[$i]->content[$j]->reflow($this, $context);
1253
1254          // Offset current X value by the cell width
1255          $this->_current_x += $cw;
1256        };
1257
1258        // Current cell have been processed or skipped
1259        $span = max(0, $span-1);
1260      }
1261
1262      // calculate row height and do vertical align
1263      //      $this->table_fit_row($i);
1264
1265      // row height calculation offset current Y coordinate by the row height calculated
1266      //      $this->_current_y -= $this->table_row_height($i);
1267      $this->_current_y -= $this->content[$i]->row_height();
1268    }
1269
1270    // Calculate (and possibly adjust height of table rows)
1271    $heights = $this->_row_heights(0.1);
1272
1273    // adjust row heights to fit cells spanning several rows
1274    foreach ($this->get_rowspans() as $rowspan) {
1275      // Get height of the cell
1276      $cell_height = $this->content[$rowspan->row]->content[$rowspan->column]->get_full_height();
1277
1278      // Get calculated height of the spanned-over rows
1279      $cell_row_heights = array_slice($heights, $rowspan->row, $rowspan->size);
1280
1281      // Get list of non-constrained columns
1282      $flags = array_slice($this->get_non_constrained_flags(), $rowspan->row, $rowspan->size);
1283
1284      // Expand row heights (only for non-constrained columns)
1285      $new_heights = expand_to_with_flags($cell_height,
1286                                          $cell_row_heights,
1287                                          $flags);
1288
1289      // Check if rows could not be expanded
1290      //      if (array_sum($new_heights) < $cell_height-1) {
1291      if (array_sum($new_heights) < $cell_height - EPSILON) {
1292        // Get list of non-constant-constrained columns
1293        $flags = array_slice($this->get_non_constant_constrained_flags(), $rowspan->row, $rowspan->size);
1294
1295        // use non-constant-constrained rows
1296        $new_heights = expand_to_with_flags($cell_height,
1297                                            $cell_row_heights,
1298                                            $flags);
1299      };
1300
1301      // Update the rows heights
1302      array_splice($heights,
1303                   $rowspan->row,
1304                   $rowspan->size,
1305                   $new_heights);
1306    }
1307
1308    // Now expand rows to full table height
1309    $table_height = max($this->get_height(), array_sum($heights));
1310
1311    // Get list of non-constrained columns
1312    $flags = $this->get_non_constrained_height_flags();
1313
1314    // Expand row heights (only for non-constrained columns)
1315    $heights = expand_to_with_flags($table_height,
1316                                    $heights,
1317                                    $flags);
1318
1319    // Check if rows could not be expanded
1320    if (array_sum($heights) < $table_height - EPSILON) {
1321      // Get list of non-constant-constrained columns
1322      $flags = $this->get_non_constant_constrained_flags();
1323
1324      // use non-constant-constrained rows
1325      $heights = expand_to_with_flags($table_height,
1326                                      $heights,
1327                                      $flags);
1328    };
1329
1330    // Now we calculated row heights, time to actually resize them
1331    $this->table_resize_rows($heights);
1332
1333    // Update size of cells spanning several rows
1334    $this->table_fit_rowspans($heights);
1335
1336    // Expand total table height, if needed
1337    $total_height = array_sum($heights);
1338    if ($total_height > $this->get_height()) {
1339      $hc = new HCConstraint(array($total_height, false),
1340                             array($total_height, false),
1341                             array($total_height, false));
1342      $this->put_height_constraint($hc);
1343    };
1344  }
1345
1346  function isBlockLevel() {
1347    return true;
1348  }
1349}
1350?>