Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG-GRAVITYPDF.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ New features
* A reset such as `* { margin: 0; padding: 0 }` now takes away the default margins of paragraphs, headings and lists, a list's indent and a cell's padding, as in a browser. It reaches the lines of the table of contents and the index as well, which are the document's elements, so their levels are no longer indented. Style `div.mpdf_toc_level_1` and the like to indent them again.
* mPDF's own `<barcode>`, `<dottab>` and `<textcircle>` are not HTML elements. A rule reaches them only by naming their tag, or an id or class they carry, so `*`, and `:empty`, `[attr]` or `:not()` alone, leave them as they are: `:empty { display: none }` does not hide a barcode.
* **Font weights as numbers, `bolder` and `lighter`, and `font-size: larger` and `smaller`.** In the standard CSS mode, `font-weight` takes a number from 1 to 1000, and `bolder` and `lighter` step from the parent's weight by the table in CSS Fonts. The weight is inherited as a number, so `bolder` inside `bolder` gives 900 and `lighter` inside 700 gives 400, and `h1 { font-weight: lighter }` is regular, one step down from its parent's 400. A weight above 500 is drawn with the family's bold face and any other with its regular face. `larger` and `smaller` multiply and divide the parent's size by 1.2. A form field reads them against the document's size, as it reads its other font sizes. The `font` shorthand's weight is read the same way. All of these were ignored before, so a document that has them changes. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to read only `normal` and `bold`, as before. [#545] [#647]
* **The `text-decoration` longhands, and `text-align: start`, `end` and `match-parent`, in standard mode.** They were ignored before, so a document that has them changes. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to keep ignoring them. [#552] [#607]
* `text-decoration-line`, `text-decoration-style`, `text-decoration-color` and `text-decoration-thickness` are read, and so are the style, colour and thickness parts of the `text-decoration` shorthand, which resets those it leaves out. `p { text-decoration: underline wavy #f00 }` draws a red wavy underline where it drew a straight one in the text colour.
* The styles are `solid`, `double`, `dotted`, `dashed` and `wavy`. A thickness is a length, or a percentage of the font size; `auto` and `from-font` keep the font's.
* `text-underline-offset` sets the top of the underline that far below the baseline, and `text-underline-position: under` puts it below the descenders. Both are inherited, and read where the underline is set.
* `text-align: start` and `end` align a block's or a cell's text to the left or right by its direction, so `end` aligns ltr text right and rtl text left. `match-parent` takes the parent's alignment, with `start` or `end` taken against the parent's direction.

Performance
-----------
Expand Down Expand Up @@ -964,4 +969,5 @@ These changes do not change output.
[#588]: https://github.com/GravityPDF/mpdf/issues/588
[#691]: https://github.com/GravityPDF/mpdf/pull/691
[#584]: https://github.com/GravityPDF/mpdf/issues/584
[#607]: https://github.com/GravityPDF/mpdf/issues/607
[#689]: https://github.com/GravityPDF/mpdf/pull/689
5 changes: 5 additions & 0 deletions src/Css/ComputedValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ final class ComputedValues
'TEXT-INDENT' => true,
'TEXT-SHADOW' => true,
'TEXT-OUTLINE-WIDTH' => true,
'TEXT-UNDERLINE-OFFSET' => true,
'PADDING-TOP' => true,
'PADDING-RIGHT' => true,
'PADDING-BOTTOM' => true,
Expand Down Expand Up @@ -187,6 +188,10 @@ public function compute(array $properties, array $parent, $own = null, $medium =
}
}

if (isset($own['TEXT-ALIGN'], $properties['TEXT-ALIGN']) && strtolower($properties['TEXT-ALIGN']) === 'match-parent') {
$properties['TEXT-ALIGN'] = TextAlign::matchParent($parent);
}

if (isset($own['LINE-HEIGHT'], $properties['LINE-HEIGHT'])) {
$properties['LINE-HEIGHT'] = $this->computeLineHeight($properties['LINE-HEIGHT'], $fontSize);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Css/CssMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ private function mergeInCascadeOrder($inherit, $tag, array $attr)
$properties = array_merge($this->cssProperties, InheritedProperties::of($computed, InheritedProperties::names()));

// An inline element, and a block in a table cell, is drawn over the state of what it is in, which already
// holds what it inherits
return $inherit === 'INLINE' || $inherit === '' ? array_intersect_key($properties, $own) : $properties;
// holds what it inherits. What places an underline is read where the underline is set, so it comes too
return $inherit === 'INLINE' || $inherit === '' ? array_intersect_key($properties, $own) + InheritedProperties::of($properties, InheritedProperties::DECORATION) : $properties;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/Css/CssWideKeywords.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,15 @@ final class CssWideKeywords
'POSITION' => 'static',
'TEXT-ALIGN' => 'start',
'TEXT-DECORATION' => 'none',
'TEXT-DECORATION-COLOR' => 'currentcolor',
'TEXT-DECORATION-STYLE' => 'solid',
'TEXT-DECORATION-THICKNESS' => 'auto',
'TEXT-INDENT' => '0',
'TEXT-OUTLINE' => 'none',
'TEXT-SHADOW' => 'none',
'TEXT-TRANSFORM' => 'none',
'TEXT-UNDERLINE-OFFSET' => 'auto',
'TEXT-UNDERLINE-POSITION' => 'auto',
'VERTICAL-ALIGN' => 'baseline',
'VISIBILITY' => 'visible',
'WHITE-SPACE' => 'normal',
Expand Down
13 changes: 11 additions & 2 deletions src/Css/InheritedProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ final class InheritedProperties
'LIST-STYLE-POSITION',
];

/**
* Read by Mpdf::setCSS() where an element sets an underline, so an element hands them on through the computed
* values on its frame alone. Under CssMode::LEGACY they are not read
*/
const DECORATION = [
'TEXT-UNDERLINE-OFFSET',
'TEXT-UNDERLINE-POSITION',
];

/**
* What a positioned block handed its content under CssMode::LEGACY, in the order it was written. Text decorations
* and the stacking order are not inherited, but the <div> that stands in for the block takes them too
Expand Down Expand Up @@ -90,13 +99,13 @@ final class InheritedProperties
];

/**
* @return string[] Every inherited property mPDF supports: TEXT, then BLOCK
* @return string[] Every inherited property mPDF supports: TEXT, then BLOCK, then DECORATION
*/
public static function names()
{
static $names = null;
if ($names === null) {
$names = array_merge(self::TEXT, self::BLOCK);
$names = array_merge(self::TEXT, self::BLOCK, self::DECORATION);
}

return $names;
Expand Down
39 changes: 39 additions & 0 deletions src/Css/NormalizeProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class NormalizeProperties
'BORDER-LEFT-WIDTH' => true, 'BORDER-SPACING' => true,
'BORDER-RADIUS' => true, 'BORDER-TOP-LEFT-RADIUS' => true, 'BORDER-TOP-RIGHT-RADIUS' => true,
'BORDER-BOTTOM-LEFT-RADIUS' => true, 'BORDER-BOTTOM-RIGHT-RADIUS' => true,
'TEXT-DECORATION-THICKNESS' => true, 'TEXT-UNDERLINE-OFFSET' => true,
];

/**
Expand All @@ -92,6 +93,7 @@ class NormalizeProperties
private static $colorProperties = [
'COLOR' => true, 'BACKGROUND-COLOR' => true,
'BORDER-TOP-COLOR' => true, 'BORDER-RIGHT-COLOR' => true, 'BORDER-BOTTOM-COLOR' => true, 'BORDER-LEFT-COLOR' => true,
'TEXT-DECORATION-COLOR' => true,
];

/**
Expand Down Expand Up @@ -140,6 +142,8 @@ class NormalizeProperties
],
'LIST-STYLE' => ['LIST-STYLE-TYPE', 'LIST-STYLE-IMAGE', 'LIST-STYLE-POSITION'],
'TEXT-OUTLINE' => ['TEXT-OUTLINE', 'TEXT-OUTLINE-WIDTH', 'TEXT-OUTLINE-COLOR'],
'TEXT-DECORATION' => ['TEXT-DECORATION', 'TEXT-DECORATION-STYLE', 'TEXT-DECORATION-COLOR', 'TEXT-DECORATION-THICKNESS'],
'TEXT-DECORATION-LINE' => ['TEXT-DECORATION'],
'BREAK-BEFORE' => ['PAGE-BREAK-BEFORE'],
'BREAK-AFTER' => ['PAGE-BREAK-AFTER'],
'BREAK-INSIDE' => ['PAGE-BREAK-INSIDE'],
Expand Down Expand Up @@ -230,6 +234,11 @@ public function normalize($prop)
$this->processBorderShorthandProperty($k, $v);
} elseif ($k === 'TEXT-OUTLINE') {
$this->processTextOutlineProperty($v);
} elseif ($standard && $k === 'TEXT-DECORATION') {
$this->processTextDecorationProperty($v);
} elseif ($standard && $k === 'TEXT-DECORATION-LINE') {
// TEXT-DECORATION holds the lines alone, so what reads it, and propagates it, reads this too
$this->properties['TEXT-DECORATION'] = $v;
} elseif ($k === 'SIZE' || $k === 'SHEET-SIZE') {
$this->processPageSizeProperty($k, $v);
} elseif ($k === 'BREAK-BEFORE' || $k === 'BREAK-AFTER' || $k === 'BREAK-INSIDE') {
Expand Down Expand Up @@ -1196,6 +1205,36 @@ protected function processTextOutlineProperty($v)
}
}

/**
* Expands the text-decoration shorthand into the lines, kept as TEXT-DECORATION, and the style, colour and
* thickness, each reset to its initial value where the shorthand leaves it out
*
* @param string $v Property value
* @return void
*/
private function processTextDecorationProperty($v)
{
$lines = [];
$parts = [];
foreach (['TEXT-DECORATION-STYLE', 'TEXT-DECORATION-COLOR', 'TEXT-DECORATION-THICKNESS'] as $longhand) {
$parts[$longhand] = CssWideKeywords::initialValue($longhand);
}
foreach ($this->splitComponents($v) as $component) {
if ($component === 'none' || $component === 'blink' || in_array($component, TextDecorations::LINES, true)) {
$lines[] = $component;
} elseif (in_array($component, TextDecorations::STYLES, true)) {
$parts['TEXT-DECORATION-STYLE'] = $component;
} elseif ($component === 'auto' || $component === 'from-font' || $this->sizeConverter->isLength($component)) {
$parts['TEXT-DECORATION-THICKNESS'] = $component;
} else {
$parts['TEXT-DECORATION-COLOR'] = $component;
}
}

$this->properties['TEXT-DECORATION'] = $lines ? implode(' ', $lines) : 'none';
$this->properties = array_merge($this->properties, $parts);
}

/**
* Process page size CSS properties.
*
Expand Down
42 changes: 42 additions & 0 deletions src/Css/TextAlign.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Mpdf\Css;

/**
* The logical values of text-align, which mPDF reads under CssMode::STANDARD: start and end, taken against the
* direction of the element that has them, and match-parent, which takes the parent's alignment with its start or end
* taken against the parent's direction
*/
final class TextAlign
{

/**
* @param string $value start or end
* @param string|null $direction ltr or rtl; ltr when null
*
* @return string L or R, as mPDF holds an alignment
*/
public static function physical($value, $direction)
{
$rtl = strtolower((string) $direction) === 'rtl';

return (strtolower($value) === 'end') !== $rtl ? 'R' : 'L';
}

/**
* What text-align: match-parent computes to
*
* @param array $parent The computed values on the parent's frame
*
* @return string The parent's text-align, with start and end replaced by left or right
*/
public static function matchParent(array $parent)
{
$align = isset($parent['TEXT-ALIGN']) ? strtolower($parent['TEXT-ALIGN']) : 'start';
if (!in_array($align, ['start', 'end', 'match-parent'], true)) {
return $parent['TEXT-ALIGN'];
}

return self::physical($align, isset($parent['DIRECTION']) ? $parent['DIRECTION'] : null) === 'R' ? 'right' : 'left';
}
}
106 changes: 104 additions & 2 deletions src/Css/TextDecorations.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
* their own.
*
* Each decoration is a TextVars bit in the text state, with the text parameter that says how the element that set it
* draws it: its colour, font, font size and baseline shift, as Mpdf::setCSS() records them. mPDF draws one line of each
* kind, so where an element and its ancestor set the same decoration the element's own is drawn.
* draws it: its colour, font, font size and baseline shift, as Mpdf::setCSS() records them, and under CssMode::STANDARD
* any style, thickness and underline position its CSS sets. mPDF draws one line of each kind, so where an element and
* its ancestor set the same decoration the element's own is drawn.
*
* Under CssMode::LEGACY text-decoration is inherited as it was in mPDF v7.
*/
Expand All @@ -32,6 +33,39 @@ final class TextDecorations
TextVars::FD_OVERLINE => 'o-decoration',
];

/**
* Each decoration's TextVars bit, and the keyword that sets it
*/
const LINES = [
TextVars::FD_UNDERLINE => 'underline',
TextVars::FD_LINETHROUGH => 'line-through',
TextVars::FD_OVERLINE => 'overline',
];

/**
* The keys of a decoration's text parameter that Mpdf::setCSS() sets only where the element's CSS asks for them. An
* element's own decoration drops them first, so it does not keep those of the decoration it replaces
*/
const DRAWN_AS = ['style' => true, 'thickness' => true, 'offset' => true, 'under' => true];

/**
* The values of text-decoration-style
*/
const STYLES = ['solid', 'double', 'dotted', 'dashed', 'wavy'];

/**
* Which way from its position a decoration's second line goes, and a wave's first arch: away from the text for an
* underline and an overline, and either side of the line for a line-through. In PDF units, which run up the page
*/
const UNDER = -1;
const THROUGH = 0;
const OVER = 1;

/**
* Where a cubic Bézier arch's control points go along a half wave, as a fraction of it, to follow a sine
*/
const SINE_CONTROL = 0.3642;

/**
* Under CssMode::STANDARD, starts an element's text with the decorations of the element it is in, or with none when
* the element is floated, positioned out of the flow or an inline block. Call it before Mpdf::setCSS() adds the
Expand Down Expand Up @@ -111,4 +145,72 @@ private static function propagateInto(array $properties)
&& !in_array($position, ['absolute', 'fixed'], true)
&& !in_array($display, ['inline-block', 'inline-table'], true);
}

/**
* The PDF operators that stroke a decoration, in the stroke colour and line width already set. All lengths are in
* PDF units.
*
* - solid, or a style not listed: one line
* - double: two lines of the thickness, a thickness apart, the second away from the text
* - dotted: round dots the thickness across, a dot's width apart
* - dashed: dashes three thicknesses long, two apart
* - wavy: a sine three thicknesses high from crest to trough, of half wavelength three thicknesses, fitted to the
* width with whole half waves, each a cubic Bézier arch
*
* @param string $style One of STYLES
* @param float $x Where the decoration starts
* @param float $y Where its centre line runs
* @param float $width
* @param float $thickness
* @param int $side UNDER, THROUGH or OVER
*
* @return string
*/
public static function stroke($style, $x, $y, $width, $thickness, $side)
{
$line = function ($y) use ($x, $width) {
return sprintf('%.3F %.3F m %.3F %.3F l S', $x, $y, $x + $width, $y);
};

switch ($style) {
case 'double':
$first = $side === self::THROUGH ? $y + $thickness : $y;
$second = $side === self::THROUGH ? $y - $thickness : $y + 2 * $thickness * $side;

return $line($first) . ' ' . $line($second);

case 'dotted':
return sprintf('1 J [0 %.3F] 0 d ', 2 * $thickness) . $line($y) . ' [] 0 d';

case 'dashed':
return sprintf('[%.3F %.3F] 0 d ', 3 * $thickness, 2 * $thickness) . $line($y) . ' [] 0 d';

case 'wavy':
$count = max(1, (int) round($width / (3 * $thickness)));
$half = $width / $count;
// A cubic's control points at height h raise its middle by 3h/4, so 2 thicknesses give a crest of 1.5
$height = 2 * $thickness;
$crest = $side === self::THROUGH ? self::OVER : $side;

$path = sprintf('1 j %.3F %.3F m', $x, $y);
for ($i = 0; $i < $count; $i++) {
$start = $x + $i * $half;
$control = $y + $crest * $height;
$path .= sprintf(
' %.3F %.3F %.3F %.3F %.3F %.3F c',
$start + self::SINE_CONTROL * $half,
$control,
$start + (1 - self::SINE_CONTROL) * $half,
$control,
$start + $half,
$y
);
$crest = -$crest;
}

return $path . ' S';
}

return $line($y);
}
}
Loading
Loading