From 979fa817e7087162cd0dccffc8a8050d259f9fdb Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Wed, 30 Sep 2026 07:18:16 +1000 Subject: [PATCH 1/2] Read the border and background shorthands with their parts in any order border and its sides take the width, style and colour in any order, so solid #c00 2mm, 2mm #c00 solid and red 2mm solid draw a border. background takes a colour after url(), a size after the position, and its repeat, attachment and origin and clip boxes in any order. Only the last layer may carry a colour; mPDF still draws only the first layer, over that colour. A shorthand with a part that is none of its parts is dropped, as a browser drops it, so the value it would replace still applies. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG-GRAVITYPDF.md | 6 + src/Css/NormalizeProperties.php | 356 +++++++++++------- tests/Mpdf/BorderBackgroundShorthandTest.php | 102 +++++ tests/Mpdf/Css/NormalizePropertiesTest.php | 158 +++++++- .../BackgroundShorthandSnapshotTest.php | 73 ++++ .../Snapshots/BorderShorthandSnapshotTest.php | 74 ++++ tests/data/snapshots/background-shorthand.pdf | Bin 0 -> 46364 bytes tests/data/snapshots/border-shorthand.pdf | Bin 0 -> 38633 bytes 8 files changed, 632 insertions(+), 137 deletions(-) create mode 100644 tests/Mpdf/BorderBackgroundShorthandTest.php create mode 100644 tests/Snapshots/BackgroundShorthandSnapshotTest.php create mode 100644 tests/Snapshots/BorderShorthandSnapshotTest.php create mode 100644 tests/data/snapshots/background-shorthand.pdf create mode 100644 tests/data/snapshots/border-shorthand.pdf diff --git a/CHANGELOG-GRAVITYPDF.md b/CHANGELOG-GRAVITYPDF.md index a92d16088..bfe0def3a 100644 --- a/CHANGELOG-GRAVITYPDF.md +++ b/CHANGELOG-GRAVITYPDF.md @@ -154,6 +154,11 @@ Bugfixes * A channel outside its range wrapped instead of being clamped: `rgb(255.5, 0, 0)` drew black and `rgb(300, 0, 0)` dark red. Channels, alphas, and `hsl()` saturation and lightness are now clamped, as CSS does. * `rebeccapurple` was missing. mPDF's `violetred`, which is not a CSS colour, is kept. * A `list-style-image` whose URL had capitals, such as `url(img/List-Bullet.png)`, drew no marker on a case-sensitive disk or server, because the URL was lowercased before the image was fetched. The URL now keeps its case. [#552] [#623] +* The `border` shorthand drew nothing when its parts came in some orders, such as `border: solid #c00 2mm`, `2mm #c00 solid` or `red 2mm solid`. The width, style and colour of `border` and of each side are now read in any order. [#552] [#583] + * A lone colour, as in `border-top: #f00`, was read as the width. +* The `background` shorthand lost a colour written after `url()` and misplaced the image, and lost a size after the position (`center / cover`). Its colour, image, position and size, repeat, attachment, and origin and clip boxes are now read in any order. [#552] [#583] + * With several layers, mPDF still draws only the first, now over the colour given in the last. +* A `border` or `background` shorthand with a part that is none of its parts, such as `border: 1px solid bogus`, is dropped, as a browser drops it, so the value it would have replaced still applies. `border: 1px solid bogus` used to draw a black border. [#552] [#583] * A percentage `width`, `min-width` or `max-width` on an image in a table cell was resolved against the block around the table, not the cell. In a 60mm cell, `max-width: 20%` came out as 36mm. [#223] [#505] * An `@page :left` or `@page :right` rule was ignored unless the style sheet also had a plain `@page` rule. With one, the margins of a `:right` rule were applied to left pages too. [#555] [#556] * When a later `WriteHTML()` call turned the document right to left, the text on the page already started was set between the swapped side margins. That page now keeps its margins, and the swap starts with the next page. [#554] [#558] @@ -778,6 +783,7 @@ These changes do not change output. [#570]: https://github.com/GravityPDF/mpdf/pull/570 [#578]: https://github.com/GravityPDF/mpdf/pull/578 [#580]: https://github.com/GravityPDF/mpdf/pull/580 +[#583]: https://github.com/GravityPDF/mpdf/pull/583 [#615]: https://github.com/GravityPDF/mpdf/pull/615 [#622]: https://github.com/GravityPDF/mpdf/pull/622 [#623]: https://github.com/GravityPDF/mpdf/pull/623 diff --git a/src/Css/NormalizeProperties.php b/src/Css/NormalizeProperties.php index 6e3a7f94e..2755a6f5d 100644 --- a/src/Css/NormalizeProperties.php +++ b/src/Css/NormalizeProperties.php @@ -44,6 +44,21 @@ class NormalizeProperties */ private $colorConverter; + /** + * The keywords every property takes, which a shorthand takes only as its whole value + */ + const CSS_WIDE_KEYWORDS = ['inherit', 'initial', 'unset', 'revert']; + + /** + * An unsigned number, as a regex fragment + */ + const NUMBER = '(\d+\.?\d*|\.\d+)'; + + /** + * The background-repeat keywords that may be given in pairs, one for each axis + */ + const REPEAT_KEYWORDS = ['repeat', 'no-repeat', 'space', 'round']; + /** * @var array */ @@ -388,31 +403,16 @@ protected function processFontFamilyProperty($propertyKey, $value) */ protected function processBorderProperty($propertyKey, $value) { - switch ($propertyKey) { - case 'BORDER': - $value = $value !== '1' ? $this->normalizeBorderString($value) : '1px solid #000000'; - - $this->properties['BORDER-TOP'] = $value; - $this->properties['BORDER-RIGHT'] = $value; - $this->properties['BORDER-BOTTOM'] = $value; - $this->properties['BORDER-LEFT'] = $value; - break; + $value = $propertyKey === 'BORDER' && $value === '1' ? '1px solid #000000' : $this->normalizeBorderString($value); - case 'BORDER-TOP': - $this->properties['BORDER-TOP'] = $this->normalizeBorderString($value); - break; - - case 'BORDER-RIGHT': - $this->properties['BORDER-RIGHT'] = $this->normalizeBorderString($value); - break; - - case 'BORDER-BOTTOM': - $this->properties['BORDER-BOTTOM'] = $this->normalizeBorderString($value); - break; + // A value that is not a border is dropped, as a browser drops it + if ($value === false) { + return; + } - case 'BORDER-LEFT': - $this->properties['BORDER-LEFT'] = $this->normalizeBorderString($value); - break; + $sides = $propertyKey === 'BORDER' ? ['BORDER-TOP', 'BORDER-RIGHT', 'BORDER-BOTTOM', 'BORDER-LEFT'] : [$propertyKey]; + foreach ($sides as $side) { + $this->properties[$side] = $value; } } @@ -473,58 +473,168 @@ protected function processBorderShorthandProperty($key, $value) /** * Parse CSS background shorthand property. * - * Extracts background color, image, repeat, and position from the - * background shorthand property. Supports gradients and url() images. + * 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. * * @param string $s Background property value - * @return array Array with keys 'c' (color), 'i' (image), 'r' (repeat), 'p' (position) + * @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 */ protected function parseCssBackground($s) { - $background = [ - 'c' => false, // color - 'i' => false, // image - 'r' => false, // repeat - 'p' => false, // position - ]; - - if (preg_match('/(-moz-|-webkit-|-o-)*(repeating-)*(linear|radial)-gradient\(.*\)/i', $s, $m)) { - $background['i'] = $m[0]; - return $background; + $layers = [[]]; + foreach ($this->splitComponents($s) as $component) { + if ($component === ',') { + $layers[] = []; + } else { + $layers[count($layers) - 1][] = $component; + } + } + + // A CSS-wide keyword gives the initial value, no background, until the keywords are resolved + if (count($layers) === 1 && count($layers[0]) === 1 && in_array(strtolower($layers[0][0]), self::CSS_WIDE_KEYWORDS, true)) { + return []; } - if (preg_match('/url\(/i', $s)) { - // If color, set and strip it off - if (preg_match('/^\s*(#[0-9a-fA-F]{3,8}|(rgba|rgb|device-cmyka|cmyka|device-cmyk|cmyk|hsla|hsl|spot)\(.*?\)|[a-zA-Z]{3,})\s+(url\(.*)/i', $s, $m)) { - $background['c'] = strtolower($m[1]); - $s = $m[3]; + $last = count($layers) - 1; + foreach ($layers as $n => $components) { + $layers[$n] = $this->parseBackgroundLayer($components); + if ($layers[$n] === false || ($n !== $last && isset($layers[$n]['c']))) { + return false; } + } - if (preg_match('/url\([\'\"]{0,1}(.*?)[\'\"]{0,1}\)\s*(.*)/i', $s, $m)) { - $background['i'] = $m[1]; - $s = strtolower($m[2]); - if (preg_match('/(repeat-x|repeat-y|no-repeat|repeat)/', $s, $m)) { - $background['r'] = $m[1]; - } + if (isset($layers[$last]['c'])) { + $layers[0]['c'] = $layers[$last]['c']; + } - // Remove repeat, attachment (discarded) and also any inherit - $s = preg_replace('/(repeat-x|repeat-y|no-repeat|repeat|scroll|fixed|inherit)/', '', $s); - $bits = preg_split('/\s+/', trim($s)); + return $layers[0]; + } - $normalizedPosition = $this->normalizeBackgroundPosition($bits); - if ($normalizedPosition !== false) { - $background['p'] = $normalizedPosition; + /** + * Parse one layer of the background shorthand, whose parts may come in any order and each at most once. + * 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 + */ + private function parseBackgroundLayer(array $components) + { + $layer = []; + $count = count($components); + + for ($i = 0; $i < $count; $i++) { + $keyword = strtolower($components[$i]); + + if (preg_match('/^url\(\s*([\'"]?)(.*)\1\s*\)$/is', $components[$i], $m)) { + $kind = 'i'; + $value = $m[2]; + } elseif (preg_match('/^(?:-(?!moz-)[a-z]+-)?((-moz-)?(repeating-)?(linear|radial)-gradient\(.*\))$/is', $components[$i], $m)) { + // Drops a vendor prefix other than -moz-, which the gradient parser reads + $kind = 'i'; + $value = $m[1]; + } elseif ($keyword === 'none') { + $kind = 'i'; + $value = ''; + } elseif ($this->isBackgroundPosition($keyword)) { + $kind = 'p'; + $bits = [$keyword]; + while (count($bits) < 4 && $i + 1 < $count && $this->isBackgroundPosition(strtolower($components[$i + 1]))) { + $bits[] = strtolower($components[++$i]); + } + // normalizeBackgroundPosition() reads one or two values; for the three- and four-value forms the position is left out + $value = $this->normalizeBackgroundPosition($bits); + + 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]); + } + } + $layer['s'] = implode(' ', $size); + } + } elseif ($keyword === 'repeat-x' || $keyword === 'repeat-y') { + $kind = 'r'; + $value = $keyword; + } elseif (in_array($keyword, self::REPEAT_KEYWORDS, true)) { + $kind = 'r'; + $value = $keyword; + if ($i + 1 < $count && in_array(strtolower($components[$i + 1]), self::REPEAT_KEYWORDS, true)) { + $i++; } + } elseif (in_array($keyword, ['scroll', 'fixed', 'local'], true)) { + $kind = 'a'; + $value = $keyword; + } elseif (in_array($keyword, ['border-box', 'padding-box', 'content-box'], true)) { + $kind = isset($layer['o']) ? 'k' : 'o'; + $value = $keyword; + } elseif ($this->isColorComponent($keyword)) { + $kind = 'c'; + $value = $keyword; + } else { + return false; + } + + if (isset($layer[$kind])) { + return false; } - return $background; + $layer[$kind] = $value; + } + + if (count($layer) === 0) { + return false; } - if (preg_match('/^\s*(#[0-9a-fA-F]{3,8}|(rgba|rgb|device-cmyka|cmyka|device-cmyk|cmyk|hsla|hsl|spot)\(.*?\)|[a-zA-Z]{3,})/i', $s, $m)) { - $background['c'] = strtolower($m[1]); + if (isset($layer['o']) && !isset($layer['k'])) { + $layer['k'] = $layer['o']; } - return $background; + return $layer; + } + + /** + * Whether a background component is part of a position: a keyword, a length or a percentage + * + * @param string $component Lowercased component + * @return bool + */ + private function isBackgroundPosition($component) + { + return preg_match('/^(left|right|top|bottom|center|[+-]?' . self::NUMBER . '([a-z]+|%)?)$/', $component) === 1; + } + + /** + * Whether a component of a shorthand is a colour + * + * @param string $component Lowercased component + * @return bool + */ + private function isColorComponent($component) + { + return $component === 'transparent' || $component === 'currentcolor' || $this->colorConverter->isColor($component); + } + + /** + * Splits a shorthand value into its components at whitespace, keeping each function call and quoted + * string whole. A comma or slash outside them is a component of its own. + * + * @param string $value + * @return string[] + */ + private function splitComponents($value) + { + // Possessive, so an unclosed parenthesis fails at once rather than backtracking through every split + preg_match_all('/[,\/]|(?:[^\s,\/()"\']++|"[^"]*+"|\'[^\']*+\'|(\((?:[^()"\']++|"[^"]*+"|\'[^\']*+\'|(?1))*+\)))++/', $value, $m); + + return $m[0]; } /** @@ -748,96 +858,62 @@ protected function normalizeBackgroundPosition($bits) /** * Parse and normalize border shorthand property. * - * Converts border shorthand syntax into standardized "width style color" format. - * Handles various input formats and orders. + * Converts border shorthand syntax, with its width, style and colour in any order, into the + * "width style color" format the border longhands are merged into. * * @param string $bd Border property value - * @return string Normalized border string in format "width style color" + * @return string|false Normalized border string, or false when the value is not a border */ protected function normalizeBorderString($bd) { - preg_match_all("/\((.*?)\)/", $bd, $m); - foreach ($m[1] as $i => $value) { - $sub = str_replace(' ', '', $m[1][$i]); - $bd = str_replace($m[1][$i], $sub, $bd); - } - - $prop = preg_split('/\s+/', trim($bd)); - if (count($prop) > 3) { - return ''; + $parts = $this->parseBorderParts($this->splitComponents($bd)); + if ($parts === false) { + return false; } - $parts = $this->parseBorderParts($prop); - $w = $parts['w']; - $s = $parts['s']; - $c = $parts['c']; - - $s = strtolower($s); - - return $w . ' ' . $s . ' ' . $c; + return $parts['w'] . ' ' . $parts['s'] . ' ' . $parts['c']; } /** * Parse border property parts (width, style, color). * - * Helper method for normalizeBorderString to determine width, style, and color - * from split border property string. + * Each part may come in any position and at most once. A part left out takes its initial value. A CSS-wide + * keyword gives the initial value of all three until the keywords are resolved. * - * @param array $prop Split border property string - * @return array Array containing 'w' (width), 's' (style), 'c' (color) + * @param string[] $prop Components of the border property value + * @return array|false Array containing 'w' (width), 's' (style), 'c' (color), or false when a component is none of them */ protected function parseBorderParts($prop) { - $width = 'medium'; - $color = '#000000'; - $style = 'none'; + $parts = []; - switch (count($prop)) { - case 1: - if (in_array($prop[0], $this->mpdf->borderstyles, true) || $prop[0] === 'none' || $prop[0] === 'hidden') { - $style = $prop[0]; - } elseif (is_array($this->colorConverter->convert($prop[0], $this->mpdf->PDFAXwarnings))) { - $color = $prop[0]; - } else { - $width = $prop[0]; - } - break; + if (count($prop) === 1 && in_array($prop[0], self::CSS_WIDE_KEYWORDS, true)) { + $prop = []; + } elseif (count($prop) === 0) { + return false; + } - case 2: - if (in_array($prop[1], $this->mpdf->borderstyles, true) || $prop[1] === 'none' || $prop[1] === 'hidden') { - $width = $prop[0]; - $style = $prop[1]; - } elseif (in_array($prop[0], $this->mpdf->borderstyles, true) || $prop[0] === 'none' || $prop[0] === 'hidden') { - $style = $prop[0]; - $color = $prop[1]; - } else { - $width = $prop[0]; - $color = $prop[1]; - } - break; + foreach ($prop as $part) { + if (in_array($part, $this->mpdf->borderstyles, true) || $part === 'none' || $part === 'hidden') { + $kind = 's'; + } elseif (preg_match('/^(thin|medium|thick|' . self::NUMBER . '[a-z]*)$/', $part)) { + $kind = 'w'; + } elseif ($this->isColorComponent($part)) { + $kind = 'c'; + // Keeps a spot colour, whose name can have spaces, as one word of the border string + $part = str_replace(' ', '', $part); + } else { + return false; + } - case 3: - if (0 === strpos($prop[0], '#')) { - $color = $prop[0]; - $width = $prop[1]; - $style = $prop[2]; - } elseif (substr($prop[0], 1, 1) === '#') { - $style = $prop[0]; - $color = $prop[1]; - $width = $prop[2]; - } elseif (in_array($prop[0], $this->mpdf->borderstyles) || $prop[0] === 'none' || $prop[0] === 'hidden') { - $style = $prop[0]; - $width = $prop[1]; - $color = $prop[2]; - } else { - $width = $prop[0]; - $style = $prop[1]; - $color = $prop[2]; - } - break; + if (isset($parts[$kind])) { + return false; + } + + $parts[$kind] = $part; } - return ['w' => $width, 's' => $style, 'c' => $color]; + return $parts + ['w' => 'medium', 's' => 'none', 'c' => '#000000']; } /** @@ -854,22 +930,30 @@ protected function processBackgroundProperty($property, $value) switch ($property) { case 'BACKGROUND': $bg = $this->parseCssBackground($value); - if ($bg['c']) { - $this->properties['BACKGROUND-COLOR'] = $bg['c']; - } else { - $this->properties['BACKGROUND-COLOR'] = 'transparent'; + + // A value that is not a background is dropped, as a browser drops it + if ($bg === false) { + break; } - if ($bg['i']) { - $this->properties['BACKGROUND-IMAGE'] = $bg['i']; - if ($bg['r']) { + $this->properties['BACKGROUND-COLOR'] = isset($bg['c']) ? $bg['c'] : 'transparent'; + $this->properties['BACKGROUND-IMAGE'] = isset($bg['i']) ? $bg['i'] : ''; + + if ($this->properties['BACKGROUND-IMAGE'] !== '') { + if (isset($bg['r'])) { $this->properties['BACKGROUND-REPEAT'] = $bg['r']; } - if ($bg['p']) { + if (!empty($bg['p'])) { $this->properties['BACKGROUND-POSITION'] = $bg['p']; } - } else { - $this->properties['BACKGROUND-IMAGE'] = ''; + if (isset($bg['s'])) { + $this->properties['BACKGROUND-SIZE'] = $bg['s']; + } + } + + if (isset($bg['o'])) { + $this->properties['BACKGROUND-ORIGIN'] = $bg['o']; + $this->properties['BACKGROUND-CLIP'] = $bg['k']; } break; diff --git a/tests/Mpdf/BorderBackgroundShorthandTest.php b/tests/Mpdf/BorderBackgroundShorthandTest.php new file mode 100644 index 000000000..4f440fe29 --- /dev/null +++ b/tests/Mpdf/BorderBackgroundShorthandTest.php @@ -0,0 +1,102 @@ +assertDrawsSilently(function (Mpdf $mpdf) use ($css) { + $mpdf->WriteHTML('
Text
'); + }); + + $this->assertSame($sides, substr_count($pdf, self::RED_2MM_SIDE)); + } + + /** + * Each case: the declarations, and how many 2mm red sides they draw + * + * @return array[] + */ + public function borderOrderProvider() + { + return [ + 'width style colour' => ['border: 2mm solid #c00', 4], + 'style colour width' => ['border: solid #c00 2mm', 4], + 'width colour style' => ['border: 2mm #c00 solid', 4], + 'colour width style' => ['border: #c00 2mm solid', 4], + 'style width colour' => ['border: solid 2mm #c00', 4], + 'one side' => ['border-bottom: solid rgb(204 0 0) 2mm', 1], + ]; + } + + /** + * A border with a colour mPDF cannot read is dropped, so the earlier rule's border is drawn + */ + public function testKeepsTheEarlierBorderForAnInvalidOne() + { + $pdf = $this->assertDrawsSilently(function (Mpdf $mpdf) { + $mpdf->WriteHTML('
Text
'); + }); + + $this->assertSame(4, substr_count($pdf, self::RED_2MM_SIDE)); + } + + /** + * A colour after the image is painted under it, and the image stays at the top left + */ + public function testPaintsAColourGivenAfterTheImage() + { + $pdf = $this->assertDrawsSilently(function (Mpdf $mpdf) { + $mpdf->WriteHTML('
Text
'); + }); + + $this->assertStringContainsString('0.000 1.000 0.000 rg', $pdf); + $this->assertStringContainsString('/Matrix [1 0 0 1 42.520 ', $pdf); + $this->assertStringContainsString('/BBox [0 0 75.000 87.000]', $pdf); + } + + /** + * The size after the slash covers the box + */ + public function testSizesTheImageAfterTheSlash() + { + $pdf = $this->assertDrawsSilently(function (Mpdf $mpdf) { + $mpdf->WriteHTML('
Text
'); + }); + + // 60mm wide, and as tall as the 100 by 116 pixel image is at that width + $this->assertStringContainsString('/BBox [0 0 170.079 197.291]', $pdf); + } + + /** + * A background with a colour mPDF cannot read is dropped, so the earlier rule's colour is painted + */ + public function testKeepsTheEarlierBackgroundForAnInvalidOne() + { + $pdf = $this->assertDrawsSilently(function (Mpdf $mpdf) { + $mpdf->WriteHTML('
Text
'); + }); + + $this->assertStringContainsString('0.000 1.000 0.000 rg', $pdf); + $this->assertStringNotContainsString('/Pattern', $pdf); + } + +} diff --git a/tests/Mpdf/Css/NormalizePropertiesTest.php b/tests/Mpdf/Css/NormalizePropertiesTest.php index 7d43acc79..aba8ec0a3 100644 --- a/tests/Mpdf/Css/NormalizePropertiesTest.php +++ b/tests/Mpdf/Css/NormalizePropertiesTest.php @@ -215,7 +215,7 @@ public function providerBorderString() { return [ ['solid', 'medium solid #000000'], - ['#ff0000', '#ff0000 none #000000'], // Note: internal logic might produce this weird output, verified from CssManagerTest + ['#ff0000', 'medium none #ff0000'], ['2px', '2px none #000000'], ['2px solid', '2px solid #000000'], ['solid #ff0000', 'medium solid #ff0000'], @@ -223,6 +223,49 @@ public function providerBorderString() ['2px solid #ff0000', '2px solid #ff0000'], ['#ff0000 2px solid', '2px solid #ff0000'], ['none', 'medium none #000000'], + ['solid #c00 2mm', '2mm solid #c00'], + ['2mm #c00 solid', '2mm solid #c00'], + ['red 2mm solid', '2mm solid red'], + ['solid 2mm red', '2mm solid red'], + ['#c00 solid', 'medium solid #c00'], + ['dashed thick', 'thick dashed #000000'], + ['solid rgb(204 0 0) 0.5mm', '0.5mm solid rgb(204,0,0)'], + ['transparent 1px solid', '1px solid transparent'], + ['SOLID 2MM #C00', '2mm solid #c00'], + ['inherit', 'medium none #000000'], + ]; + } + + /** + * A border value with a part that is no width, style or colour, or with a part twice, is dropped + * + * @dataProvider providerInvalidBorder + * + * @param string $property + * @param string $value + */ + public function testDropsInvalidBorder($property, $value) + { + $this->assertSame([], $this->normalizeProperties->normalize([$property => $value])); + } + + /** + * Border values that are not borders + * + * @return string[][] + */ + public function providerInvalidBorder() + { + return [ + 'unknown colour' => ['BORDER', '1px solid bogus'], + 'unknown colour on one side' => ['BORDER-LEFT', 'bogus 1px solid'], + 'two styles' => ['BORDER', 'solid dashed'], + 'two widths' => ['BORDER-TOP', '1px 2px solid'], + 'two colours' => ['BORDER', '1px solid red blue'], + 'four parts' => ['BORDER', '1px solid red !important'], + 'a slash' => ['BORDER', '1px / solid'], + 'a negative width' => ['BORDER', '-1px solid red'], + 'a keyword with a part' => ['BORDER', 'inherit solid'], ]; } @@ -259,6 +302,119 @@ public function testParseCSSbackground() $this->assertEquals($gradient, $res['BACKGROUND-IMAGE']); } + /** + * The parts of the background shorthand, in any order + * + * @dataProvider providerBackground + * + * @param string $value + * @param array $expected The properties it expands to + */ + public function testBackgroundPartsInAnyOrder($value, array $expected) + { + $this->assertSame($expected, $this->normalizeProperties->normalize(['BACKGROUND' => $value])); + } + + /** + * Background shorthands and what they expand to + * + * @return array[] + */ + public function providerBackground() + { + return [ + 'colour after the image' => [ + 'url(bg.png) no-repeat #0f0', + ['BACKGROUND-COLOR' => '#0f0', 'BACKGROUND-IMAGE' => 'bg.png', 'BACKGROUND-REPEAT' => 'no-repeat'], + ], + 'position and size' => [ + 'url(bg.png) no-repeat center / cover', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => 'bg.png', 'BACKGROUND-REPEAT' => 'no-repeat', 'BACKGROUND-POSITION' => '50% 50%', 'BACKGROUND-SIZE' => 'cover'], + ], + 'two size values' => [ + 'left top/50% auto url(bg.png)', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => 'bg.png', 'BACKGROUND-POSITION' => '0% 0%', 'BACKGROUND-SIZE' => '50% auto'], + ], + 'quoted image and lengths' => [ + 'repeat-x URL("Sub Dir/A (1).png") #FFF 10mm 20mm', + ['BACKGROUND-COLOR' => '#fff', 'BACKGROUND-IMAGE' => 'Sub Dir/A (1).png', 'BACKGROUND-REPEAT' => 'repeat-x', 'BACKGROUND-POSITION' => '10mm 20mm'], + ], + 'attachment' => [ + 'fixed url(bg.png) rgb(0 0 255)', + ['BACKGROUND-COLOR' => 'rgb(0,0,255)', 'BACKGROUND-IMAGE' => 'bg.png'], + ], + 'origin and clip' => [ + 'content-box url(bg.png) padding-box', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => 'bg.png', 'BACKGROUND-ORIGIN' => 'content-box', 'BACKGROUND-CLIP' => 'padding-box'], + ], + 'one box for both' => [ + '#eee border-box', + ['BACKGROUND-COLOR' => '#eee', 'BACKGROUND-IMAGE' => '', 'BACKGROUND-ORIGIN' => 'border-box', 'BACKGROUND-CLIP' => 'border-box'], + ], + 'colour after a gradient' => [ + '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' => [ + 'url(a.png) no-repeat, url(b.png) repeat-y #fff', + ['BACKGROUND-COLOR' => '#fff', 'BACKGROUND-IMAGE' => 'a.png', 'BACKGROUND-REPEAT' => 'no-repeat'], + ], + 'none' => [ + 'none', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => ''], + ], + 'a keyword on its own' => [ + 'inherit', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => ''], + ], + 'a -webkit- gradient' => [ + '-webkit-linear-gradient(left, red, blue)', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => 'linear-gradient(left, red, blue)'], + ], + 'a -moz- gradient' => [ + '-moz-linear-gradient(left, red, blue)', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => '-moz-linear-gradient(left, red, blue)'], + ], + 'a four-value position mPDF cannot place' => [ + 'url(bg.png) right 5mm bottom 5mm #fff', + ['BACKGROUND-COLOR' => '#fff', 'BACKGROUND-IMAGE' => 'bg.png'], + ], + ]; + } + + /** + * A background value with a part that is none of its parts, or with a part twice, is dropped + * + * @dataProvider providerInvalidBackground + * + * @param string $value + */ + public function testDropsInvalidBackground($value) + { + $this->assertSame([], $this->normalizeProperties->normalize(['BACKGROUND' => $value])); + } + + /** + * Background values that are not backgrounds + * + * @return string[][] + */ + public function providerInvalidBackground() + { + return [ + 'unknown colour' => ['url(bg.png) bogus'], + 'two colours' => ['#fff #000'], + 'two images' => ['url(a.png) url(b.png)'], + 'a colour in a layer before the last' => ['#fff url(a.png), url(b.png)'], + 'an empty layer' => ['url(a.png),'], + 'a slash with no size' => ['url(bg.png) center /'], + 'a slash with no position' => ['url(bg.png) / cover'], + 'a position split by another part' => ['url(bg.png) left no-repeat top'], + 'an image mPDF cannot draw' => ['#fff conic-gradient(red, blue)'], + 'an unclosed parenthesis' => ['#fff url(' . str_repeat('a b ', 5000)], + ]; + } + public function testNonExistentFontFamily() { $result = $this->normalizeProperties->normalize(['FONT-FAMILY' => 'abc']); diff --git a/tests/Snapshots/BackgroundShorthandSnapshotTest.php b/tests/Snapshots/BackgroundShorthandSnapshotTest.php new file mode 100644 index 000000000..448ed703c --- /dev/null +++ b/tests/Snapshots/BackgroundShorthandSnapshotTest.php @@ -0,0 +1,73 @@ + + + +

Background shorthand

+ +

A colour after the image

+

background: url(bg.jpg) no-repeat #99ff99. The image at the top left, once, on a light green box.

+
+ +

A size after the position

+

background: url(bg.jpg) no-repeat center / cover. The image fills the whole box, cut at its top and bottom.

+
+

background: #ffcc99 right bottom / 25% url(bg.jpg) no-repeat. A small image, a quarter of the box wide, in the bottom right corner of an orange box.

+
+ +

Origin and clip

+

background: content-box #ffcc99 url(bg.jpg) no-repeat, on a box with 5mm padding. The orange and the image start 5mm inside the grey border, leaving a white frame.

+
+ +

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.

+
+ +

A background that is not a background is dropped

+

background: url(bg.jpg) bogus is dropped, so the light green of the earlier rule stays, with no image.

+
+ mpdf = $this->createMpdf(); + $this->mpdf->SetBasePath(__DIR__ . '/../data'); + + $this->mpdf->WriteHTML($html); + } + +} diff --git a/tests/Snapshots/BorderShorthandSnapshotTest.php b/tests/Snapshots/BorderShorthandSnapshotTest.php new file mode 100644 index 000000000..05c8a09c3 --- /dev/null +++ b/tests/Snapshots/BorderShorthandSnapshotTest.php @@ -0,0 +1,74 @@ + + + +

Border shorthand

+ +

Width, style and colour in any order

+

Each box has a 2mm solid red border on all four sides.

+
border: 2mm solid #cc0000
+
border: solid #cc0000 2mm
+
border: 2mm #cc0000 solid
+
border: #cc0000 2mm solid
+
border: solid 2mm #cc0000
+
border: red 2mm solid
+ +

One side, and a colour function first

+

A 1mm dashed blue line under the text only.

+
border-bottom: dashed blue 1mm
+

A 1.5mm double green border on all four sides.

+
border: rgb(0 128 0) 1.5mm double
+ +

Table cells and inline text

+

Each cell has a 1mm solid blue border. The word "boxed" has a 0.5mm dotted red border.

+
solid 1mm #3366ccsolid 1mm #3366cc
+

Text with a boxed word.

+ +

A border that is not a border is dropped

+

border: 1px solid bogus is dropped, so the 2mm solid red border of the earlier rule stays.

+
border: 1px solid bogus
+ mpdf = $this->createMpdf(); + + $this->mpdf->WriteHTML($html); + } + +} diff --git a/tests/data/snapshots/background-shorthand.pdf b/tests/data/snapshots/background-shorthand.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b10728698f32500f329de4fd9717eec625b4a0ce GIT binary patch literal 46364 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 Q0p$i*e9`FWBo(Fp4}~Z86aWAK literal 0 HcmV?d00001 diff --git a/tests/data/snapshots/border-shorthand.pdf b/tests/data/snapshots/border-shorthand.pdf new file mode 100644 index 0000000000000000000000000000000000000000..33fadae0d3dfd93f146827bde93234e469b1b76c GIT binary patch literal 38633 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�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

Cod0Dtl>?=TPc|~hB~rCJkux1*kz^bPu=CA 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

bIO524;-8cgiBytpIe!W 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 literal 0 HcmV?d00001 From a732c048630a5df5629f87571b5da2c622f0feb1 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Wed, 30 Sep 2026 10:10:44 +1000 Subject: [PATCH 2/2] Keep the -webkit- and -o- prefix of a gradient in the background shorthand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since #577, a -webkit- or -o- gradient is told apart from an unprefixed one by its prefix, and keeps the legacy angles and start side. The background shorthand parser dropped every prefix but -moz-, so `background: -webkit-linear-gradient(left, …)` was read as an unprefixed gradient. It now keeps -moz-, -webkit- and -o-, and still drops any other. Co-Authored-By: Claude Opus 5.5 --- src/Css/NormalizeProperties.php | 4 ++-- tests/Mpdf/Css/NormalizePropertiesTest.php | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Css/NormalizeProperties.php b/src/Css/NormalizeProperties.php index 2755a6f5d..06dff59d9 100644 --- a/src/Css/NormalizeProperties.php +++ b/src/Css/NormalizeProperties.php @@ -530,8 +530,8 @@ private function parseBackgroundLayer(array $components) if (preg_match('/^url\(\s*([\'"]?)(.*)\1\s*\)$/is', $components[$i], $m)) { $kind = 'i'; $value = $m[2]; - } elseif (preg_match('/^(?:-(?!moz-)[a-z]+-)?((-moz-)?(repeating-)?(linear|radial)-gradient\(.*\))$/is', $components[$i], $m)) { - // Drops a vendor prefix other than -moz-, which the gradient parser reads + } elseif (preg_match('/^(?:-(?!(?:moz|webkit|o)-)[a-z]+-)?((-moz-|-webkit-|-o-)?(repeating-)?(linear|radial)-gradient\(.*\))$/is', $components[$i], $m)) { + // Drops a vendor prefix other than -moz-, -webkit- and -o-, which the gradient parser reads for their legacy angles $kind = 'i'; $value = $m[1]; } elseif ($keyword === 'none') { diff --git a/tests/Mpdf/Css/NormalizePropertiesTest.php b/tests/Mpdf/Css/NormalizePropertiesTest.php index aba8ec0a3..0955b541d 100644 --- a/tests/Mpdf/Css/NormalizePropertiesTest.php +++ b/tests/Mpdf/Css/NormalizePropertiesTest.php @@ -367,9 +367,13 @@ public function providerBackground() 'inherit', ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => ''], ], - 'a -webkit- gradient' => [ + 'a -webkit- gradient keeps its prefix for its legacy angles' => [ '-webkit-linear-gradient(left, red, blue)', - ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => 'linear-gradient(left, red, blue)'], + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => '-webkit-linear-gradient(left, red, blue)'], + ], + 'a gradient with another vendor prefix' => [ + '-ms-linear-gradient(to right, red, blue)', + ['BACKGROUND-COLOR' => 'transparent', 'BACKGROUND-IMAGE' => 'linear-gradient(to right, red, blue)'], ], 'a -moz- gradient' => [ '-moz-linear-gradient(left, red, blue)',