Passed
Push — master ( 6f9ff4...75f13c )
by
unknown
01:05
created

src/FrameReflower/AbstractFrameReflower.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @package dompdf
4
 * @link    http://dompdf.github.com/
5
 * @author  Benj Carson <[email protected]>
6
 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
7
 */
8
namespace Dompdf\FrameReflower;
9
10
use Dompdf\Adapter\CPDF;
11
use Dompdf\Css\Style;
12
use Dompdf\Dompdf;
13
use Dompdf\Helpers;
14
use Dompdf\Frame;
15
use Dompdf\FrameDecorator\Block;
16
use Dompdf\Frame\Factory;
17
18
/**
19
 * Base reflower class
20
 *
21
 * Reflower objects are responsible for determining the width and height of
22
 * individual frames.  They also create line and page breaks as necessary.
23
 *
24
 * @package dompdf
25
 */
26
abstract class AbstractFrameReflower
27
{
28
29
    /**
30
     * Frame for this reflower
31
     *
32
     * @var Frame
33
     */
34
    protected $_frame;
35
36
    /**
37
     * Cached min/max size
38
     *
39
     * @var array
40
     */
41
    protected $_min_max_cache;
42
43
    /**
44
     * AbstractFrameReflower constructor.
45
     * @param Frame $frame
46
     */
47
    function __construct(Frame $frame)
48
    {
49
        $this->_frame = $frame;
50
        $this->_min_max_cache = null;
51
    }
52
53
    function dispose()
54
    {
55
    }
56
57
    /**
58
     * @return Dompdf
59
     */
60
    function get_dompdf()
61
    {
62
        return $this->_frame->get_dompdf();
63
    }
64
65
    /**
66
     * Collapse frames margins
67
     * http://www.w3.org/TR/CSS2/box.html#collapsing-margins
68
     */
69
    protected function _collapse_margins()
70
    {
71
        $frame = $this->_frame;
72
        $cb = $frame->get_containing_block();
73
        $style = $frame->get_style();
74
75
        // Margins of float/absolutely positioned/inline-block elements do not collapse.
76
        if (!$frame->is_in_flow() || $frame->is_inline_block()) {
77
            return;
78
        }
79
80
        $t = $style->length_in_pt($style->margin_top, $cb["h"]);
81
        $b = $style->length_in_pt($style->margin_bottom, $cb["h"]);
82
83
        // Handle 'auto' values
84
        if ($t === "auto") {
85
            $style->margin_top = "0pt";
86
            $t = 0;
87
        }
88
89
        if ($b === "auto") {
90
            $style->margin_bottom = "0pt";
91
            $b = 0;
92
        }
93
94
        // Collapse vertical margins:
95
        $n = $frame->get_next_sibling();
96
        if ( $n && !$n->is_block() & !$n->is_table() ) {
97
            while ($n = $n->get_next_sibling()) {
98
                if ($n->is_block() || $n->is_table()) {
99
                    break;
100
                }
101
102
                if (!$n->get_first_child()) {
103
                    $n = null;
104
                    break;
105
                }
106
            }
107
        }
108
109
        if ($n) {
110
            $n_style = $n->get_style();
111
            $n_t = (float)$n_style->length_in_pt($n_style->margin_top, $cb["h"]);
112
113
            $b = $this->_get_collapsed_margin_length($b, $n_t);
114
            $style->margin_bottom = $b . "pt";
115
            $n_style->margin_top = "0pt";
116
        }
117
118
        // Collapse our first child's margin, if there is no border or padding
119 View Code Duplication
        if ($style->border_top_width == 0 && $style->length_in_pt($style->padding_top) == 0) {
0 ignored issues
show
The property border_top_width does not exist on object<Dompdf\Css\Style>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
120
            $f = $this->_frame->get_first_child();
121
            if ( $f && !$f->is_block() && !$f->is_table() ) {
122
                while ( $f = $f->get_next_sibling() ) {
123
                    if ( $f->is_block() || $f->is_table() ) {
124
                        break;
125
                    }
126
127
                    if ( !$f->get_first_child() ) {
128
                        $f = null;
129
                        break;
130
                    }
131
                }
132
            }
133
134
            // Margin are collapsed only between block-level boxes
135
            if ($f) {
136
                $f_style = $f->get_style();
137
                $f_t = (float)$f_style->length_in_pt($f_style->margin_top, $cb["h"]);
138
139
                $t = $this->_get_collapsed_margin_length($t, $f_t);
140
                $style->margin_top = $t."pt";
141
                $f_style->margin_top = "0pt";
142
            }
143
        }
144
145
        // Collapse our last child's margin, if there is no border or padding
146 View Code Duplication
        if ($style->border_bottom_width == 0 && $style->length_in_pt($style->padding_bottom) == 0) {
0 ignored issues
show
The property border_bottom_width does not exist on object<Dompdf\Css\Style>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
147
            $l = $this->_frame->get_last_child();
148
            if ( $l && !$l->is_block() && !$l->is_table() ) {
149
                while ( $l = $l->get_prev_sibling() ) {
150
                    if ( $l->is_block() || $l->is_table() ) {
151
                        break;
152
                    }
153
154
                    if ( !$l->get_last_child() ) {
155
                        $l = null;
156
                        break;
157
                    }
158
                }
159
            }
160
161
            // Margin are collapsed only between block-level boxes
162
            if ($l) {
163
                $l_style = $l->get_style();
164
                $l_b = (float)$l_style->length_in_pt($l_style->margin_bottom, $cb["h"]);
165
166
                $b = $this->_get_collapsed_margin_length($b, $l_b);
167
                $style->margin_bottom = $b."pt";
168
                $l_style->margin_bottom = "0pt";
169
            }
170
        }
171
    }
172
173
    /**
174
     * Get the combined (collapsed) length of two adjoining margins.
175
     * 
176
     * See http://www.w3.org/TR/CSS2/box.html#collapsing-margins.
177
     * 
178
     * @param number $length1
179
     * @param number $length2
180
     * @return number
181
     */
182
    private function _get_collapsed_margin_length($length1, $length2)
183
    {
184
        if ($length1 < 0 && $length2 < 0) {
185
            return min($length1, $length2); // min(x, y) = - max(abs(x), abs(y)), if x < 0 && y < 0
186
        }
187
        
188
        if ($length1 < 0 || $length2 < 0) {
189
            return $length1 + $length2; // x + y = x - abs(y), if y < 0
190
        }
191
        
192
        return max($length1, $length2);
193
    }
194
195
    /**
196
     * @param Block|null $block
197
     * @return mixed
198
     */
199
    abstract function reflow(Block $block = null);
200
201
    /**
202
     * Required for table layout: Returns an array(0 => min, 1 => max, "min"
203
     * => min, "max" => max) of the minimum and maximum widths of this frame.
204
     * This provides a basic implementation.  Child classes should override
205
     * this if necessary.
206
     *
207
     * @return array|null
208
     */
209
    function get_min_max_width()
210
    {
211
        if (!is_null($this->_min_max_cache)) {
212
            return $this->_min_max_cache;
213
        }
214
215
        $style = $this->_frame->get_style();
216
217
        // Account for margins & padding
218
        $dims = array($style->padding_left,
219
            $style->padding_right,
220
            $style->border_left_width,
221
            $style->border_right_width,
222
            $style->margin_left,
223
            $style->margin_right);
224
225
        $cb_w = $this->_frame->get_containing_block("w");
226
        $delta = (float)$style->length_in_pt($dims, $cb_w);
227
228
        // Handle degenerate case
229
        if (!$this->_frame->get_first_child()) {
230
            return $this->_min_max_cache = array(
231
                $delta, $delta,
232
                "min" => $delta,
233
                "max" => $delta,
234
            );
235
        }
236
237
        $low = array();
238
        $high = array();
239
240
        for ($iter = $this->_frame->get_children()->getIterator(); $iter->valid(); $iter->next()) {
241
            $inline_min = 0;
242
            $inline_max = 0;
243
244
            // Add all adjacent inline widths together to calculate max width
245
            while ($iter->valid() && in_array($iter->current()->get_style()->display, Style::$INLINE_TYPES)) {
246
                $child = $iter->current();
247
248
                $minmax = $child->get_min_max_width();
249
250
                if (in_array($iter->current()->get_style()->white_space, array("pre", "nowrap"))) {
251
                    $inline_min += $minmax["min"];
252
                } else {
253
                    $low[] = $minmax["min"];
254
                }
255
256
                $inline_max += $minmax["max"];
257
                $iter->next();
258
            }
259
260
            if ($inline_max > 0) {
261
                $high[] = $inline_max;
262
            }
263
            if ($inline_min > 0) {
264
                $low[] = $inline_min;
265
            }
266
267
            if ($iter->valid()) {
268
                list($low[], $high[]) = $iter->current()->get_min_max_width();
269
                continue;
270
            }
271
        }
272
        $min = count($low) ? max($low) : 0;
273
        $max = count($high) ? max($high) : 0;
274
275
        // Use specified width if it is greater than the minimum defined by the
276
        // content.  If the width is a percentage ignore it for now.
277
        $width = $style->width;
278
        if ($width !== "auto" && !Helpers::is_percent($width)) {
279
            $width = (float)$style->length_in_pt($width, $cb_w);
280
            if ($min < $width) {
281
                $min = $width;
282
            }
283
            if ($max < $width) {
284
                $max = $width;
285
            }
286
        }
287
288
        $min += $delta;
289
        $max += $delta;
290
        return $this->_min_max_cache = array($min, $max, "min" => $min, "max" => $max);
291
    }
292
293
    /**
294
     * Parses a CSS string containing quotes and escaped hex characters
295
     *
296
     * @param $string string The CSS string to parse
297
     * @param $single_trim
298
     * @return string
299
     */
300
    protected function _parse_string($string, $single_trim = false)
301
    {
302
        if ($single_trim) {
303
            $string = preg_replace('/^[\"\']/', "", $string);
304
            $string = preg_replace('/[\"\']$/', "", $string);
305
        } else {
306
            $string = trim($string, "'\"");
307
        }
308
309
        $string = str_replace(array("\\\n", '\\"', "\\'"),
310
            array("", '"', "'"), $string);
311
312
        // Convert escaped hex characters into ascii characters (e.g. \A => newline)
313
        $string = preg_replace_callback("/\\\\([0-9a-fA-F]{0,6})/",
314
            function ($matches) { return \Dompdf\Helpers::unichr(hexdec($matches[1])); },
315
            $string);
316
        return $string;
317
    }
318
319
    /**
320
     * Parses a CSS "quotes" property
321
     *
322
     * @return array|null An array of pairs of quotes
323
     */
324
    protected function _parse_quotes()
325
    {
326
        // Matches quote types
327
        $re = '/(\'[^\']*\')|(\"[^\"]*\")/';
328
329
        $quotes = $this->_frame->get_style()->quotes;
330
331
        // split on spaces, except within quotes
332
        if (!preg_match_all($re, "$quotes", $matches, PREG_SET_ORDER)) {
333
            return null;
334
        }
335
336
        $quotes_array = array();
337
        foreach ($matches as $_quote) {
338
            $quotes_array[] = $this->_parse_string($_quote[0], true);
339
        }
340
341
        if (empty($quotes_array)) {
342
            $quotes_array = array('"', '"');
343
        }
344
345
        return array_chunk($quotes_array, 2);
346
    }
347
348
    /**
349
     * Parses the CSS "content" property
350
     *
351
     * @return string|null The resulting string
352
     */
353
    protected function _parse_content()
354
    {
355
        // Matches generated content
356
        $re = "/\n" .
357
            "\s(counters?\\([^)]*\\))|\n" .
358
            "\A(counters?\\([^)]*\\))|\n" .
359
            "\s([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\3|\n" .
360
            "\A([\"']) ( (?:[^\"']|\\\\[\"'])+ )(?<!\\\\)\\5|\n" .
361
            "\s([^\s\"']+)|\n" .
362
            "\A([^\s\"']+)\n" .
363
            "/xi";
364
365
        $content = $this->_frame->get_style()->content;
366
367
        $quotes = $this->_parse_quotes();
368
369
        // split on spaces, except within quotes
370
        if (!preg_match_all($re, $content, $matches, PREG_SET_ORDER)) {
371
            return null;
372
        }
373
374
        $text = "";
375
376
        foreach ($matches as $match) {
377 View Code Duplication
            if (isset($match[2]) && $match[2] !== "") {
378
                $match[1] = $match[2];
379
            }
380
381 View Code Duplication
            if (isset($match[6]) && $match[6] !== "") {
382
                $match[4] = $match[6];
383
            }
384
385 View Code Duplication
            if (isset($match[8]) && $match[8] !== "") {
386
                $match[7] = $match[8];
387
            }
388
389
            if (isset($match[1]) && $match[1] !== "") {
390
                // counters?(...)
391
                $match[1] = mb_strtolower(trim($match[1]));
392
393
                // Handle counter() references:
394
                // http://www.w3.org/TR/CSS21/generate.html#content
395
396
                $i = mb_strpos($match[1], ")");
397
                if ($i === false) {
398
                    continue;
399
                }
400
401
                preg_match('/(counters?)(^\()*?\(\s*([^\s,]+)\s*(,\s*["\']?([^"\'\)]*)["\']?\s*(,\s*([^\s)]+)\s*)?)?\)/i', $match[1], $args);
402
                $counter_id = $args[3];
403
                if (strtolower($args[1]) == 'counter') {
404
                    // counter(name [,style])
405 View Code Duplication
                    if (isset($args[5])) {
406
                        $type = trim($args[5]);
407
                    } else {
408
                        $type = null;
409
                    }
410
                    $p = $this->_frame->lookup_counter_frame($counter_id);
411
412
                    $text .= $p->counter_value($counter_id, $type);
413
414
                } else if (strtolower($args[1]) == 'counters') {
415
                    // counters(name, string [,style])
416
                    if (isset($args[5])) {
417
                        $string = $this->_parse_string($args[5]);
418
                    } else {
419
                        $string = "";
420
                    }
421
422 View Code Duplication
                    if (isset($args[7])) {
423
                        $type = trim($args[7]);
424
                    } else {
425
                        $type = null;
426
                    }
427
428
                    $p = $this->_frame->lookup_counter_frame($counter_id);
429
                    $tmp = array();
430
                    while ($p) {
431
                        // We only want to use the counter values when they actually increment the counter
432
                        if (array_key_exists($counter_id, $p->_counters)) {
433
                            array_unshift($tmp, $p->counter_value($counter_id, $type));
434
                        }
435
                        $p = $p->lookup_counter_frame($counter_id);
436
                    }
437
                    $text .= implode($string, $tmp);
438
                } else {
439
                    // countertops?
440
                    continue;
441
                }
442
443
            } else if (isset($match[4]) && $match[4] !== "") {
444
                // String match
445
                $text .= $this->_parse_string($match[4]);
446
            } else if (isset($match[7]) && $match[7] !== "") {
447
                // Directive match
448
449
                if ($match[7] === "open-quote") {
450
                    // FIXME: do something here
451
                    $text .= $quotes[0][0];
452
                } else if ($match[7] === "close-quote") {
453
                    // FIXME: do something else here
454
                    $text .= $quotes[0][1];
455
                } else if ($match[7] === "no-open-quote") {
456
                    // FIXME:
457
                } else if ($match[7] === "no-close-quote") {
458
                    // FIXME:
459
                } else if (mb_strpos($match[7], "attr(") === 0) {
460
                    $i = mb_strpos($match[7], ")");
461
                    if ($i === false) {
462
                        continue;
463
                    }
464
465
                    $attr = mb_substr($match[7], 5, $i - 5);
466
                    if ($attr == "") {
467
                        continue;
468
                    }
469
470
                    $text .= $this->_frame->get_parent()->get_node()->getAttribute($attr);
471
                } else {
472
                    continue;
473
                }
474
            }
475
        }
476
477
        return $text;
478
    }
479
480
    /**
481
     * Sets the generated content of a generated frame
482
     */
483
    protected function _set_content()
484
    {
485
        $frame = $this->_frame;
486
        $style = $frame->get_style();
487
488
        // if the element was pushed to a new page use the saved counter value, otherwise use the CSS reset value
489
        if ($style->counter_reset && ($reset = $style->counter_reset) !== "none") {
490
            $vars = preg_split('/\s+/', trim($reset), 2);
491
            $frame->reset_counter($vars[0], (isset($frame->_counters['__' . $vars[0]]) ? $frame->_counters['__' . $vars[0]] : (isset($vars[1]) ? $vars[1] : 0)));
492
        }
493
494
        if ($style->counter_increment && ($increment = $style->counter_increment) !== "none") {
495
            $frame->increment_counters($increment);
496
        }
497
498
        if ($style->content && $frame->get_node()->nodeName === "dompdf_generated") {
499
            $content = $this->_parse_content();
500
            // add generated content to the font subset
501
            // FIXME: This is currently too late because the font subset has already been generated.
502
            //        See notes in issue #750.
503
            if ($frame->get_dompdf()->getOptions()->getIsFontSubsettingEnabled() && $frame->get_dompdf()->get_canvas() instanceof CPDF) {
504
                $frame->get_dompdf()->get_canvas()->register_string_subset($style->font_family, $content);
505
            }
506
507
            $node = $frame->get_node()->ownerDocument->createTextNode($content);
508
509
            $new_style = $style->get_stylesheet()->create_style();
510
            $new_style->inherit($style);
511
512
            $new_frame = new Frame($node);
513
            $new_frame->set_style($new_style);
514
515
            Factory::decorate_frame($new_frame, $frame->get_dompdf(), $frame->get_root());
516
            $frame->append_child($new_frame);
517
        }
518
    }
519
520
    /**
521
     * Determine current frame width based on contents
522
     *
523
     * @return float
524
     */
525
    public function calculate_auto_width()
526
    {
527
        return $this->_frame->get_margin_width();
528
    }
529
}
530