Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG-GRAVITYPDF.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Read this section before upgrading from upstream mPDF. Each entry says what chan
* A table's `border` gives each cell a 1px border of its own, which a rule for the table's border leaves alone. Before, the cells copied the table's border.
* **`line-height` on an inline element sets the height of its line**, as in a browser. mPDF ignored it, so `<span style="line-height: 30mm">` left its line as it was. Now each inline element's box is as tall as its line-height, and a line grows to hold the tallest box on it, text or image, in blocks and table cells alike. The block's own line height is the least a line can be, so a smaller inline line-height, or `0`, does not shrink it. A percentage in `vertical-align` is taken of the element's own line-height. `line-height` on a `<p>` or `<div>` inside a table cell, which mPDF lays out as inline content, can now make its lines taller than the cell's. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to ignore it as before. [#548] [#646]
* **`rem` is read against the font size of `html`, in standard mode**, as in a browser. It was read against the font size of `body`, and inside a table against the table's. Now `1rem` is the default font size, from the `default_font_size` configuration or `SetDefaultFontSize()`, unless an `html` or `:root` rule sizes `html`: with `html { font-size: 62.5% }`, `1.6rem` is the default size again. A `body { font-size }` rule no longer changes it. To keep the old sizes, give them in `em` or points, or set `'cssMode' => \Mpdf\CssMode::LEGACY`. [#529] [#642]
* **A border, `box-shadow` or `text-shadow` that names no colour is drawn in the element's colour**, which is `currentColor`, as in a browser. A border was drawn black and a shadow grey (`#888888`). `div { color: #c00; border: 1px solid; box-shadow: 2mm 2mm }` now draws a red border and a red shadow. This applies to the `border` shorthands and to a border built from `border-style` or `border-width` alone. Name the colour, as in `border: 1px solid #000`, or set `'cssMode' => \Mpdf\CssMode::LEGACY`, to keep black borders and grey shadows. [#540] [#651]
* `currentColor` written in `background-color`, a gradient stop, `box-shadow`, `text-shadow`, `topntail` or `thead-underline` is read as the element's colour too, and `color: currentColor` as the colour it inherits. Before, the background was not painted, the gradient lost the stop, the shadows were grey, the table lines black, and `color: currentColor` on a block drew black. Legacy mode ignores `currentColor` there as before.

New features
------------
Expand Down Expand Up @@ -206,6 +208,12 @@ Bugfixes
* 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]
* `tr:nth-child()` counted the rows of the whole table less its header and footer rows, so a second `<tbody>` carried on the count of the first, and `tr:first-child` missed the first footer row when `<tfoot>` came after the body. Rows are now counted within their `<thead>`, `<tbody>` or `<tfoot>`, or within the run of rows written straight into the table. [#528] [#582]
* `currentColor` in a border raised two "Uninitialized string offset" warnings for each side and drew it black, and in `text-outline` it raised an undefined-key warning and drew black. Both are now drawn in the element's colour, set or inherited. [#540] [#651]
* Colours that convert to nothing were drawn black or grey instead of drawing nothing. [#552] [#540] [#651]
* A `transparent` side of a border, as in Bootstrap's `border: 1px solid transparent`, was drawn black, with warnings on a block. It now takes its width and draws nothing, on blocks, inline elements, table cells and images.
* `border-style: hidden` on a block or an image drew a black hairline, with warnings.
* `color: transparent` drew black text. The text is now laid out and written in the invisible text rendering mode, so it takes its place and can be selected but is not seen. Its `text-shadow` is still drawn.
* A `box-shadow` or `text-shadow` in `transparent` was grey. It is no longer drawn.
* `font: small-caps 14pt serif` drew the text in full-size capitals, as `text-transform: uppercase` does. It is now drawn in small capitals, as `font-variant: small-caps` draws it. The `font` shorthand also kept a `line-height` and a `font-variant` it inherited; like the style and weight, in standard mode they are now reset to `normal` when the shorthand does not name them, as in a browser. To keep a line height, give it in the shorthand, as in `font: 14pt/1.5 serif`, or declare `line-height` after it. A `font` shorthand with a negative line height, such as `font: 12pt/-1 serif`, is dropped whole. `font-variant: normal` now resets `font-variant-position` as well. [#552] [#581]
* 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]
Expand Down Expand Up @@ -830,6 +838,7 @@ These changes do not change output.
[#538]: https://github.com/GravityPDF/mpdf/issues/538
[#545]: https://github.com/GravityPDF/mpdf/issues/545
[#539]: https://github.com/GravityPDF/mpdf/issues/539
[#540]: https://github.com/GravityPDF/mpdf/issues/540
[#548]: https://github.com/GravityPDF/mpdf/issues/548
[#549]: https://github.com/GravityPDF/mpdf/pull/549
[#552]: https://github.com/GravityPDF/mpdf/issues/552
Expand Down Expand Up @@ -889,3 +898,4 @@ These changes do not change output.
[#644]: https://github.com/GravityPDF/mpdf/pull/644
[#647]: https://github.com/GravityPDF/mpdf/pull/647
[#648]: https://github.com/GravityPDF/mpdf/pull/648
[#651]: https://github.com/GravityPDF/mpdf/pull/651
14 changes: 14 additions & 0 deletions src/Css/Border.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,18 @@ class Border
const RIGHT = 4;
const BOTTOM = 2;
const LEFT = 1;

/**
* Whether a side of a border, as Mpdf::border_details() reads it, draws anything. A hidden side has no width, and a
* transparent one has false for its colour, which a side packed with a table cell keeps as NUL bytes. A colour
* mPDF cannot read is drawn black, as it was
*
* @param array|null $side
*
* @return bool
*/
public static function drawsSide($side)
{
return !empty($side['s']) && !empty($side['w']) && isset($side['c']) && $side['c'] !== false && strpos($side['c'], "\0") !== 0;
}
}
25 changes: 22 additions & 3 deletions src/Css/BorderMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Mpdf\Css;

use Mpdf\CssMode;
use Mpdf\Exception\InvalidArgumentException;

class BorderMerger
Expand All @@ -26,17 +27,30 @@ class BorderMerger
'L' => 0,
];

/**
* The colour of a border that names none: currentColor in standard mode, and black in legacy mode
*
* @param string $cssMode One of the Mpdf\CssMode values
* @return string
*/
public static function initialColor($cssMode)
{
return $cssMode === CssMode::STANDARD ? 'currentcolor' : self::DEFAULTS['COLOR'];
}

/**
* Merge borders into CSS properties.
*
* @param array $newProperties properties to merge from
* @param array $cssProperties current CSS properties (passed by reference)
* @param string|null $initialColor The colour a side's shorthand is built with when no longhand gives one, in
* place of the default
* @return void
*/
public function mergeBorderProperties($newProperties, &$cssProperties)
public function mergeBorderProperties($newProperties, &$cssProperties, $initialColor = null)
{
foreach (['TOP', 'RIGHT', 'BOTTOM', 'LEFT'] as $side) {
$this->mergeSideBorder($side, $newProperties, $cssProperties);
$this->mergeSideBorder($side, $newProperties, $cssProperties, $initialColor);
}
}

Expand All @@ -49,9 +63,11 @@ public function mergeBorderProperties($newProperties, &$cssProperties)
* @param string $side Side to merge (TOP, RIGHT, BOTTOM, LEFT)
* @param array $properties Source border properties
* @param array $cssProperties Target CSS properties (passed by reference)
* @param string|null $initialColor The colour to build the shorthand with when no longhand gives one, in place of
* the default
* @return void
*/
protected function mergeSideBorder($side, $properties, &$cssProperties)
protected function mergeSideBorder($side, $properties, &$cssProperties, $initialColor = null)
{
// Merges $a['BORDER-TOP-STYLE'] to $cssProperties['BORDER-TOP'] etc.
$borderKey = 'BORDER-' . $side;
Expand Down Expand Up @@ -79,6 +95,9 @@ protected function mergeSideBorder($side, $properties, &$cssProperties)
// Build new border from scratch with defaults
if (!isset($borderParts)) {
$borderParts = self::DEFAULTS;
if ($initialColor !== null) {
$borderParts['COLOR'] = $initialColor;
}
}

$borderParts[$el] = $value;
Expand Down
125 changes: 124 additions & 1 deletion src/Css/CssMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@
class CssMerger
{

/**
* The properties that resolve currentColor in both modes. Legacy mode raised warnings for it in each
*/
const CURRENT_COLOR_PROPERTIES = [
'BORDER-TOP',
'BORDER-RIGHT',
'BORDER-BOTTOM',
'BORDER-LEFT',
'BORDER-TOP-COLOR',
'BORDER-RIGHT-COLOR',
'BORDER-BOTTOM-COLOR',
'BORDER-LEFT-COLOR',
'TEXT-OUTLINE-COLOR',
];

/**
* The other properties mPDF reads a colour from that can name currentColor, which legacy mode ignores in them. A
* background image names it in the stops of a gradient
*/
const STANDARD_CURRENT_COLOR_PROPERTIES = [
'BACKGROUND-COLOR',
'BACKGROUND-IMAGE',
'BOX-SHADOW',
'TEXT-SHADOW',
'TOPNTAIL',
'THEAD-UNDERLINE',
];

/**
* @var \Mpdf\Mpdf
*/
Expand Down Expand Up @@ -168,6 +196,7 @@ public function merge($inherit, $tag, $attr)
$this->mergeTagSpecificSelectors($tag, $attr, $classes, $languageCode);
$this->mergeDescendantSelectors($inherit, $tag, $attr, $classes, $languageCode);
$this->mergeInlineStyle($tag, $attr);
$this->resolveCurrentColor($inherit);

return $this->cssProperties;
}
Expand Down Expand Up @@ -233,10 +262,104 @@ private function mergeInCascadeOrder($inherit, $tag, array $attr)

list($inline, $importantInline) = isset($attr['STYLE']) ? $this->inlineStyleParser->parseByImportance($attr['STYLE']) : [[], []];
$this->mergeEach(array_merge([$inline], $importantRules, [$importantInline], $importantDefaultRules), $dominance);
$this->resolveCurrentColor($inherit);

return $this->cssProperties;
}

/**
* Replaces currentColor in the merged properties with the element's colour, which is its own color, or the one it
* inherits. In standard mode, color: currentColor is the colour the element inherits, and a shadow that names no
* colour takes currentColor, as a border does. Legacy mode resolves it only where it raised warnings
*
* @param string $inherit Inheritance context (BLOCK, INLINE, TABLE, TOPTABLE, or empty)
* @return void
*/
private function resolveCurrentColor($inherit)
{
$properties = self::CURRENT_COLOR_PROPERTIES;

if ($this->mpdf->cssMode === CssMode::STANDARD) {
$properties = array_merge($properties, self::STANDARD_CURRENT_COLOR_PROPERTIES);

foreach (['BOX-SHADOW', 'TEXT-SHADOW'] as $property) {
if (isset($this->cssProperties[$property])) {
$this->cssProperties[$property] = ShadowParser::withColor($this->cssProperties[$property], 'currentcolor');
}
}

if (isset($this->cssProperties['COLOR']) && strtolower($this->cssProperties['COLOR']) === 'currentcolor') {
$inherited = $this->inheritedColor($inherit);
if ($inherited === null) {
unset($this->cssProperties['COLOR']);
} else {
$this->cssProperties['COLOR'] = $inherited;
}
}
}

$color = null;
foreach ($properties as $property) {
if (!isset($this->cssProperties[$property]) || stripos($this->cssProperties[$property], 'currentcolor') === false) {
continue;
}

// The same word in a url() is part of the address
if ($property === 'BACKGROUND-IMAGE' && stripos($this->cssProperties[$property], 'gradient(') === false) {
continue;
}

if ($color === null) {
$color = $this->elementColor($inherit);
}

$this->cssProperties[$property] = str_ireplace('currentcolor', $color, $this->cssProperties[$property]);
}
}

/**
* The colour currentColor stands for in the element's merged properties
*
* @param string $inherit Inheritance context
* @return string A colour with no spaces, which a border or shadow value keeps as one component
*/
private function elementColor($inherit)
{
$color = isset($this->cssProperties['COLOR']) ? $this->cssProperties['COLOR'] : '';
if (!$this->colorConverter->isColor($color) && strtolower($color) !== 'transparent') {
$color = $this->inheritedColor($inherit);
}

if ($color === null) {
$color = isset($this->cssManager->CSS['BODY']['COLOR']) ? $this->cssManager->CSS['BODY']['COLOR'] : '#000000';
}

return str_replace(' ', '', $color);
}

/**
* The colour an element takes when it sets none, as mPDF passes it on in each context: a block takes the colour of
* the block it is opened in, a table and its parts the table's, and anything else the colour of the text around it
*
* @param string $inherit Inheritance context
* @return string|null Null for the document's default colour
*/
private function inheritedColor($inherit)
{
if ($inherit === 'TABLE' || $inherit === 'TOPTABLE') {
return isset($this->mpdf->base_table_properties['COLOR']) ? $this->mpdf->base_table_properties['COLOR'] : null;
}

if ($inherit === 'BLOCK') {
$level = $this->getBlockLevel();
$colorarray = isset($this->mpdf->blk[$level]['InlineProperties']['colorarray']) ? $this->mpdf->blk[$level]['InlineProperties']['colorarray'] : '';
} else {
$colorarray = $this->mpdf->colorarray;
}

return $colorarray ? $this->colorConverter->colAtoString($colorarray) : null;
}

/**
* Merges html's rules into body's CSS, and body's own rules again over them. mPDF has no html element: the
* document's frame stands for body, and html is matched as its parent, so html's declarations, its !important
Expand Down Expand Up @@ -1214,7 +1337,7 @@ protected function setMergedCss(&$property, $strictMode = true, $borderDominance
*/
protected function mergeBorderProperties($properties)
{
$this->borderMerger->mergeBorderProperties($properties, $this->cssProperties);
$this->borderMerger->mergeBorderProperties($properties, $this->cssProperties, BorderMerger::initialColor($this->mpdf->cssMode));
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/Css/InlinePropertyConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ public function convert($properties)
$css['TEXT-OUTLINE-COLOR'] = $this->colorConverter->colAtoString($properties['textparam']['outline-COLOR']);
}

if (!empty($properties['textparam']['transparent'])) {
$css['COLOR'] = 'transparent';
}

if (!empty($properties['textparam']['outline-WIDTH'])) {
$css['TEXT-OUTLINE-WIDTH'] = $properties['textparam']['outline-WIDTH'] . 'mm';
}
Expand Down
5 changes: 3 additions & 2 deletions src/Css/NormalizeProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -901,7 +901,8 @@ protected function normalizeBorderString($bd)
* Parse border property parts (width, style, color).
*
* 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.
* keyword gives the initial value of all three until the keywords are resolved. The initial colour is currentColor
* in standard mode, and black in legacy mode.
*
* @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
Expand Down Expand Up @@ -936,7 +937,7 @@ protected function parseBorderParts($prop)
$parts[$kind] = $part;
}

return $parts + ['w' => 'medium', 's' => 'none', 'c' => '#000000'];
return $parts + ['w' => 'medium', 's' => 'none', 'c' => BorderMerger::initialColor($this->mpdf->cssMode)];
}

/**
Expand Down
29 changes: 29 additions & 0 deletions src/Css/ShadowParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ public function normalizeShadowColors($value)
return $value;
}

/**
* Gives each shadow in a box-shadow or text-shadow value that names no colour the colour given. A component that
* starts with a letter or # and is not inset is a colour, and so is none, which leaves the value as it is.
*
* @param string $value Shadow property value
* @param string $color
* @return string
*/
public static function withColor($value, $color)
{
$shadows = preg_split('/,(?![^(]*\))/', $value);
foreach ($shadows as $i => $shadow) {
if (!preg_match('/(^|\s)(?!inset\b)[a-z#]/i', trim($shadow))) {
$shadows[$i] = rtrim($shadow) . ' ' . $color;
}
}

return implode(',', $shadows);
}

/**
* Parse box-shadow CSS property.
*
Expand Down Expand Up @@ -139,6 +159,11 @@ protected function parseSingleBoxShadow($s)
);
}

if (isset($boxShadow['col']) && $boxShadow['col'] === false) {
// A transparent shadow draws nothing
return null;
}

if (empty($boxShadow['col'])) {
$boxShadow['col'] = $this->colorConverter->convert('#888888', $this->mpdf->PDFAXwarnings);
}
Expand Down Expand Up @@ -243,6 +268,10 @@ protected function parseSingleTextShadow($s)
);
}

if (isset($textShadow['col']) && $textShadow['col'] === false) {
return null;
}

if (empty($textShadow['col'])) {
$textShadow['col'] = $this->colorConverter->convert(
'#888888',
Expand Down
Loading
Loading