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', + 'BACKGROUND-POSITION' => 'p', + 'BACKGROUND-SIZE' => 's', + 'BACKGROUND-ORIGIN' => 'o', + 'BACKGROUND-CLIP' => 'k', + ]; + /** * @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 ($k === 'BACKGROUND' || array_key_exists($k, self::LAYERED_LONGHANDS)) { $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) { @@ -645,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); } @@ -707,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 * @@ -1039,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) @@ -1065,7 +1097,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 +1106,42 @@ private function mergeBackgroundParts(array $bg) */ protected function processBackgroundProperty($property, $value) { + if ($property !== 'BACKGROUND' && $this->mpdf->cssMode === CssMode::STANDARD && strpos($value, ',') !== false) { + $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); + // mPDF v7 drew only the top layer + $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]) && $layer[$part] !== false ? $layer[$part] : CssWideKeywords::initialValue($longhand); + } + $this->properties[$longhand] = BackgroundLayers::join($values); + } break; case 'BACKGROUND-IMAGE': @@ -1124,9 +1170,59 @@ 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') { + return $this->isBackgroundSize($components) ? implode(' ', $components) : false; + } + + $part = self::LAYERED_LONGHANDS[$property]; + $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..52fb36958 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,64 @@ 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; + // 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); + } + } + + $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 +3009,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 +13355,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 +13392,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 +13540,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 +13588,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 +18822,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 +19463,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 +23534,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 +23886,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 +23923,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 +24236,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 +24283,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 +24455,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..bf4a4ba53 --- /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 mPDF v7 did + */ + 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 b10728698..835a8e891 100644 Binary files a/tests/data/snapshots/background-shorthand.pdf and b/tests/data/snapshots/background-shorthand.pdf differ diff --git a/tests/data/snapshots/multiple-backgrounds.pdf b/tests/data/snapshots/multiple-backgrounds.pdf new file mode 100644 index 000000000..0196cd6f4 Binary files /dev/null and b/tests/data/snapshots/multiple-backgrounds.pdf differ