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
3 changes: 3 additions & 0 deletions CHANGELOG-GRAVITYPDF.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Read this section before upgrading from upstream mPDF. Each entry says what chan
* **`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]
* `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.
* **`text-indent` indents the first line of a table cell**, as in a browser, in standard mode. mPDF ignored it in cells, including the value a cell inherits, so `body { text-indent: 10mm }` indented paragraphs and left cells alone. Now a cell's first line is indented by its own `text-indent` or the one it inherits from its table and the blocks around it, and a cell sized to its content is made wide enough for the indent. Set `td, th { text-indent: 0 }` to keep cells unindented, or set `'cssMode' => \Mpdf\CssMode::LEGACY`. [#608]
* **A word too long for its line overflows it when `word-break` or `overflow-wrap` leave it nowhere to break**, as in a browser, in standard mode. mPDF ignored both and always cut such a word where the line was full. That is still what happens when neither property is set, but `overflow-wrap: normal`, `word-break: normal` or `word-break: keep-all` on its own now let the word run out of its box. Set `overflow-wrap: anywhere` to have it cut, or set `'cssMode' => \Mpdf\CssMode::LEGACY`. [#607]
* A `<p>` or `<div>` inside a cell, which mPDF lays out as part of the cell's text, is indented only when it starts the cell.

New features
Expand Down Expand Up @@ -163,6 +164,7 @@ New features
* **`vertical-align: middle`, `top`, `bottom`, `text-top` and `text-bottom` on inline text**, in standard mode. They worked on images only, and left text on the baseline. Now `middle` puts the middle of the element's line box half the x-height of the block's font above the baseline, `text-top` and `text-bottom` line its top or bottom up with the top or bottom of the block's font, and `top` and `bottom` with the top or bottom of the line, which grows to hold it. The text, images, background, border and decorations inside the element move with it. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to leave text on the baseline as before. [#608]
* Each element is aligned against the block's font, not against the inline element it is in, and an image inside an element aligned with `top` or `bottom` is aligned with the line on its own.
* **`text-indent: hanging` and `each-line`**, in standard mode. `hanging` indents every line but the first, and `each-line` indents the first line after each `<br>` too. A value with either keyword was read as no indent before. [#608]
* **`word-break`, `overflow-wrap` and `word-wrap`**, in standard mode, in blocks and table cells. Both are inherited, and `word-wrap` is read as `overflow-wrap`. `word-break: break-all` breaks a line where it is full, inside a word or not. `keep-all` stops CJK text breaking between its letters, so it breaks at spaces and punctuation only. `overflow-wrap: break-word` and `anywhere`, and `word-break: break-word`, cut a word too long for its line at the edge. In a table cell `break-all`, `anywhere` and `word-break: break-word` let the column be as narrow as its widest letter, so `td { word-break: break-all }` keeps a 30mm table 30mm wide around a long URL, where the table used to widen to hold it. [#607]

Performance
-----------
Expand Down Expand Up @@ -970,4 +972,5 @@ 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
[#607]: https://github.com/GravityPDF/mpdf/issues/607
[#608]: https://github.com/GravityPDF/mpdf/issues/608
2 changes: 2 additions & 0 deletions src/Css/CssWideKeywords.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ final class CssWideKeywords
'MARGIN-TOP' => '0',
'OPACITY' => '1',
'OVERFLOW' => 'visible',
'OVERFLOW-WRAP' => 'normal',
'PADDING-BOTTOM' => '0',
'PADDING-LEFT' => '0',
'PADDING-RIGHT' => '0',
Expand All @@ -131,6 +132,7 @@ final class CssWideKeywords
'VERTICAL-ALIGN' => 'baseline',
'VISIBILITY' => 'visible',
'WHITE-SPACE' => 'normal',
'WORD-BREAK' => 'normal',
'WORD-SPACING' => 'normal',
'Z-INDEX' => 'auto',
];
Expand Down
12 changes: 10 additions & 2 deletions src/Css/InheritedProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ final class InheritedProperties
'LIST-STYLE-POSITION',
];

/**
* Held in the text state like TEXT, but read only under CssMode::STANDARD, so no legacy channel hands them on
*/
const WORD_BREAK = [
'WORD-BREAK',
'OVERFLOW-WRAP',
];

/**
* 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 +98,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 WORD_BREAK
*/
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::WORD_BREAK);
}

return $names;
Expand Down
23 changes: 23 additions & 0 deletions src/Css/NormalizeProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class NormalizeProperties
'BREAK-BEFORE' => ['PAGE-BREAK-BEFORE'],
'BREAK-AFTER' => ['PAGE-BREAK-AFTER'],
'BREAK-INSIDE' => ['PAGE-BREAK-INSIDE'],
'WORD-WRAP' => ['OVERFLOW-WRAP'],
];

public function __construct(Mpdf $mpdf, SizeConverter $sizeConverter, ColorConverter $colorConverter)
Expand Down Expand Up @@ -234,6 +235,8 @@ public function normalize($prop)
$this->processPageSizeProperty($k, $v);
} elseif ($k === 'BREAK-BEFORE' || $k === 'BREAK-AFTER' || $k === 'BREAK-INSIDE') {
$this->processBreakProperty($k, $v);
} elseif ($standard && ($k === 'WORD-BREAK' || $k === 'OVERFLOW-WRAP' || $k === 'WORD-WRAP')) {
$this->processWordBreakProperty($k, $v);
} elseif (in_array($k, ['BACKGROUND', 'BACKGROUND-IMAGE', 'BACKGROUND-REPEAT', 'BACKGROUND-POSITION'], true)) {
$this->processBackgroundProperty($k, $v);
} elseif ($k === 'IMAGE-ORIENTATION') {
Expand Down Expand Up @@ -1293,6 +1296,26 @@ private function processBreakProperty($property, $value)
}
}

/**
* Keeps word-break and overflow-wrap when the value is one they take, as a browser drops an invalid declaration.
* word-wrap is the old name of overflow-wrap
*
* @param string $property WORD-BREAK, OVERFLOW-WRAP or WORD-WRAP
* @param string $value
* @return void
*/
private function processWordBreakProperty($property, $value)
{
if ($property === 'WORD-WRAP') {
$property = 'OVERFLOW-WRAP';
}

$value = trim($value);
if (WordBreak::isValid($property, $value)) {
$this->properties[$property] = $value;
}
}

/**
* Process image orientation CSS properties.
*
Expand Down
122 changes: 122 additions & 0 deletions src/Css/WordBreak.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

namespace Mpdf\Css;

/**
* Where word-break and overflow-wrap let a line break, read from the text parameters Mpdf::setCSS() keeps them in
* under CssMode::STANDARD. word-wrap is read as overflow-wrap.
*
* When no element sets either property, a word too long for its line is cut at the edge, as mPDF has always done: on
* paper an overflowing word is lost. Once one of them is set, a word overflows unless they let it break, as in a
* browser.
*/
final class WordBreak
{

/**
* CJK punctuation, as a pattern for preg_match(). Under word-break: keep-all a line of CJK text breaks only beside
* one of these, or at a space
*/
const CJK_PUNCTUATION = '/[\x{3001}-\x{3003}\x{3008}-\x{3011}\x{3014}-\x{301f}\x{fe10}-\x{fe19}\x{fe30}-\x{fe6b}\x{ff01}-\x{ff0f}\x{ff1a}-\x{ff20}\x{ff3b}-\x{ff40}\x{ff5b}-\x{ff65}]/u';

/**
* The values each property takes, as NormalizeProperties keeps them
*/
const VALUES = [
'WORD-BREAK' => ['normal', 'break-all', 'keep-all', 'break-word'],
'OVERFLOW-WRAP' => ['normal', 'break-word', 'anywhere'],
];

/**
* Whether a value is one the property takes
*
* @param string $property WORD-BREAK or OVERFLOW-WRAP
* @param string $value Lowercased
*
* @return bool
*/
public static function isValid($property, $value)
{
return in_array($value, self::VALUES[$property], true);
}

/**
* Whether a line may break between any two letters, so it is filled to its edge (word-break: break-all)
*
* @param array|null $textparam
*
* @return bool
*/
public static function breaksAll($textparam)
{
return self::wordBreak($textparam) === 'break-all';
}

/**
* Whether CJK text keeps its letters together, breaking only at spaces and punctuation (word-break: keep-all)
*
* @param array|null $textparam
*
* @return bool
*/
public static function keepsAll($textparam)
{
return self::wordBreak($textparam) === 'keep-all';
}

/**
* Whether a word too long for its line is broken at the edge of the line rather than overflowing it
*
* @param array|null $textparam
*
* @return bool
*/
public static function breaksLongWords($textparam)
{
if (!isset($textparam['word-break']) && !isset($textparam['overflow-wrap'])) {
return true;
}

return self::narrowsMinContent($textparam) || self::overflowWrap($textparam) === 'break-word';
}

/**
* Whether a table cell's narrowest width is taken from its widest letter rather than its widest word. Unlike
* overflow-wrap: break-word, anywhere and break-all count the breaks they allow in the narrowest width, and
* word-break: break-word is read as overflow-wrap: anywhere
*
* @param array|null $textparam
*
* @return bool
*/
public static function narrowsMinContent($textparam)
{
$wordBreak = self::wordBreak($textparam);

return $wordBreak === 'break-all' || $wordBreak === 'break-word' || self::overflowWrap($textparam) === 'anywhere';
}

/**
* The word-break the text is set in, normal when no element sets it
*
* @param array|null $textparam
*
* @return string
*/
private static function wordBreak($textparam)
{
return isset($textparam['word-break']) ? $textparam['word-break'] : 'normal';
}

/**
* The overflow-wrap the text is set in, normal when no element sets it
*
* @param array|null $textparam
*
* @return string
*/
private static function overflowWrap($textparam)
{
return isset($textparam['overflow-wrap']) ? $textparam['overflow-wrap'] : 'normal';
}
}
Loading
Loading