From 9a4e092e9daca931a4205789907a74d30eb6d851 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 12 Oct 2026 07:51:54 +1100 Subject: [PATCH 1/2] Draw every layer of a multiple background (#609) A background or background-image with several comma-separated layers drew only its first layer, and the layer longhands were read as one value or dropped. In standard mode the shorthand and the longhands now keep a value for each layer, joined by BackgroundLayers so the list cascades like any other value, and SetBackground() reads every layer. Blocks, tables, rows, cells, body and @page paint the layers from the last to the first, over the colour of the last layer and under the clip of its background-clip. A single layer is read and painted as before, and legacy mode keeps drawing only the first layer. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG-GRAVITYPDF.md | 4 + src/Css/BackgroundLayers.php | 67 ++ src/Css/NormalizeProperties.php | 147 +++- src/Mpdf.php | 700 ++++++++++-------- tests/Mpdf/Css/NormalizePropertiesTest.php | 102 ++- tests/Mpdf/MultipleBackgroundsTest.php | 211 ++++++ .../BackgroundShorthandSnapshotTest.php | 2 +- .../MultipleBackgroundsSnapshotTest.php | 77 ++ tests/data/snapshots/background-shorthand.pdf | Bin 46364 -> 46244 bytes tests/data/snapshots/multiple-backgrounds.pdf | Bin 0 -> 53871 bytes 10 files changed, 974 insertions(+), 336 deletions(-) create mode 100644 src/Css/BackgroundLayers.php create mode 100644 tests/Mpdf/MultipleBackgroundsTest.php create mode 100644 tests/Snapshots/MultipleBackgroundsSnapshotTest.php create mode 100644 tests/data/snapshots/multiple-backgrounds.pdf diff --git a/CHANGELOG-GRAVITYPDF.md b/CHANGELOG-GRAVITYPDF.md index a674b3d97..35468e8cb 100644 --- a/CHANGELOG-GRAVITYPDF.md +++ b/CHANGELOG-GRAVITYPDF.md @@ -80,6 +80,9 @@ Read this section before upgrading from upstream mPDF. Each entry says what chan * A bidirectional embedding, as in `…
…
…
`, is opened again for the inline element's text after the block. * **Stylesheet rules that name `sup`, `sub` or `center` apply**, in both CSS modes. mPDF left the three tags out of `allowedCSStags`, so `sup { color: red }`, `p sub { … }` and `center { … }` did nothing while a class rule on the same element worked. A rule's `font-size` and `vertical-align` take the place of the 55% size and the raise or drop mPDF gives the tags by default, rather than adding to them, so normalize.css's `sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline }` now draws them at 75% on the baseline. To keep ignoring such rules, pass an `allowedCSStags` without `SUP`, `SUB` and `CENTER`. [#649] [#680] * **`min-width` and `max-width` hold the width of a block**, as in a browser, in standard mode. mPDF never read them on a block, so `max-width: 100mm; margin: 0 auto`, the usual centred container, filled the page, `width: 150mm; max-width: 100mm` was 150mm wide, and `width: 50mm; min-width: 80mm` was 50mm. Now the width is held within the two, `max-width` first, then `min-width`, a percentage taken of the container's inner width as `width` is, and the block's auto margins, direction or float place the narrowed box. Floats and positioned blocks take the same limits. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to ignore the two properties as before. [#588] [#691] +* **A background with several comma-separated layers draws every layer**, as in a browser, in standard mode. mPDF drew only the first, so `background: url(logo.png) no-repeat, linear-gradient(red, blue)` lost the gradient and `background-image: url(a.png), url(b.png)` lost `b.png`. Now each layer is drawn, the first on top and the last at the bottom, over the colour the last layer names, on blocks, tables, rows, cells, `body` and `@page`. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to draw only the first layer as before. [#552] [#609] + * `background-image`, `-repeat`, `-position`, `-size`, `-origin` and `-clip` take a comma-separated list, one value per layer. A list shorter than the list of images repeats, and a longer one is cut short, so `background-position: right bottom, left top` with one image places it at the bottom right; before, the list was dropped. + * Every layer is clipped to the box the last layer's `background-clip` names, the one the colour takes. * `box-sizing` is not read, so the limits measure the content, and a block with padding and borders is wider by them. `min-width` and `max-width` on a table or cell are still not read. New features @@ -965,3 +968,4 @@ These changes do not change output. [#691]: https://github.com/GravityPDF/mpdf/pull/691 [#584]: https://github.com/GravityPDF/mpdf/issues/584 [#689]: https://github.com/GravityPDF/mpdf/pull/689 +[#609]: https://github.com/GravityPDF/mpdf/issues/609 diff --git a/src/Css/BackgroundLayers.php b/src/Css/BackgroundLayers.php new file mode 100644 index 000000000..8061e7130 --- /dev/null +++ b/src/Css/BackgroundLayers.php @@ -0,0 +1,67 @@ + ['i', ''], + 'BACKGROUND-REPEAT' => ['r', 'repeat'], + 'BACKGROUND-POSITION' => ['p', '0% 0%'], + 'BACKGROUND-SIZE' => ['s', 'auto'], + 'BACKGROUND-ORIGIN' => ['o', 'padding-box'], + 'BACKGROUND-CLIP' => ['k', 'border-box'], + ]; + /** * @var array */ @@ -234,7 +247,7 @@ public function normalize($prop) $this->processPageSizeProperty($k, $v); } elseif ($k === 'BREAK-BEFORE' || $k === 'BREAK-AFTER' || $k === 'BREAK-INSIDE') { $this->processBreakProperty($k, $v); - } elseif (in_array($k, ['BACKGROUND', 'BACKGROUND-IMAGE', 'BACKGROUND-REPEAT', 'BACKGROUND-POSITION'], true)) { + } elseif (in_array($k, ['BACKGROUND', 'BACKGROUND-IMAGE', 'BACKGROUND-REPEAT', 'BACKGROUND-POSITION', 'BACKGROUND-SIZE', 'BACKGROUND-ORIGIN', 'BACKGROUND-CLIP'], true)) { $this->processBackgroundProperty($k, $v); } elseif ($k === 'IMAGE-ORIENTATION') { $this->processImageOrientationProperty($v); @@ -569,23 +582,16 @@ protected function processBorderShorthandProperty($key, $value) * Parse CSS background shorthand property. * * Reads the colour, image, position with an optional "/ size", repeat, attachment and boxes of each - * comma-separated layer, in any order. Only the first layer is drawn, over the colour, which only the - * last layer may carry. + * comma-separated layer, in any order. Only the last layer may carry the colour. * * @param string $s Background property value - * @return array|false The parts given, keyed 'c' (color), 'i' (image), 'r' (repeat), 'p' (position), - * 's' (size), 'o' (origin) and 'k' (clip), or false when the value is not a background + * @return array[]|false The parts of each layer, the top layer first, keyed 'c' (color), 'i' (image), 'r' (repeat), + * 'p' (position), 's' (size), 'o' (origin) and 'k' (clip); an empty list for a CSS-wide + * keyword; or false when the value is not a background */ - protected function parseCssBackground($s) + private function parseBackgroundLayers($s) { - $layers = [[]]; - foreach ($this->splitComponents($s) as $component) { - if ($component === ',') { - $layers[] = []; - } else { - $layers[count($layers) - 1][] = $component; - } - } + $layers = $this->splitLayers($s); // A CSS-wide keyword gives the initial value, no background. The standard CSS mode passes it to the longhands // before this, for CssMerger to resolve @@ -601,11 +607,27 @@ protected function parseCssBackground($s) } } - if (isset($layers[$last]['c'])) { - $layers[0]['c'] = $layers[$last]['c']; + return $layers; + } + + /** + * Splits a value at the commas outside its function calls and quoted strings + * + * @param string $value + * @return string[][] The components of each comma-separated part + */ + private function splitLayers($value) + { + $layers = [[]]; + foreach ($this->splitComponents($value) as $component) { + if ($component === ',') { + $layers[] = []; + } else { + $layers[count($layers) - 1][] = $component; + } } - return $layers[0]; + return $layers; } /** @@ -613,7 +635,7 @@ protected function parseCssBackground($s) * A single box keyword sets both the origin and the clip. * * @param string[] $components - * @return array|false The parts given, keyed as parseCssBackground() returns them, or false when a component is none of them + * @return array|false The parts given, keyed as parseBackgroundLayers() returns them, or false when a component is none of them */ private function parseBackgroundLayer(array $components) { @@ -1065,7 +1087,8 @@ private function mergeBackgroundParts(array $bg) /** * Process background related CSS properties. * - * Handles BACKGROUND, BACKGROUND-IMAGE, BACKGROUND-REPEAT, and BACKGROUND-POSITION. + * Handles BACKGROUND and its longhands. The standard CSS mode reads a comma-separated list of layers into each + * of them, kept as BackgroundLayers joins them. * * @param string $property Property name * @param string $value Property value @@ -1073,29 +1096,42 @@ private function mergeBackgroundParts(array $bg) */ protected function processBackgroundProperty($property, $value) { + if ($property !== 'BACKGROUND' && $this->mpdf->cssMode === CssMode::STANDARD) { + $layers = $this->splitLayers($value); + if (count($layers) > 1) { + $this->processLayeredBackgroundProperty($property, $layers); + return; + } + } + switch ($property) { case 'BACKGROUND': - $bg = $this->parseCssBackground($value); + $layers = $this->parseBackgroundLayers($value); // A value that is not a background is dropped, as a browser drops it - if ($bg === false) { + if ($layers === false) { break; } - $this->properties['BACKGROUND-COLOR'] = isset($bg['c']) ? $bg['c'] : 'transparent'; - $this->properties['BACKGROUND-IMAGE'] = isset($bg['i']) ? $bg['i'] : ''; + $layers = $layers ?: [[]]; + $bottom = $layers[count($layers) - 1]; + $this->properties['BACKGROUND-COLOR'] = isset($bottom['c']) ? $bottom['c'] : 'transparent'; if (!$this->resetsOmittedParts()) { - $this->mergeBackgroundParts($bg); + // Only the top layer is drawn + $this->properties['BACKGROUND-IMAGE'] = isset($layers[0]['i']) ? $layers[0]['i'] : ''; + $this->mergeBackgroundParts($layers[0]); break; } // A part not given takes its initial value, replacing a longhand given before it in the cascade - $this->properties['BACKGROUND-REPEAT'] = isset($bg['r']) ? $bg['r'] : 'repeat'; - $this->properties['BACKGROUND-POSITION'] = !empty($bg['p']) ? $bg['p'] : '0% 0%'; - $this->properties['BACKGROUND-SIZE'] = isset($bg['s']) ? $bg['s'] : 'auto'; - $this->properties['BACKGROUND-ORIGIN'] = isset($bg['o']) ? $bg['o'] : 'padding-box'; - $this->properties['BACKGROUND-CLIP'] = isset($bg['k']) ? $bg['k'] : 'border-box'; + foreach (self::LAYERED_LONGHANDS as $longhand => $part) { + $values = []; + foreach ($layers as $layer) { + $values[] = isset($layer[$part[0]]) && $layer[$part[0]] !== false ? $layer[$part[0]] : $part[1]; + } + $this->properties[$longhand] = BackgroundLayers::join($values); + } break; case 'BACKGROUND-IMAGE': @@ -1124,9 +1160,62 @@ protected function processBackgroundProperty($property, $value) $this->properties['BACKGROUND-POSITION'] = $normalizedPosition; } break; + + default: + $this->properties[$property] = $value; } } + /** + * Sets a background longhand that lists a value for each layer. The list is dropped when one of its values is not + * a value of the property, as a browser drops it + * + * @param string $property Property name + * @param string[][] $layers The components of each layer's value + * @return void + */ + private function processLayeredBackgroundProperty($property, array $layers) + { + $values = []; + foreach ($layers as $components) { + $value = $this->layerValue($property, $components); + if ($value === false) { + return; + } + $values[] = $value; + } + + $this->properties[$property] = BackgroundLayers::join($values); + } + + /** + * Reads one layer's value of a background longhand, the way the shorthand reads that part + * + * @param string $property Property name + * @param string[] $components + * @return string|false The value, or false when it is not a value of the property + */ + private function layerValue($property, array $components) + { + if ($property === 'BACKGROUND-SIZE') { + $size = implode(' ', $components); + $length = '(auto|' . self::NUMBER . '([a-z]+|%)?)'; + + return preg_match('/^(cover|contain|' . $length . '( ' . $length . ')?)$/', $size) ? $size : false; + } + + $part = self::LAYERED_LONGHANDS[$property][0]; + $layer = $this->parseBackgroundLayer($components); + if (!isset($layer[$part]) || $layer[$part] === false) { + return false; + } + + // One box keyword sets both the origin and the clip in the shorthand; a longhand takes one keyword + $expected = $part === 'o' || $part === 'k' ? ['o' => $layer[$part], 'k' => $layer[$part]] : [$part => $layer[$part]]; + + return $layer == $expected ? $layer[$part] : false; + } + /** * Process border radius property. * diff --git a/src/Mpdf.php b/src/Mpdf.php index b700f3353..c3c013494 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -5,6 +5,7 @@ use Mpdf\Config\ConfigVariables; use Mpdf\Config\FontVariables; use Mpdf\Conversion; +use Mpdf\Css\BackgroundLayers; use Mpdf\Css\Border; use Mpdf\Css\BorderRadius; use Mpdf\Css\CommentParser; @@ -2822,7 +2823,63 @@ function _resizeBackgroundImage($imw, $imh, $cw, $ch, $resize, $repx, $repy, $pb return [$imw, $imh, $repx, $repy]; } + /** + * Reads an element's background image or images + * + * @param array $properties The element's CSS + * @param float $maxwidth The width percentages and font-relative lengths are read against + * @return array|false What backgroundLayer() returns for a background of one layer; for more than one, the same of + * each layer drawn, the top layer first, under the key 'layers'; or false when none is drawn + */ function SetBackground(&$properties, &$maxwidth) + { + $layers = []; + $images = BackgroundLayers::split($properties['BACKGROUND-IMAGE']); + foreach ($images as $n => $image) { + if ($image === '') { + continue; + } + + $layer = $properties; + $layer['BACKGROUND-IMAGE'] = $image; + foreach (['BACKGROUND-REPEAT', 'BACKGROUND-POSITION', 'BACKGROUND-SIZE', 'BACKGROUND-ORIGIN'] as $property) { + if (isset($properties[$property])) { + $layer[$property] = BackgroundLayers::valueFor($properties[$property], $n); + } + } + + $layer = $this->backgroundLayer($layer, $maxwidth); + if ($layer) { + $layers[] = $layer; + } + } + + if (count($layers) < 2) { + return $layers ? $layers[0] : false; + } + + return ['layers' => $layers]; + } + + /** + * The layers of a background SetBackground() read, in the order they are painted: the bottom layer first + * + * @param array $background + * @return array[] + */ + private function backgroundLayers(array $background) + { + return isset($background['layers']) ? array_reverse($background['layers']) : [$background]; + } + + /** + * Reads one layer of an element's background + * + * @param array $properties The element's CSS, with the layer's values + * @param float $maxwidth The width percentages and font-relative lengths are read against + * @return array|false The gradient, or the image and how it is placed, or false when the image cannot be read + */ + private function backgroundLayer(array $properties, $maxwidth) { if (isset($properties['BACKGROUND-ORIGIN']) && ($properties['BACKGROUND-ORIGIN'] == 'border-box' || $properties['BACKGROUND-ORIGIN'] == 'content-box')) { $origin = $properties['BACKGROUND-ORIGIN']; @@ -2951,23 +3008,25 @@ function PrintBodyBackgrounds() } } if ($this->bodyBackgroundImage) { - if (isset($this->bodyBackgroundImage['gradient']) && $this->bodyBackgroundImage['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $this->bodyBackgroundImage['gradient'])) { - $g = $this->gradient->parseMozGradient($this->bodyBackgroundImage['gradient']); - if ($g) { - $s .= $this->gradient->Gradient($clx, $cly, $clw, $clh, $g['type'], $g['stops'], $g['colorspace'], $g['coords'], $g['extend'], true); - } - } elseif ($this->bodyBackgroundImage['image_id']) { // Background pattern - $n = count($this->patterns) + 1; - // If using resize, uses TrimBox (not including the bleed) - list($orig_w, $orig_h, $x_repeat, $y_repeat) = $this->_resizeBackgroundImage($this->bodyBackgroundImage['orig_w'], $this->bodyBackgroundImage['orig_h'], $clw, $clh, $this->bodyBackgroundImage['resize'], $this->bodyBackgroundImage['x_repeat'], $this->bodyBackgroundImage['y_repeat']); + foreach ($this->backgroundLayers($this->bodyBackgroundImage) as $bg) { + if (isset($bg['gradient']) && $bg['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + $s .= $this->gradient->Gradient($clx, $cly, $clw, $clh, $g['type'], $g['stops'], $g['colorspace'], $g['coords'], $g['extend'], true); + } + } elseif ($bg['image_id']) { // Background pattern + $n = count($this->patterns) + 1; + // If using resize, uses TrimBox (not including the bleed) + list($orig_w, $orig_h, $x_repeat, $y_repeat) = $this->_resizeBackgroundImage($bg['orig_w'], $bg['orig_h'], $clw, $clh, $bg['resize'], $bg['x_repeat'], $bg['y_repeat']); - $this->patterns[$n] = ['x' => $clx, 'y' => $cly, 'w' => $clw, 'h' => $clh, 'pgh' => $this->h, 'image_id' => $this->bodyBackgroundImage['image_id'], 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $this->bodyBackgroundImage['x_pos'], 'y_pos' => $this->bodyBackgroundImage['y_pos'], 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'itype' => $this->bodyBackgroundImage['itype']]; - if (($this->bodyBackgroundImage['opacity'] > 0 || $this->bodyBackgroundImage['opacity'] === '0') && $this->bodyBackgroundImage['opacity'] < 1) { - $opac = $this->SetAlpha($this->bodyBackgroundImage['opacity'], 'Normal', true); - } else { - $opac = ''; + $this->patterns[$n] = ['x' => $clx, 'y' => $cly, 'w' => $clw, 'h' => $clh, 'pgh' => $this->h, 'image_id' => $bg['image_id'], 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $bg['x_pos'], 'y_pos' => $bg['y_pos'], 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'itype' => $bg['itype']]; + if (($bg['opacity'] > 0 || $bg['opacity'] === '0') && $bg['opacity'] < 1) { + $opac = $this->SetAlpha($bg['opacity'], 'Normal', true); + } else { + $opac = ''; + } + $s .= sprintf('q /Pattern cs /P%d scn %s %.3F %.3F %.3F %.3F re f Q', $n, $opac, ($clx * Mpdf::SCALE), ($cly * Mpdf::SCALE), $clw * Mpdf::SCALE, $clh * Mpdf::SCALE) . "\n"; } - $s .= sprintf('q /Pattern cs /P%d scn %s %.3F %.3F %.3F %.3F re f Q', $n, $opac, ($clx * Mpdf::SCALE), ($cly * Mpdf::SCALE), $clw * Mpdf::SCALE, $clh * Mpdf::SCALE) . "\n"; } } /* -- END BACKGROUNDS -- */ @@ -13295,9 +13354,31 @@ function TableHeaderFooter($content = '', $tablestartpage = '', $tablestartcolum } if (isset($tablehf['background-image']) && $paintcell) { - if ($tablehf['background-image']['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $tablehf['background-image']['gradient'])) { - $g = $this->gradient->parseMozGradient($tablehf['background-image']['gradient']); - if ($g) { + // The layers of a list are drawn in their order, so its gradients go in with its images + $gradientSlot = isset($tablehf['background-image']['layers']) ? 8 : 7; + foreach ($this->backgroundLayers($tablehf['background-image']) as $bg) { + if ($bg['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + if ($table['borders_separate']) { + $px = $x + ($table['border_spacing_H'] / 2); + $py = $y + ($table['border_spacing_V'] / 2); + $pw = $w - $table['border_spacing_H']; + $ph = $h - $table['border_spacing_V']; + } else { + $px = $x; + $py = $y; + $pw = $w; + $ph = $h; + } + if ($this->ColActive) { + $this->gradient->Gradient($px, $py, $pw, $ph, $g['type'], $g['stops'], $g['colorspace'], $g['coords'], $g['extend']); + } else { + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + } + } + } elseif ($bg['image_id']) { // Background pattern + $n = count($this->patterns) + 1; if ($table['borders_separate']) { $px = $x + ($table['border_spacing_H'] / 2); $py = $y + ($table['border_spacing_V'] / 2); @@ -13310,35 +13391,17 @@ function TableHeaderFooter($content = '', $tablestartpage = '', $tablestartcolum $ph = $h; } if ($this->ColActive) { - $this->gradient->Gradient($px, $py, $pw, $ph, $g['type'], $g['stops'], $g['colorspace'], $g['coords'], $g['extend']); - } else { - $this->tableBackgrounds[$level * 9 + 7][] = ['gradient' => true, 'x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; - } - } - } elseif ($tablehf['background-image']['image_id']) { // Background pattern - $n = count($this->patterns) + 1; - if ($table['borders_separate']) { - $px = $x + ($table['border_spacing_H'] / 2); - $py = $y + ($table['border_spacing_V'] / 2); - $pw = $w - $table['border_spacing_H']; - $ph = $h - $table['border_spacing_V']; - } else { - $px = $x; - $py = $y; - $pw = $w; - $ph = $h; - } - if ($this->ColActive) { - list($orig_w, $orig_h, $x_repeat, $y_repeat) = $this->_resizeBackgroundImage($tablehf['background-image']['orig_w'], $tablehf['background-image']['orig_h'], $pw, $ph, $tablehf['background-image']['resize'], $tablehf['background-image']['x_repeat'], $tablehf['background-image']['y_repeat']); - $this->patterns[$n] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'pgh' => $this->h, 'image_id' => $tablehf['background-image']['image_id'], 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $tablehf['background-image']['x_pos'], 'y_pos' => $tablehf['background-image']['y_pos'], 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'itype' => $tablehf['background-image']['itype']]; - if ($tablehf['background-image']['opacity'] > 0 && $tablehf['background-image']['opacity'] < 1) { - $opac = $this->SetAlpha($tablehf['background-image']['opacity'], 'Normal', true); + list($orig_w, $orig_h, $x_repeat, $y_repeat) = $this->_resizeBackgroundImage($bg['orig_w'], $bg['orig_h'], $pw, $ph, $bg['resize'], $bg['x_repeat'], $bg['y_repeat']); + $this->patterns[$n] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'pgh' => $this->h, 'image_id' => $bg['image_id'], 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $bg['x_pos'], 'y_pos' => $bg['y_pos'], 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'itype' => $bg['itype']]; + if ($bg['opacity'] > 0 && $bg['opacity'] < 1) { + $opac = $this->SetAlpha($bg['opacity'], 'Normal', true); + } else { + $opac = ''; + } + $this->writer->write(sprintf('q /Pattern cs /P%d scn %s %.3F %.3F %.3F %.3F re f Q', $n, $opac, $px * Mpdf::SCALE, ($this->h - $py) * Mpdf::SCALE, $pw * Mpdf::SCALE, -$ph * Mpdf::SCALE)); } else { - $opac = ''; + $this->tableBackgrounds[$level * 9 + 8][] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'image_id' => $bg['image_id'], 'orig_w' => $bg['orig_w'], 'orig_h' => $bg['orig_h'], 'x_pos' => $bg['x_pos'], 'y_pos' => $bg['y_pos'], 'x_repeat' => $bg['x_repeat'], 'y_repeat' => $bg['y_repeat'], 'clippath' => '', 'resize' => $bg['resize'], 'opacity' => $bg['opacity'], 'itype' => $bg['itype']]; } - $this->writer->write(sprintf('q /Pattern cs /P%d scn %s %.3F %.3F %.3F %.3F re f Q', $n, $opac, $px * Mpdf::SCALE, ($this->h - $py) * Mpdf::SCALE, $pw * Mpdf::SCALE, -$ph * Mpdf::SCALE)); - } else { - $this->tableBackgrounds[$level * 9 + 8][] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'image_id' => $tablehf['background-image']['image_id'], 'orig_w' => $tablehf['background-image']['orig_w'], 'orig_h' => $tablehf['background-image']['orig_h'], 'x_pos' => $tablehf['background-image']['x_pos'], 'y_pos' => $tablehf['background-image']['y_pos'], 'x_repeat' => $tablehf['background-image']['x_repeat'], 'y_repeat' => $tablehf['background-image']['y_repeat'], 'clippath' => '', 'resize' => $tablehf['background-image']['resize'], 'opacity' => $tablehf['background-image']['opacity'], 'itype' => $tablehf['background-image']['itype']]; } } } @@ -13476,9 +13539,42 @@ function TableHeaderFooter($content = '', $tablestartpage = '', $tablestartcolum } if (isset($content[$i][0]['trbackground-images']) && ($colctr == 1 || $table['borders_separate'])) { - if ($content[$i][0]['trbackground-images']['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $content[$i][0]['trbackground-images']['gradient'])) { - $g = $this->gradient->parseMozGradient($content[$i][0]['trbackground-images']['gradient']); - if ($g) { + // The layers of a list are drawn in their order, so its gradients go in with its images + $gradientSlot = isset($content[$i][0]['trbackground-images']['layers']) ? 5 : 4; + foreach ($this->backgroundLayers($content[$i][0]['trbackground-images']) as $bg) { + if ($bg['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + $gx = $x0; + $gy = $y; + $gh = $h; + $gw = $table['w'] - ($table['max_cell_border_width']['L'] / 2) - ($table['max_cell_border_width']['R'] / 2) - $table['margin']['L'] - $table['margin']['R']; + if ($table['borders_separate']) { + $gw -= ($table['padding']['L'] + $table['border_details']['L']['w'] + $table['padding']['R'] + $table['border_details']['R']['w'] + $table['border_spacing_H']); + $clx = $x + ($table['border_spacing_H'] / 2); + $cly = $y + ($table['border_spacing_V'] / 2); + $clw = $w - $table['border_spacing_H']; + $clh = $h - $table['border_spacing_V']; + // Set clipping path + $s = $this->_setClippingPath($clx, $cly, $clw, $clh); // mPDF 6 + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => $s]; + } else { + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + } + } + } else { + $image_id = $bg['image_id']; + $orig_w = $bg['orig_w']; + $orig_h = $bg['orig_h']; + $x_pos = $bg['x_pos']; + $y_pos = $bg['y_pos']; + $x_repeat = $bg['x_repeat']; + $y_repeat = $bg['y_repeat']; + $resize = $bg['resize']; + $opacity = $bg['opacity']; + $itype = $bg['itype']; + + $clippath = ''; $gx = $x0; $gy = $y; $gh = $h; @@ -13491,40 +13587,11 @@ function TableHeaderFooter($content = '', $tablestartpage = '', $tablestartcolum $clh = $h - $table['border_spacing_V']; // Set clipping path $s = $this->_setClippingPath($clx, $cly, $clw, $clh); // mPDF 6 - $this->tableBackgrounds[$level * 9 + 4][] = ['gradient' => true, 'x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => $s]; + $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => $s, 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } else { - $this->tableBackgrounds[$level * 9 + 4][] = ['gradient' => true, 'x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } } - } else { - $image_id = $content[$i][0]['trbackground-images']['image_id']; - $orig_w = $content[$i][0]['trbackground-images']['orig_w']; - $orig_h = $content[$i][0]['trbackground-images']['orig_h']; - $x_pos = $content[$i][0]['trbackground-images']['x_pos']; - $y_pos = $content[$i][0]['trbackground-images']['y_pos']; - $x_repeat = $content[$i][0]['trbackground-images']['x_repeat']; - $y_repeat = $content[$i][0]['trbackground-images']['y_repeat']; - $resize = $content[$i][0]['trbackground-images']['resize']; - $opacity = $content[$i][0]['trbackground-images']['opacity']; - $itype = $content[$i][0]['trbackground-images']['itype']; - - $clippath = ''; - $gx = $x0; - $gy = $y; - $gh = $h; - $gw = $table['w'] - ($table['max_cell_border_width']['L'] / 2) - ($table['max_cell_border_width']['R'] / 2) - $table['margin']['L'] - $table['margin']['R']; - if ($table['borders_separate']) { - $gw -= ($table['padding']['L'] + $table['border_details']['L']['w'] + $table['padding']['R'] + $table['border_details']['R']['w'] + $table['border_spacing_H']); - $clx = $x + ($table['border_spacing_H'] / 2); - $cly = $y + ($table['border_spacing_V'] / 2); - $clw = $w - $table['border_spacing_H']; - $clh = $h - $table['border_spacing_V']; - // Set clipping path - $s = $this->_setClippingPath($clx, $cly, $clw, $clh); // mPDF 6 - $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => $s, 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; - } else { - $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; - } } } } @@ -18754,146 +18821,153 @@ function PaintDivBB($divider = '', $blockstate = 0, $blvl = 0) } if (isset($this->blk[$blvl]['background-image'])) { - if (isset($this->blk[$blvl]['background-image']['gradient']) && $this->blk[$blvl]['background-image']['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $this->blk[$blvl]['background-image']['gradient'])) { - $g = $this->gradient->parseMozGradient($this->blk[$blvl]['background-image']['gradient']); - if ($g) { - $gx = $x0; - $gy = $y0; + // A gradient layer is sized from the whole box + $bgw = $w; + $bgh = $h; + foreach ($this->backgroundLayers($this->blk[$blvl]['background-image']) as $bg) { + $w = $bgw; + $h = $bgh; + if (isset($bg['gradient']) && $bg['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + $gx = $x0; + $gy = $y0; + // origin specifies the background-positioning-area (bpa) + if ($bg['origin'] == 'padding-box') { + $gx += $this->blk[$blvl]['border_left']['w']; + $w -= ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['border_right']['w']); + if ($this->blk[$blvl]['border_top'] && $divider != 'pagetop' && !$continuingpage) { + $gy += $this->blk[$blvl]['border_top']['w']; + } + if ($this->blk[$blvl]['border_bottom'] && $blockstate != 1 && $divider != 'pagebottom') { + $gy1 = $y1 - $this->blk[$blvl]['border_bottom']['w']; + } else { + $gy1 = $y1; + } + $h = $gy1 - $gy; + } elseif ($bg['origin'] == 'content-box') { + $gx += $this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left']; + $w -= ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left'] + $this->blk[$blvl]['border_right']['w'] + $this->blk[$blvl]['padding_right']); + if ($this->blk[$blvl]['border_top'] && $divider != 'pagetop' && !$continuingpage) { + $gy += $this->blk[$blvl]['border_top']['w'] + $this->blk[$blvl]['padding_top']; + } + if ($this->blk[$blvl]['border_bottom'] && $blockstate != 1 && $divider != 'pagebottom') { + $gy1 = $y1 - ($this->blk[$blvl]['border_bottom']['w'] + $this->blk[$blvl]['padding_bottom']); + } else { + $gy1 = $y1 - $this->blk[$blvl]['padding_bottom']; + } + $h = $gy1 - $gy; + } + + if (isset($bg['size']['w']) && $bg['size']['w']) { + $size = $bg['size']; + if ($size['w'] != 'contain' && $size['w'] != 'cover') { + if (stristr($size['w'], '%')) { + $size['w'] = (float) $size['w']; + $size['w'] /= 100; + $w *= $size['w']; + } elseif ($size['w'] != 'auto') { + $w = $size['w']; + } + if (stristr($size['h'], '%')) { + $size['h'] = (float) $size['h']; + $size['h'] /= 100; + $h *= $size['h']; + } elseif ($size['h'] != 'auto') { + $h = $size['h']; + } + } + } + $this->pageBackgrounds[$blvl][] = [ + 'gradient' => true, + 'x' => $gx, + 'y' => $gy, + 'w' => $w, + 'h' => $h, + 'gradtype' => $g['type'], + 'stops' => $g['stops'], + 'colorspace' => $g['colorspace'], + 'coords' => $g['coords'], + 'extend' => $g['extend'], + 'clippath' => $s, + 'visibility' => $this->visibility, + 'z-index' => $this->current_layer + ]; + } + + } else { + + $image_id = $bg['image_id']; + $orig_w = $bg['orig_w']; + $orig_h = $bg['orig_h']; + $x_pos = $bg['x_pos']; + $y_pos = $bg['y_pos']; + $x_repeat = $bg['x_repeat']; + $y_repeat = $bg['y_repeat']; + $resize = $bg['resize']; + $opacity = $bg['opacity']; + $itype = $bg['itype']; + $size = $bg['size']; // origin specifies the background-positioning-area (bpa) - if ($this->blk[$blvl]['background-image']['origin'] == 'padding-box') { - $gx += $this->blk[$blvl]['border_left']['w']; - $w -= ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['border_right']['w']); + + $bpa = ['x' => $x0, 'y' => $y0, 'w' => $w, 'h' => $h]; + + if ($bg['origin'] == 'padding-box') { + + $bpa['x'] = $x0 + $this->blk[$blvl]['border_left']['w']; + $bpa['w'] = $w - ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['border_right']['w']); if ($this->blk[$blvl]['border_top'] && $divider != 'pagetop' && !$continuingpage) { - $gy += $this->blk[$blvl]['border_top']['w']; + $bpa['y'] = $y0 + $this->blk[$blvl]['border_top']['w']; + } else { + $bpa['y'] = $y0; } if ($this->blk[$blvl]['border_bottom'] && $blockstate != 1 && $divider != 'pagebottom') { - $gy1 = $y1 - $this->blk[$blvl]['border_bottom']['w']; + $bpay = $y1 - $this->blk[$blvl]['border_bottom']['w']; } else { - $gy1 = $y1; + $bpay = $y1; } - $h = $gy1 - $gy; - } elseif ($this->blk[$blvl]['background-image']['origin'] == 'content-box') { - $gx += $this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left']; - $w -= ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left'] + $this->blk[$blvl]['border_right']['w'] + $this->blk[$blvl]['padding_right']); + $bpa['h'] = $bpay - $bpa['y']; + + } elseif ($bg['origin'] == 'content-box') { + + $bpa['x'] = $x0 + $this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left']; + $bpa['w'] = $w - ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left'] + $this->blk[$blvl]['border_right']['w'] + $this->blk[$blvl]['padding_right']); if ($this->blk[$blvl]['border_top'] && $divider != 'pagetop' && !$continuingpage) { - $gy += $this->blk[$blvl]['border_top']['w'] + $this->blk[$blvl]['padding_top']; + $bpa['y'] = $y0 + $this->blk[$blvl]['border_top']['w'] + $this->blk[$blvl]['padding_top']; + } else { + $bpa['y'] = $y0 + $this->blk[$blvl]['padding_top']; } if ($this->blk[$blvl]['border_bottom'] && $blockstate != 1 && $divider != 'pagebottom') { - $gy1 = $y1 - ($this->blk[$blvl]['border_bottom']['w'] + $this->blk[$blvl]['padding_bottom']); + $bpay = $y1 - ($this->blk[$blvl]['border_bottom']['w'] + $this->blk[$blvl]['padding_bottom']); } else { - $gy1 = $y1 - $this->blk[$blvl]['padding_bottom']; + $bpay = $y1 - $this->blk[$blvl]['padding_bottom']; } - $h = $gy1 - $gy; - } + $bpa['h'] = $bpay - $bpa['y']; - if (isset($this->blk[$blvl]['background-image']['size']['w']) && $this->blk[$blvl]['background-image']['size']['w']) { - $size = $this->blk[$blvl]['background-image']['size']; - if ($size['w'] != 'contain' && $size['w'] != 'cover') { - if (stristr($size['w'], '%')) { - $size['w'] = (float) $size['w']; - $size['w'] /= 100; - $w *= $size['w']; - } elseif ($size['w'] != 'auto') { - $w = $size['w']; - } - if (stristr($size['h'], '%')) { - $size['h'] = (float) $size['h']; - $size['h'] /= 100; - $h *= $size['h']; - } elseif ($size['h'] != 'auto') { - $h = $size['h']; - } - } } + $this->pageBackgrounds[$blvl][] = [ - 'gradient' => true, - 'x' => $gx, - 'y' => $gy, + 'x' => $x0, + 'y' => $y0, 'w' => $w, 'h' => $h, - 'gradtype' => $g['type'], - 'stops' => $g['stops'], - 'colorspace' => $g['colorspace'], - 'coords' => $g['coords'], - 'extend' => $g['extend'], + 'image_id' => $image_id, + 'orig_w' => $orig_w, + 'orig_h' => $orig_h, + 'x_pos' => $x_pos, + 'y_pos' => $y_pos, + 'x_repeat' => $x_repeat, + 'y_repeat' => $y_repeat, 'clippath' => $s, + 'resize' => $resize, + 'opacity' => $opacity, + 'itype' => $itype, 'visibility' => $this->visibility, - 'z-index' => $this->current_layer + 'z-index' => $this->current_layer, + 'size' => $size, + 'bpa' => $bpa ]; } - - } else { - - $image_id = $this->blk[$blvl]['background-image']['image_id']; - $orig_w = $this->blk[$blvl]['background-image']['orig_w']; - $orig_h = $this->blk[$blvl]['background-image']['orig_h']; - $x_pos = $this->blk[$blvl]['background-image']['x_pos']; - $y_pos = $this->blk[$blvl]['background-image']['y_pos']; - $x_repeat = $this->blk[$blvl]['background-image']['x_repeat']; - $y_repeat = $this->blk[$blvl]['background-image']['y_repeat']; - $resize = $this->blk[$blvl]['background-image']['resize']; - $opacity = $this->blk[$blvl]['background-image']['opacity']; - $itype = $this->blk[$blvl]['background-image']['itype']; - $size = $this->blk[$blvl]['background-image']['size']; - // origin specifies the background-positioning-area (bpa) - - $bpa = ['x' => $x0, 'y' => $y0, 'w' => $w, 'h' => $h]; - - if ($this->blk[$blvl]['background-image']['origin'] == 'padding-box') { - - $bpa['x'] = $x0 + $this->blk[$blvl]['border_left']['w']; - $bpa['w'] = $w - ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['border_right']['w']); - if ($this->blk[$blvl]['border_top'] && $divider != 'pagetop' && !$continuingpage) { - $bpa['y'] = $y0 + $this->blk[$blvl]['border_top']['w']; - } else { - $bpa['y'] = $y0; - } - if ($this->blk[$blvl]['border_bottom'] && $blockstate != 1 && $divider != 'pagebottom') { - $bpay = $y1 - $this->blk[$blvl]['border_bottom']['w']; - } else { - $bpay = $y1; - } - $bpa['h'] = $bpay - $bpa['y']; - - } elseif ($this->blk[$blvl]['background-image']['origin'] == 'content-box') { - - $bpa['x'] = $x0 + $this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left']; - $bpa['w'] = $w - ($this->blk[$blvl]['border_left']['w'] + $this->blk[$blvl]['padding_left'] + $this->blk[$blvl]['border_right']['w'] + $this->blk[$blvl]['padding_right']); - if ($this->blk[$blvl]['border_top'] && $divider != 'pagetop' && !$continuingpage) { - $bpa['y'] = $y0 + $this->blk[$blvl]['border_top']['w'] + $this->blk[$blvl]['padding_top']; - } else { - $bpa['y'] = $y0 + $this->blk[$blvl]['padding_top']; - } - if ($this->blk[$blvl]['border_bottom'] && $blockstate != 1 && $divider != 'pagebottom') { - $bpay = $y1 - ($this->blk[$blvl]['border_bottom']['w'] + $this->blk[$blvl]['padding_bottom']); - } else { - $bpay = $y1 - $this->blk[$blvl]['padding_bottom']; - } - $bpa['h'] = $bpay - $bpa['y']; - - } - - $this->pageBackgrounds[$blvl][] = [ - 'x' => $x0, - 'y' => $y0, - 'w' => $w, - 'h' => $h, - 'image_id' => $image_id, - 'orig_w' => $orig_w, - 'orig_h' => $orig_h, - 'x_pos' => $x_pos, - 'y_pos' => $y_pos, - 'x_repeat' => $x_repeat, - 'y_repeat' => $y_repeat, - 'clippath' => $s, - 'resize' => $resize, - 'opacity' => $opacity, - 'itype' => $itype, - 'visibility' => $this->visibility, - 'z-index' => $this->current_layer, - 'size' => $size, - 'bpa' => $bpa - ]; } } /* -- END BACKGROUNDS -- */ @@ -19388,6 +19462,8 @@ function setCSS($arrayaux, $type = '', $tag = '') break; case 'BACKGROUND-CLIP': + // Every layer is drawn under the clip of the bottom one, which the colour takes + $v = BackgroundLayers::bottomValue($v, isset($arrayaux['BACKGROUND-IMAGE']) ? $arrayaux['BACKGROUND-IMAGE'] : ''); if (strtoupper($v) == 'PADDING-BOX') { $this->blk[$this->blklvl]['background_clip'] = 'padding-box'; } elseif (strtoupper($v) == 'CONTENT-BOX') { @@ -23457,23 +23533,27 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl } if (isset($table['background-image'])) { - if ($table['background-image']['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $table['background-image']['gradient'])) { - $g = $this->gradient->parseMozGradient($table['background-image']['gradient']); - if ($g) { - $this->tableBackgrounds[$level * 9 + 1][] = ['gradient' => true, 'x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + // The layers of a list are drawn in their order, so its gradients go in with its images + $gradientSlot = isset($table['background-image']['layers']) ? 2 : 1; + foreach ($this->backgroundLayers($table['background-image']) as $bg) { + if ($bg['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + } + } else { + $image_id = $bg['image_id']; + $orig_w = $bg['orig_w']; + $orig_h = $bg['orig_h']; + $x_pos = $bg['x_pos']; + $y_pos = $bg['y_pos']; + $x_repeat = $bg['x_repeat']; + $y_repeat = $bg['y_repeat']; + $resize = $bg['resize']; + $opacity = $bg['opacity']; + $itype = $bg['itype']; + $this->tableBackgrounds[$level * 9 + 2][] = ['x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } - } else { - $image_id = $table['background-image']['image_id']; - $orig_w = $table['background-image']['orig_w']; - $orig_h = $table['background-image']['orig_h']; - $x_pos = $table['background-image']['x_pos']; - $y_pos = $table['background-image']['y_pos']; - $x_repeat = $table['background-image']['x_repeat']; - $y_repeat = $table['background-image']['y_repeat']; - $resize = $table['background-image']['resize']; - $opacity = $table['background-image']['opacity']; - $itype = $table['background-image']['itype']; - $this->tableBackgrounds[$level * 9 + 2][] = ['x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } } /* -- END BACKGROUNDS -- */ @@ -23805,9 +23885,31 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl } if (isset($cell['background-image']) && $paintcell) { - if (isset($cell['background-image']['gradient']) && $cell['background-image']['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $cell['background-image']['gradient'])) { - $g = $this->gradient->parseMozGradient($cell['background-image']['gradient']); - if ($g) { + // The layers of a list are drawn in their order, so its gradients go in with its images + $gradientSlot = isset($cell['background-image']['layers']) ? 8 : 7; + foreach ($this->backgroundLayers($cell['background-image']) as $bg) { + if (isset($bg['gradient']) && $bg['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + if ($table['borders_separate']) { + $px = $x + ($table['border_spacing_H'] / 2); + $py = $y + ($table['border_spacing_V'] / 2); + $pw = $w - $table['border_spacing_H']; + $ph = $h - $table['border_spacing_V']; + } else { + $px = $x; + $py = $y; + $pw = $w; + $ph = $h; + } + if ($this->ColActive) { + $this->gradient->Gradient($px, $py, $pw, $ph, $g['type'], $g['stops'], $g['colorspace'], $g['coords'], $g['extend']); + } else { + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + } + } + } elseif (isset($bg['image_id']) && $bg['image_id']) { // Background pattern + $n = count($this->patterns) + 1; if ($table['borders_separate']) { $px = $x + ($table['border_spacing_H'] / 2); $py = $y + ($table['border_spacing_V'] / 2); @@ -23820,45 +23922,27 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl $ph = $h; } if ($this->ColActive) { - $this->gradient->Gradient($px, $py, $pw, $ph, $g['type'], $g['stops'], $g['colorspace'], $g['coords'], $g['extend']); - } else { - $this->tableBackgrounds[$level * 9 + 7][] = ['gradient' => true, 'x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; - } - } - } elseif (isset($cell['background-image']['image_id']) && $cell['background-image']['image_id']) { // Background pattern - $n = count($this->patterns) + 1; - if ($table['borders_separate']) { - $px = $x + ($table['border_spacing_H'] / 2); - $py = $y + ($table['border_spacing_V'] / 2); - $pw = $w - $table['border_spacing_H']; - $ph = $h - $table['border_spacing_V']; - } else { - $px = $x; - $py = $y; - $pw = $w; - $ph = $h; - } - if ($this->ColActive) { - list($orig_w, $orig_h, $x_repeat, $y_repeat) = $this->_resizeBackgroundImage($cell['background-image']['orig_w'], $cell['background-image']['orig_h'], $pw, $ph, $cell['background-image']['resize'], $cell['background-image']['x_repeat'], $cell['background-image']['y_repeat']); - $this->patterns[$n] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'pgh' => $this->h, 'image_id' => $cell['background-image']['image_id'], 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $cell['background-image']['x_pos'], 'y_pos' => $cell['background-image']['y_pos'], 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat]; - if ($cell['background-image']['opacity'] > 0 && $cell['background-image']['opacity'] < 1) { - $opac = $this->SetAlpha($cell['background-image']['opacity'], 'Normal', true); + list($orig_w, $orig_h, $x_repeat, $y_repeat) = $this->_resizeBackgroundImage($bg['orig_w'], $bg['orig_h'], $pw, $ph, $bg['resize'], $bg['x_repeat'], $bg['y_repeat']); + $this->patterns[$n] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'pgh' => $this->h, 'image_id' => $bg['image_id'], 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $bg['x_pos'], 'y_pos' => $bg['y_pos'], 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat]; + if ($bg['opacity'] > 0 && $bg['opacity'] < 1) { + $opac = $this->SetAlpha($bg['opacity'], 'Normal', true); + } else { + $opac = ''; + } + $this->writer->write(sprintf('q /Pattern cs /P%d scn %s %.3F %.3F %.3F %.3F re f Q', $n, $opac, $px * Mpdf::SCALE, ($this->h - $py) * Mpdf::SCALE, $pw * Mpdf::SCALE, -$ph * Mpdf::SCALE)); } else { - $opac = ''; + $image_id = $bg['image_id']; + $orig_w = $bg['orig_w']; + $orig_h = $bg['orig_h']; + $x_pos = $bg['x_pos']; + $y_pos = $bg['y_pos']; + $x_repeat = $bg['x_repeat']; + $y_repeat = $bg['y_repeat']; + $resize = $bg['resize']; + $opacity = $bg['opacity']; + $itype = $bg['itype']; + $this->tableBackgrounds[$level * 9 + 8][] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } - $this->writer->write(sprintf('q /Pattern cs /P%d scn %s %.3F %.3F %.3F %.3F re f Q', $n, $opac, $px * Mpdf::SCALE, ($this->h - $py) * Mpdf::SCALE, $pw * Mpdf::SCALE, -$ph * Mpdf::SCALE)); - } else { - $image_id = $cell['background-image']['image_id']; - $orig_w = $cell['background-image']['orig_w']; - $orig_h = $cell['background-image']['orig_h']; - $x_pos = $cell['background-image']['x_pos']; - $y_pos = $cell['background-image']['y_pos']; - $x_repeat = $cell['background-image']['x_repeat']; - $y_repeat = $cell['background-image']['y_repeat']; - $resize = $cell['background-image']['resize']; - $opacity = $cell['background-image']['opacity']; - $itype = $cell['background-image']['itype']; - $this->tableBackgrounds[$level * 9 + 8][] = ['x' => $px, 'y' => $py, 'w' => $pw, 'h' => $ph, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } } } @@ -24151,9 +24235,41 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl } } if (isset($table['trbackground-images'][$i]) && ($j == 0 || $table['borders_separate'])) { - if (isset($table['trbackground-images'][$i]['gradient']) && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $table['trbackground-images'][$i]['gradient'])) { - $g = $this->gradient->parseMozGradient($table['trbackground-images'][$i]['gradient']); - if ($g) { + // The layers of a list are drawn in their order, so its gradients go in with its images + $gradientSlot = isset($table['trbackground-images'][$i]['layers']) ? 5 : 4; + foreach ($this->backgroundLayers($table['trbackground-images'][$i]) as $bg) { + if (isset($bg['gradient']) && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + $gx = $x0; + $gy = $y; + $gh = $h; + $gw = $table['w'] - ($table['max_cell_border_width']['L'] / 2) - ($table['max_cell_border_width']['R'] / 2) - $table['margin']['L'] - $table['margin']['R']; + if ($table['borders_separate']) { + $gw -= ($table['padding']['L'] + $table['border_details']['L']['w'] + $table['padding']['R'] + $table['border_details']['R']['w'] + $table['border_spacing_H']); + $clx = $x + ($table['border_spacing_H'] / 2); + $cly = $y + ($table['border_spacing_V'] / 2); + $clw = $w - $table['border_spacing_H']; + $clh = $h - $table['border_spacing_V']; + // Set clipping path + $s = $this->_setClippingPath($clx, $cly, $clw, $clh); // mPDF 6 + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => $s]; + } else { + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + } + } + } else { + $image_id = $bg['image_id']; + $orig_w = $bg['orig_w']; + $orig_h = $bg['orig_h']; + $x_pos = $bg['x_pos']; + $y_pos = $bg['y_pos']; + $x_repeat = $bg['x_repeat']; + $y_repeat = $bg['y_repeat']; + $resize = $bg['resize']; + $opacity = $bg['opacity']; + $itype = $bg['itype']; + $clippath = ''; $gx = $x0; $gy = $y; $gh = $h; @@ -24166,39 +24282,11 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl $clh = $h - $table['border_spacing_V']; // Set clipping path $s = $this->_setClippingPath($clx, $cly, $clw, $clh); // mPDF 6 - $this->tableBackgrounds[$level * 9 + 4][] = ['gradient' => true, 'x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => $s]; + $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => $s, 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } else { - $this->tableBackgrounds[$level * 9 + 4][] = ['gradient' => true, 'x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } } - } else { - $image_id = $table['trbackground-images'][$i]['image_id']; - $orig_w = $table['trbackground-images'][$i]['orig_w']; - $orig_h = $table['trbackground-images'][$i]['orig_h']; - $x_pos = $table['trbackground-images'][$i]['x_pos']; - $y_pos = $table['trbackground-images'][$i]['y_pos']; - $x_repeat = $table['trbackground-images'][$i]['x_repeat']; - $y_repeat = $table['trbackground-images'][$i]['y_repeat']; - $resize = $table['trbackground-images'][$i]['resize']; - $opacity = $table['trbackground-images'][$i]['opacity']; - $itype = $table['trbackground-images'][$i]['itype']; - $clippath = ''; - $gx = $x0; - $gy = $y; - $gh = $h; - $gw = $table['w'] - ($table['max_cell_border_width']['L'] / 2) - ($table['max_cell_border_width']['R'] / 2) - $table['margin']['L'] - $table['margin']['R']; - if ($table['borders_separate']) { - $gw -= ($table['padding']['L'] + $table['border_details']['L']['w'] + $table['padding']['R'] + $table['border_details']['R']['w'] + $table['border_spacing_H']); - $clx = $x + ($table['border_spacing_H'] / 2); - $cly = $y + ($table['border_spacing_V'] / 2); - $clw = $w - $table['border_spacing_H']; - $clh = $h - $table['border_spacing_V']; - // Set clipping path - $s = $this->_setClippingPath($clx, $cly, $clw, $clh); // mPDF 6 - $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx + ($table['border_spacing_H'] / 2), 'y' => $gy + ($table['border_spacing_V'] / 2), 'w' => $gw - $table['border_spacing_V'], 'h' => $gh - $table['border_spacing_H'], 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => $s, 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; - } else { - $this->tableBackgrounds[$level * 9 + 5][] = ['x' => $gx, 'y' => $gy, 'w' => $gw, 'h' => $gh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; - } } } } @@ -24366,23 +24454,27 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl } if (isset($table['background-image'])) { - if (isset($table['background-image']['gradient']) && $table['background-image']['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $table['background-image']['gradient'])) { - $g = $this->gradient->parseMozGradient($table['background-image']['gradient']); - if ($g) { - $this->tableBackgrounds[$level * 9 + 1][] = ['gradient' => true, 'x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + // The layers of a list are drawn in their order, so its gradients go in with its images + $gradientSlot = isset($table['background-image']['layers']) ? 2 : 1; + foreach ($this->backgroundLayers($table['background-image']) as $bg) { + if (isset($bg['gradient']) && $bg['gradient'] && preg_match('/(-moz-)*(repeating-)*(linear|radial)-gradient/', $bg['gradient'])) { + $g = $this->gradient->parseMozGradient($bg['gradient']); + if ($g) { + $this->tableBackgrounds[$level * 9 + $gradientSlot][] = ['gradient' => true, 'x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'gradtype' => $g['type'], 'stops' => $g['stops'], 'colorspace' => $g['colorspace'], 'coords' => $g['coords'], 'extend' => $g['extend'], 'clippath' => '']; + } + } else { + $image_id = $bg['image_id']; + $orig_w = $bg['orig_w']; + $orig_h = $bg['orig_h']; + $x_pos = $bg['x_pos']; + $y_pos = $bg['y_pos']; + $x_repeat = $bg['x_repeat']; + $y_repeat = $bg['y_repeat']; + $resize = $bg['resize']; + $opacity = $bg['opacity']; + $itype = $bg['itype']; + $this->tableBackgrounds[$level * 9 + 2][] = ['x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } - } else { - $image_id = $table['background-image']['image_id']; - $orig_w = $table['background-image']['orig_w']; - $orig_h = $table['background-image']['orig_h']; - $x_pos = $table['background-image']['x_pos']; - $y_pos = $table['background-image']['y_pos']; - $x_repeat = $table['background-image']['x_repeat']; - $y_repeat = $table['background-image']['y_repeat']; - $resize = $table['background-image']['resize']; - $opacity = $table['background-image']['opacity']; - $itype = $table['background-image']['itype']; - $this->tableBackgrounds[$level * 9 + 2][] = ['x' => $bx, 'y' => $by, 'w' => $bw, 'h' => $bh, 'image_id' => $image_id, 'orig_w' => $orig_w, 'orig_h' => $orig_h, 'x_pos' => $x_pos, 'y_pos' => $y_pos, 'x_repeat' => $x_repeat, 'y_repeat' => $y_repeat, 'clippath' => '', 'resize' => $resize, 'opacity' => $opacity, 'itype' => $itype]; } } /* -- END BACKGROUNDS -- */ diff --git a/tests/Mpdf/Css/NormalizePropertiesTest.php b/tests/Mpdf/Css/NormalizePropertiesTest.php index afb1ae2a4..83a6014ef 100644 --- a/tests/Mpdf/Css/NormalizePropertiesTest.php +++ b/tests/Mpdf/Css/NormalizePropertiesTest.php @@ -456,9 +456,28 @@ public function providerBackground() 'linear-gradient(to right, red, blue) #fff', ['BACKGROUND-COLOR' => '#fff', 'BACKGROUND-IMAGE' => 'linear-gradient(to right, red, blue)'], ], - 'the colour of the last layer under the first' => [ + 'every layer, with the colour of the last' => [ 'url(a.png) no-repeat, url(b.png) repeat-y #fff', - ['BACKGROUND-COLOR' => '#fff', 'BACKGROUND-IMAGE' => 'a.png', 'BACKGROUND-REPEAT' => 'no-repeat'], + [ + 'BACKGROUND-COLOR' => '#fff', + 'BACKGROUND-IMAGE' => BackgroundLayers::join(['a.png', 'b.png']), + 'BACKGROUND-REPEAT' => BackgroundLayers::join(['no-repeat', 'repeat-y']), + 'BACKGROUND-POSITION' => BackgroundLayers::join(['0% 0%', '0% 0%']), + 'BACKGROUND-SIZE' => BackgroundLayers::join(['auto', 'auto']), + 'BACKGROUND-ORIGIN' => BackgroundLayers::join(['padding-box', 'padding-box']), + 'BACKGROUND-CLIP' => BackgroundLayers::join(['border-box', 'border-box']), + ], + ], + 'a gradient and an image, each with its own parts' => [ + 'linear-gradient(to right, red, rgb(0 0 255)) center / 50% auto content-box, url("a,b.png") no-repeat right bottom', + [ + 'BACKGROUND-IMAGE' => BackgroundLayers::join(['linear-gradient(to right, red, rgb(0,0,255))', 'a,b.png']), + 'BACKGROUND-REPEAT' => BackgroundLayers::join(['repeat', 'no-repeat']), + 'BACKGROUND-POSITION' => BackgroundLayers::join(['50% 50%', '100% 100%']), + 'BACKGROUND-SIZE' => BackgroundLayers::join(['50% auto', 'auto']), + 'BACKGROUND-ORIGIN' => BackgroundLayers::join(['content-box', 'padding-box']), + 'BACKGROUND-CLIP' => BackgroundLayers::join(['content-box', 'border-box']), + ], ], 'none' => [ 'none', @@ -516,6 +535,85 @@ public function providerInvalidBackground() ]; } + /** + * A background longhand with a comma-separated list keeps a value for each layer + * + * @dataProvider providerLayeredLonghand + * + * @param string $property + * @param string $value + * @param string[] $expected Each layer's value + */ + public function testReadsAValueForEachLayer($property, $value, array $expected) + { + $this->assertSame([$property => BackgroundLayers::join($expected)], $this->normalizeProperties->normalize([$property => $value])); + } + + /** + * Background longhand lists and each layer's value + * + * @return array[] + */ + public function providerLayeredLonghand() + { + return [ + 'images, a gradient and none' => ['BACKGROUND-IMAGE', 'url(a.png), linear-gradient(red, blue), none', ['a.png', 'linear-gradient(red, blue)', '']], + 'an image with commas in its address' => ['BACKGROUND-IMAGE', 'url(data:image/png;base64,AB,CD), url("B.png")', ['data:image/png;base64,AB,CD', 'B.png']], + 'repeats' => ['BACKGROUND-REPEAT', 'no-repeat, repeat-x', ['no-repeat', 'repeat-x']], + 'positions' => ['BACKGROUND-POSITION', 'right bottom, 10mm 5mm', ['100% 100%', '10mm 5mm']], + 'sizes' => ['BACKGROUND-SIZE', 'cover, 50% auto, 10mm', ['cover', '50% auto', '10mm']], + 'origins' => ['BACKGROUND-ORIGIN', 'content-box, border-box', ['content-box', 'border-box']], + 'clips' => ['BACKGROUND-CLIP', 'padding-box, border-box', ['padding-box', 'border-box']], + ]; + } + + /** + * A background longhand list with a value that is not a value of the property is dropped + * + * @dataProvider providerInvalidLayeredLonghand + * + * @param string $property + * @param string $value + */ + public function testDropsAnInvalidLayerList($property, $value) + { + $this->assertSame([], $this->normalizeProperties->normalize([$property => $value])); + } + + /** + * Background longhand lists that are not lists of the property's values + * + * @return string[][] + */ + public function providerInvalidLayeredLonghand() + { + return [ + 'a colour among the images' => ['BACKGROUND-IMAGE', 'url(a.png), #fff'], + 'an unknown repeat' => ['BACKGROUND-REPEAT', 'no-repeat, bogus'], + 'an empty position' => ['BACKGROUND-POSITION', 'left,'], + 'three sizes in a layer' => ['BACKGROUND-SIZE', '10mm 10mm 10mm, auto'], + 'two boxes in a layer' => ['BACKGROUND-ORIGIN', 'padding-box content-box, border-box'], + ]; + } + + /** + * The legacy CSS mode reads only the top layer of a list, with the colour of the last + */ + public function testTheLegacyModeReadsOnlyTheTopLayer() + { + $this->mpdf->cssMode = CssMode::LEGACY; + + $result = $this->normalizeProperties->normalize([ + 'BACKGROUND' => 'url(a.png) no-repeat, url(b.png) #fff', + 'BACKGROUND-POSITION' => 'right bottom, left top', + ]); + + $this->assertSame('#fff', $result['BACKGROUND-COLOR']); + $this->assertSame('a.png', $result['BACKGROUND-IMAGE']); + $this->assertSame('no-repeat', $result['BACKGROUND-REPEAT']); + $this->assertArrayNotHasKey('BACKGROUND-POSITION', $result); + } + public function testNonExistentFontFamily() { $result = $this->normalizeProperties->normalize(['FONT-FAMILY' => 'abc']); diff --git a/tests/Mpdf/MultipleBackgroundsTest.php b/tests/Mpdf/MultipleBackgroundsTest.php new file mode 100644 index 000000000..4da0a8181 --- /dev/null +++ b/tests/Mpdf/MultipleBackgroundsTest.php @@ -0,0 +1,211 @@ +paintedLayers('
Text
'); + + $this->assertSame($painted, array_column($layers, 'kind')); + } + + /** + * Backgrounds and what they paint, from the bottom up + * + * @return array[] + */ + public function providerLayerOrder() + { + return [ + 'an image over a gradient' => ['background: url(IMG) no-repeat, linear-gradient(red, blue)', ['shading', 'pattern']], + 'a gradient over an image' => ['background: linear-gradient(red, blue), url(IMG)', ['pattern', 'shading']], + 'two images' => ['background-image: url(IMG), url(IMG); background-repeat: no-repeat', ['pattern', 'pattern']], + 'a gradient over a gradient' => ['background-image: linear-gradient(red, blue), radial-gradient(green, yellow)', ['shading', 'shading']], + 'a layer of none' => ['background-image: url(IMG), none, linear-gradient(red, blue)', ['shading', 'pattern']], + ]; + } + + /** + * The bottom layer of a gradient over a gradient is the one listed last + */ + public function testPaintsTheLastGradientAtTheBottom() + { + $layers = $this->paintedLayers('
Text
'); + + // ShadingType 2 is axial, 3 radial + $this->assertSame(['3', '2'], array_column($layers, 'type')); + } + + /** + * The colour the last layer names is painted under the images + */ + public function testPaintsTheColourOfTheLastLayerUnderTheImage() + { + $pdf = $this->render('
Text
'); + + $stream = $this->pageContents($pdf)[0]; + $this->assertStringContainsString('0.000 1.000 0.000 rg', $stream); + $this->assertLessThan(strpos($stream, '/Pattern cs'), strpos($stream, '0.000 1.000 0.000 rg')); + } + + /** + * A layer takes its own position and size from the lists, and a repeat list shorter than the list of images + * repeats: the third of three layers takes the first repeat + */ + public function testEachLayerTakesItsOwnValues() + { + $layers = $this->paintedLayers('
Text
'); + + $this->assertSame([ + // The bottom layer, 20mm (56.693pt) wide and in proportion, at the top left + ['kind' => 'pattern', 'step' => '99999 99999', 'box' => '56.693 65.764', 'at' => '42.520 730.772'], + // Repeated across, at the bottom right of the 170.079pt by 85.039pt box + ['kind' => 'pattern', 'step' => '75.000 99999', 'box' => self::IMAGE_SIZE, 'at' => '137.598 711.496'], + // The top layer, once at the top left + ['kind' => 'pattern', 'step' => '99999 99999', 'box' => self::IMAGE_SIZE, 'at' => '42.520 709.536'], + ], $layers); + } + + /** + * A longhand list longer than the list of images is cut short + */ + public function testCutsAListLongerThanTheImages() + { + $layers = $this->paintedLayers('
Text
'); + + $this->assertSame([['kind' => 'pattern', 'step' => '99999 99999', 'box' => self::IMAGE_SIZE, 'at' => '137.598 711.496']], $layers); + } + + /** + * A table cell draws every layer, the last one at the bottom + */ + public function testPaintsEveryLayerOfATableCell() + { + $layers = $this->paintedLayers('
Text
'); + + $this->assertSame(['shading', 'pattern'], array_column($layers, 'kind')); + } + + /** + * A table draws every layer, the last one at the bottom + */ + public function testPaintsEveryLayerOfATable() + { + $layers = $this->paintedLayers('
Text
'); + + $this->assertSame(['pattern', 'shading'], array_column($layers, 'kind')); + } + + /** + * A table row draws every layer, the last one at the bottom + */ + public function testPaintsEveryLayerOfATableRow() + { + $layers = $this->paintedLayers('
Text
'); + + $this->assertSame(['pattern', 'shading'], array_column($layers, 'kind')); + } + + /** + * The page box of an @page rule draws every layer, the last one at the bottom + */ + public function testPaintsEveryLayerOfThePage() + { + $layers = $this->paintedLayers('Text'); + + $this->assertSame(['shading', 'pattern'], array_column($layers, 'kind')); + } + + /** + * The colour, and the layers with it, are clipped as the bottom layer's background-clip has it + */ + public function testClipsToTheBoxOfTheBottomLayer() + { + $pdf = $this->render('
Text
'); + + // The content box starts 5mm (14.173pt) in from the block's left edge at 42.520pt + $this->assertStringContainsString(' q 0 w 56.693 ', $this->pageContents($pdf)[0]); + } + + /** + * The legacy CSS mode draws only the top layer, as before + */ + public function testTheLegacyModeDrawsOnlyTheTopLayer() + { + $layers = $this->paintedLayers('
Text
', ['cssMode' => CssMode::LEGACY]); + + $this->assertSame(['pattern'], array_column($layers, 'kind')); + } + + /** + * Puts the test image's path in place of IMG + * + * @param string $css + * @return string + */ + private function withImage($css) + { + return str_replace('IMG', $this->backgroundImage(), $css); + } + + /** + * The gradients and images the first page paints, from the bottom up. A gradient gives its shading type; an + * image pattern its steps, its box and where it is placed, in points + * + * @param string $html + * @param array $config + * @return array[] + */ + private function paintedLayers($html, $config = []) + { + $pdf = $this->assertDrawsSilently(function (Mpdf $mpdf) use ($html) { + $mpdf->WriteHTML($html); + }, $config); + + preg_match_all('#/(Sh\d+) sh|/Pattern cs /(P\d+) scn#', $this->pageContents($pdf)[0], $fills, PREG_SET_ORDER); + + $layers = []; + foreach ($fills as $fill) { + $name = isset($fill[2]) ? $fill[2] : $fill[1]; + preg_match('#/' . $name . ' (\d+) 0 R#', $pdf, $ref); + $object = $this->object($pdf, $ref[1]); + + if (isset($fill[2])) { + preg_match('#/BBox \[0 0 ([\d.]+ [\d.]+)\]\n/XStep ([\d.]+)\n/YStep ([\d.]+)\n/Matrix \[1 0 0 1 ([\d.]+ [\d.]+)\]#', $object, $pattern); + $layers[] = ['kind' => 'pattern', 'step' => $pattern[2] . ' ' . $pattern[3], 'box' => $pattern[1], 'at' => $pattern[4]]; + } else { + preg_match('#/ShadingType (\d)#', $object, $shading); + $layers[] = ['kind' => 'shading', 'type' => $shading[1]]; + } + } + + return $layers; + } + +} diff --git a/tests/Snapshots/BackgroundShorthandSnapshotTest.php b/tests/Snapshots/BackgroundShorthandSnapshotTest.php index 448ed703c..a5d3a7962 100644 --- a/tests/Snapshots/BackgroundShorthandSnapshotTest.php +++ b/tests/Snapshots/BackgroundShorthandSnapshotTest.php @@ -55,7 +55,7 @@ public function generatePdf()

Two layers

-

background: url(bg.jpg) no-repeat, #ccccff. mPDF draws only the first layer, over the colour of the last: the image at the top left, once, on a light blue box.

+

background: url(bg.jpg) no-repeat, #ccccff. The image at the top left, once, on a light blue box: the colour is the last layer's.

A background that is not a background is dropped

diff --git a/tests/Snapshots/MultipleBackgroundsSnapshotTest.php b/tests/Snapshots/MultipleBackgroundsSnapshotTest.php new file mode 100644 index 000000000..7894f8458 --- /dev/null +++ b/tests/Snapshots/MultipleBackgroundsSnapshotTest.php @@ -0,0 +1,77 @@ + + + +

Multiple backgrounds

+ +

background: url(bg.jpg) no-repeat, linear-gradient(red, blue). The image once at the top left, over a red to blue gradient.

+
+ +

background: linear-gradient(red, blue) 0 0 / 50% 100%, url(bg.jpg). A red to blue gradient over the left half, covering the tiled image, which shows on the right half.

+
+ +

background-image: url(bg.jpg), url(bg.jpg); background-repeat: no-repeat; background-position: left top, right bottom. The image twice, once in the top left corner and once in the bottom right.

+
+ +

background: url(bg.jpg) no-repeat, #ccccff. The image once at the top left, on a light blue box: the colour is the last layer's.

+
+ +

background-image: linear-gradient(to right, red, blue), radial-gradient(yellow, green); background-size: 50% 100%, auto. A red to blue gradient over the left half; the right half shows the rest of a yellow to green radial gradient, yellow at the middle of the box and green at its right edge.

+
+ +

Three images, background-repeat: no-repeat, repeat-x; background-position: right top, left bottom, left top; background-size: 10mm. A small image once at the top right, a row of them along the bottom, and one at the top left, which takes the first repeat again.

+
+ +

A table cell with background: url(bg.jpg) no-repeat, linear-gradient(red, blue). The image once at the top left, over a red to blue gradient.

+
+ +
+

This page's @page rule has background: url(bg.jpg) no-repeat center, linear-gradient(#ffffcc, #99ccff). The image once in the middle of the page, over a pale yellow to pale blue gradient.

+
+ mpdf = $this->createMpdf(); + $this->mpdf->SetBasePath(__DIR__ . '/../data'); + + $this->mpdf->WriteHTML($html); + } + +} diff --git a/tests/data/snapshots/background-shorthand.pdf b/tests/data/snapshots/background-shorthand.pdf index b10728698f32500f329de4fd9717eec625b4a0ce..835a8e891edf1242bcfd6bf7a4c9fc9379435876 100644 GIT binary patch delta 1902 zcmb_dO=}ZT6rCnDX{XpW75cT%iBeLb4DaK<`KUEjL>D61Wf3eSu~8van?_MwbSL6M zg|`(^aII*%@lW&zh)`VWQrzo(lQ+qmnTi_&B{S#Ud(S=h+?T%kk$d+&_wr}XhmXJV zxhvqggoCr?0CNQ+4)ujgy})TXjB?lK>=1R}?fmJ>oVuJEwLRhj zn58oS)hYeT8j#n9D#iGDJHIH84z0__h4Jp2;mUv3aIY{Ymx>cwNB4a3^R#?0x-RV# zldH>?V|A=s)+T;8th-hV|L$9De7kM!7-uc3hVLCj)YBu3Z(9{>M;?rxtH#6Ia%|u( z34DivaKZC&W|0c+deoC|Pwo%%&=vAG4s&UI zUcNu|4&n1iHt5eUMIRc-E}ZGfW4l<=ZGtRgHkyFoPwA z1~jo>eV)j!x!!9mM!BU6qZ-;Q+!$Zw7g4+ zNF7m(PKQ%;#?QnBA`?d$-ijm@#n!?M=CP>WuF(@^1UEdH%s3-y62>_$bviDjER6vt zK^F5(A;?b%{)B`X5<&cQLGJO4V&E)DHv^m}NE8QfkEQDY_jmFCfI2pu;h~qF0ss*i z5>%HSH2@E@xgJkX0l?=Z;+-&v1q}nazIR+c+k*{Hh-QP{s0B498qHADDJ6BmYe9XJ k*BfE8>a6@TXgCUWF2V)}1vVfWZ%EMPo+^aW@ zeKp2|Cof3w;-By@comO^tI_It?DqEhxVQhHn+) zSMl!wdYD`*_?_i3u_nuOV$(lPT+n?(tcQaLN<1Xjmk62AvnPl3*CR7C869iH$9t+m z!UY6MGMuz;SY##XnhdcSYs1}4zwkSY=9vkJeEF#uVvGJcFsh%8^_Np+G+;1qz*uC% zI3`O!dbnE+8g;1z!j_kcG2R^RJNMUK((WvbX3-)9Xwug)WM|=lq9PNq&J~CCLSO%R z%i|9dGDEYF>~e8G5Q^z91D!{+eZi?17l5cf*Ie;-fti{q(~~2g^IQcHRXR6#N*^CR zDA%_mW6kKBAukqdjwv2z5*HUxJKa^pM_@K-CI;B6;)uq9o=I1%L7Jq2z@@Z6nk@z+ z35prROU6n@6WIh3OFT(b#dK0mESf}oR?R744SKaU5KNeGIUhIYK^^VU!&qHYTMjv}WxA$RZKR3WciBR()>%@-_ zAZes`cBgBcOXrGFu3UFUL4+-SQP|?gM95q}6i?J*3n9)|vua|Tap5>}A-D^IuLb26 z&4D7}%{_7eu3Bx8-3a1S%0ZzvoM6c!H=!s&OUEF9y2YTa3B+W^`GK1cgsKT}fB~p- zrvSjh77*;t9W{Vtt+AnWrvNxW8sm=Gk`VYGw0R03LFmHAnFUo9u!vhNDvHTUb&*wZ q`>3o^5h}SH$uc?nPnTPMx$JLkS2njFZ!WL+ASEb&Z0z#2E5879JD!XH diff --git a/tests/data/snapshots/multiple-backgrounds.pdf b/tests/data/snapshots/multiple-backgrounds.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0196cd6f4101076ae21bead0430364825e0f692e GIT binary patch literal 53871 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 SMBW3qR$Saz3=9$qlK&4P-U%iE literal 0 HcmV?d00001 From 2434153848632e78d1f353e8ad584b14c7cc0581 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 12 Oct 2026 07:56:24 +1100 Subject: [PATCH 2/2] Read the initial values and sizes of background layers through the existing helpers (#609) Co-Authored-By: Claude Opus 5.5 --- src/Css/NormalizeProperties.php | 57 +++++++++++++++----------- src/Mpdf.php | 1 + tests/Mpdf/MultipleBackgroundsTest.php | 2 +- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/src/Css/NormalizeProperties.php b/src/Css/NormalizeProperties.php index 65b4b9466..20d9153c4 100644 --- a/src/Css/NormalizeProperties.php +++ b/src/Css/NormalizeProperties.php @@ -63,15 +63,15 @@ class NormalizeProperties /** * The background longhands that list a value for each layer, with the key of their part in a parsed layer of the - * shorthand and their initial value + * shorthand */ const LAYERED_LONGHANDS = [ - 'BACKGROUND-IMAGE' => ['i', ''], - 'BACKGROUND-REPEAT' => ['r', 'repeat'], - 'BACKGROUND-POSITION' => ['p', '0% 0%'], - 'BACKGROUND-SIZE' => ['s', 'auto'], - 'BACKGROUND-ORIGIN' => ['o', 'padding-box'], - 'BACKGROUND-CLIP' => ['k', 'border-box'], + 'BACKGROUND-IMAGE' => 'i', + 'BACKGROUND-REPEAT' => 'r', + 'BACKGROUND-POSITION' => 'p', + 'BACKGROUND-SIZE' => 's', + 'BACKGROUND-ORIGIN' => 'o', + 'BACKGROUND-CLIP' => 'k', ]; /** @@ -247,7 +247,7 @@ public function normalize($prop) $this->processPageSizeProperty($k, $v); } elseif ($k === 'BREAK-BEFORE' || $k === 'BREAK-AFTER' || $k === 'BREAK-INSIDE') { $this->processBreakProperty($k, $v); - } elseif (in_array($k, ['BACKGROUND', 'BACKGROUND-IMAGE', 'BACKGROUND-REPEAT', 'BACKGROUND-POSITION', 'BACKGROUND-SIZE', 'BACKGROUND-ORIGIN', 'BACKGROUND-CLIP'], true)) { + } elseif ($k === 'BACKGROUND' || array_key_exists($k, self::LAYERED_LONGHANDS)) { $this->processBackgroundProperty($k, $v); } elseif ($k === 'IMAGE-ORIENTATION') { $this->processImageOrientationProperty($v); @@ -667,14 +667,11 @@ private function parseBackgroundLayer(array $components) if ($i + 1 < $count && $components[$i + 1] === '/') { $i += 2; $size = isset($components[$i]) ? [strtolower($components[$i])] : []; - if ($size !== ['cover'] && $size !== ['contain']) { - $sizeLength = '/^(auto|' . self::NUMBER . '([a-z]+|%)?)$/'; - if (!$size || !preg_match($sizeLength, $size[0])) { - return false; - } - if ($i + 1 < $count && preg_match($sizeLength, strtolower($components[$i + 1]))) { - $size[] = strtolower($components[++$i]); - } + if (!$this->isBackgroundSize($size)) { + return false; + } + if ($size !== ['cover'] && $size !== ['contain'] && $i + 1 < $count && $this->isBackgroundSize([$size[0], strtolower($components[$i + 1])])) { + $size[] = strtolower($components[++$i]); } $layer['s'] = implode(' ', $size); } @@ -729,6 +726,19 @@ private function isBackgroundPosition($component) return preg_match('/^(left|right|top|bottom|center|[+-]?' . self::NUMBER . '([a-z]+|%)?)$/', $component) === 1; } + /** + * Whether components are a background size: cover, contain, or one or two lengths, percentages or auto + * + * @param string[] $components Lowercased components + * @return bool + */ + private function isBackgroundSize(array $components) + { + $length = '(auto|' . self::NUMBER . '([a-z]+|%)?)'; + + return preg_match('/^(cover|contain|' . $length . '( ' . $length . ')?)$/', implode(' ', $components)) === 1; + } + /** * Whether a component of a shorthand is a colour * @@ -1061,7 +1071,7 @@ private function dropsNegativeLineHeight() * Sets only the parts a background shorthand names, besides its colour and image, as mPDF v7 read it: the * repeat, position and size only with an image, which they have nothing to apply to without * - * @param array $bg The shorthand as parseCssBackground() reads it + * @param array $bg The top layer of the shorthand, as parseBackgroundLayers() reads it * @return void */ private function mergeBackgroundParts(array $bg) @@ -1096,7 +1106,7 @@ private function mergeBackgroundParts(array $bg) */ protected function processBackgroundProperty($property, $value) { - if ($property !== 'BACKGROUND' && $this->mpdf->cssMode === CssMode::STANDARD) { + if ($property !== 'BACKGROUND' && $this->mpdf->cssMode === CssMode::STANDARD && strpos($value, ',') !== false) { $layers = $this->splitLayers($value); if (count($layers) > 1) { $this->processLayeredBackgroundProperty($property, $layers); @@ -1118,7 +1128,7 @@ protected function processBackgroundProperty($property, $value) $this->properties['BACKGROUND-COLOR'] = isset($bottom['c']) ? $bottom['c'] : 'transparent'; if (!$this->resetsOmittedParts()) { - // Only the top layer is drawn + // mPDF v7 drew only the top layer $this->properties['BACKGROUND-IMAGE'] = isset($layers[0]['i']) ? $layers[0]['i'] : ''; $this->mergeBackgroundParts($layers[0]); break; @@ -1128,7 +1138,7 @@ protected function processBackgroundProperty($property, $value) foreach (self::LAYERED_LONGHANDS as $longhand => $part) { $values = []; foreach ($layers as $layer) { - $values[] = isset($layer[$part[0]]) && $layer[$part[0]] !== false ? $layer[$part[0]] : $part[1]; + $values[] = isset($layer[$part]) && $layer[$part] !== false ? $layer[$part] : CssWideKeywords::initialValue($longhand); } $this->properties[$longhand] = BackgroundLayers::join($values); } @@ -1198,13 +1208,10 @@ private function processLayeredBackgroundProperty($property, array $layers) private function layerValue($property, array $components) { if ($property === 'BACKGROUND-SIZE') { - $size = implode(' ', $components); - $length = '(auto|' . self::NUMBER . '([a-z]+|%)?)'; - - return preg_match('/^(cover|contain|' . $length . '( ' . $length . ')?)$/', $size) ? $size : false; + return $this->isBackgroundSize($components) ? implode(' ', $components) : false; } - $part = self::LAYERED_LONGHANDS[$property][0]; + $part = self::LAYERED_LONGHANDS[$property]; $layer = $this->parseBackgroundLayer($components); if (!isset($layer[$part]) || $layer[$part] === false) { return false; diff --git a/src/Mpdf.php b/src/Mpdf.php index c3c013494..52fb36958 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -2842,6 +2842,7 @@ function SetBackground(&$properties, &$maxwidth) $layer = $properties; $layer['BACKGROUND-IMAGE'] = $image; + // The clip is the bottom layer's for every layer, which setCSS() reads foreach (['BACKGROUND-REPEAT', 'BACKGROUND-POSITION', 'BACKGROUND-SIZE', 'BACKGROUND-ORIGIN'] as $property) { if (isset($properties[$property])) { $layer[$property] = BackgroundLayers::valueFor($properties[$property], $n); diff --git a/tests/Mpdf/MultipleBackgroundsTest.php b/tests/Mpdf/MultipleBackgroundsTest.php index 4da0a8181..bf4a4ba53 100644 --- a/tests/Mpdf/MultipleBackgroundsTest.php +++ b/tests/Mpdf/MultipleBackgroundsTest.php @@ -154,7 +154,7 @@ public function testClipsToTheBoxOfTheBottomLayer() } /** - * The legacy CSS mode draws only the top layer, as before + * The legacy CSS mode draws only the top layer, as mPDF v7 did */ public function testTheLegacyModeDrawsOnlyTheTopLayer() {