Skip to content
Draft
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
79 changes: 43 additions & 36 deletions src/Mpdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -4882,7 +4882,10 @@ function Cell($w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill =
($this->y + $this->divheight > $this->PageBreakTrigger)
|| ($this->y + $h > $this->PageBreakTrigger)
|| (
// page-break-after:avoid wants room for one more line this tall after this one. When a fresh page
// has no such room either, breaking only adds empty pages (mpdf/mpdf#1801)
$this->y + ($h * 2) + $bottom > $this->PageBreakTrigger
&& $this->tMargin + ($h * 2) + $bottom <= $this->PageBreakTrigger
&& (isset($this->blk[$this->blklvl]['page_break_after_avoid']) && $this->blk[$this->blklvl]['page_break_after_avoid'])
)
)
Expand Down Expand Up @@ -6751,7 +6754,8 @@ function finishFlowingBlock($endofblock = false, $next = '')

if ($this->blklvl > 0 && !$is_table) {
if ($endofblock && $blockstate > 1) {
if ($this->blk[$this->blklvl]['page_break_after_avoid']) {
// As in Cell(): only ask for the extra line if a fresh page could hold it (mpdf/mpdf#1801)
if ($this->blk[$this->blklvl]['page_break_after_avoid'] && $this->tMargin + $check_h + $stackHeight <= $this->PageBreakTrigger) {
$check_h += $stackHeight;
}
$check_h += ($this->blk[$this->blklvl]['padding_bottom'] + $this->blk[$this->blklvl]['border_bottom']['w']);
Expand Down Expand Up @@ -22066,6 +22070,27 @@ function _reverseTableDir(&$table)
}
}

/**
* Splice the backgrounds collected for a table in behind the placeholder it wrote to $buffer, and spend the
* placeholder whether or not anything was put behind it: one left in place takes a copy of the next table's
* backgrounds as well, so a table with no background of its own left the one after it painted twice
*/
function spendTableBackgrounds($buffer)
{
// PrintTableBackgrounds() registers patterns and gradients as it goes, so like Td and BlockTag it is skipped
// while a page-break-inside:avoid block is only being measured and its output thrown away
$s = $this->tableBackgrounds && !$this->keep_block_together ? "\n" . $this->PrintTableBackgrounds() . "\n" : '';
$placeholder = '___TABLE___BACKGROUNDS' . $this->uniqstr;

if ($buffer === 'pages') {
$this->pages[$this->page] = str_replace($placeholder, ' ' . $s, $this->pages[$this->page]);
} else {
$this->$buffer = str_replace($placeholder, ' ' . $s, $this->$buffer);
}

$this->tableBackgrounds = [];
}

function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $splitpg = 0, $rety = 0)
{
$level = $table['level'];
Expand Down Expand Up @@ -22551,17 +22576,7 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl
}

// $this->AcceptPageBreak() has moved tablebuffer to $this->pages content
if ($this->tableBackgrounds) {
$s = $this->PrintTableBackgrounds();
if ($this->bufferoutput) {
$this->headerbuffer = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', '\\1' . "\n" . $s . "\n", $this->headerbuffer);
$this->headerbuffer = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', " ", $this->headerbuffer);
} else {
$this->pages[$this->page] = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', '\\1' . "\n" . $s . "\n", $this->pages[$this->page]);
$this->pages[$this->page] = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', " ", $this->pages[$this->page]);
}
$this->tableBackgrounds = [];
}
$this->spendTableBackgrounds($this->bufferoutput ? 'headerbuffer' : 'pages');

if ($split) {
if ($i == 0 && $j == 0) {
Expand Down Expand Up @@ -23469,25 +23484,13 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl
/* -- END BACKGROUNDS -- */
}

if ($this->tableBackgrounds && $level == 1) {
$s = $this->PrintTableBackgrounds();
if ($level == 1) {
// The same routing as BaseWriter::write() gave the placeholder
if ($this->table_rotate && !$this->processingHeader && !$this->processingFooter) {
$this->tablebuffer = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', '\\1' . "\n" . $s . "\n", $this->tablebuffer);
if ($level == 1) {
$this->tablebuffer = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', " ", $this->tablebuffer);
}
} elseif ($this->bufferoutput) {
$this->headerbuffer = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', '\\1' . "\n" . $s . "\n", $this->headerbuffer);
if ($level == 1) {
$this->headerbuffer = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', " ", $this->headerbuffer);
}
$this->spendTableBackgrounds('tablebuffer');
} else {
$this->pages[$this->page] = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', '\\1' . "\n" . $s . "\n", $this->pages[$this->page]);
if ($level == 1) {
$this->pages[$this->page] = preg_replace('/(___TABLE___BACKGROUNDS' . $this->uniqstr . ')/', " ", $this->pages[$this->page]);
}
$this->spendTableBackgrounds($this->bufferoutput ? 'headerbuffer' : 'pages');
}
$this->tableBackgrounds = [];
}


Expand Down Expand Up @@ -25894,7 +25897,9 @@ function SubstituteCharsNonCore(&$writehtml_a, &$writehtml_i, &$writehtml_e)
} else {
if ($l == 0 && $bsfctr == (count($this->backupSubsFont) - 1)) { // Not found even in last backup font
$cont = mb_substr($writehtml_e, $start + 1);
$writehtml_e = mb_substr($writehtml_e, 0, $start + 1, 'UTF-8');
// Write the trim back into the token too, here and at every splice below, as SubstituteCharsSIP()
// does: a page-break-inside:avoid block's second parse otherwise prints the untrimmed text (mpdf/mpdf#2075)
$writehtml_a[$writehtml_i] = $writehtml_e = mb_substr($writehtml_e, 0, $start + 1, 'UTF-8');
array_splice($writehtml_a, $writehtml_i + 1, 0, ['', $cont]);
$this->subPos = $writehtml_i + 1;

Expand All @@ -25908,7 +25913,7 @@ function SubstituteCharsNonCore(&$writehtml_a, &$writehtml_i, &$writehtml_e)
if ($l > 0) {
$patt = mb_substr($writehtml_e, $start, $l, 'UTF-8');
if (preg_match("/(.*?)(" . preg_quote($patt, '/') . ")(.*)/u", $writehtml_e, $m)) {
$writehtml_e = $m[1];
$writehtml_a[$writehtml_i] = $writehtml_e = $m[1];
array_splice($writehtml_a, $writehtml_i + 1, 0, ['span style="font-family: ' . $font . '"', $m[2], '/span', $m[3]]);
$this->subPos = $writehtml_i + 3;

Expand Down Expand Up @@ -26015,7 +26020,7 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e)
if ($l > 0) {
$patt = mb_substr($writehtml_e, $start, $l);
if (preg_match("/(.*?)(" . preg_quote($patt, '/') . ")(.*)/u", $writehtml_e, $m)) {
$writehtml_e = $m[1];
$writehtml_a[$writehtml_i] = $writehtml_e = $m[1];
array_splice($writehtml_a, $writehtml_i + 1, 0, ['span style="font-family: ' . $font . '"', $m[2], '/span', $m[3]]);
$this->subPos = $writehtml_i + 3;
return 4;
Expand Down Expand Up @@ -26064,7 +26069,7 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e)
if ($l > 0) {
$patt = mb_substr($writehtml_e, $start, $l);
if (preg_match("/(.*?)(" . preg_quote($patt, '/') . ")(.*)/u", $writehtml_e, $m)) {
$writehtml_e = $m[1];
$writehtml_a[$writehtml_i] = $writehtml_e = $m[1];
array_splice($writehtml_a, $writehtml_i + 1, 0, ['span style="font-family: ' . $font . '"', $m[2], '/span', $m[3]]);
$this->subPos = $writehtml_i + 3;
return 4;
Expand Down Expand Up @@ -26118,7 +26123,7 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e)
if ($ftype == 'C') {
$patt = mb_substr($writehtml_e, $start, count($repl));
if (preg_match("/(.*?)(" . preg_quote($patt, '/') . ")(.*)/u", $writehtml_e, $m)) {
$writehtml_e = $m[1];
$writehtml_a[$writehtml_i] = $writehtml_e = $m[1];
array_splice($writehtml_a, $writehtml_i + 1, 0, [$font, implode('|', $repl), '/' . $font, $m[3]]); // e.g. <tts>
$this->subPos = $writehtml_i + 3;
return 4;
Expand Down Expand Up @@ -26168,7 +26173,7 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e)
} else {
if ($l == 0 && $bsfctr == (count($this->backupSubsFont) - 1)) { // Not found even in last backup font
$cont = mb_substr($writehtml_e, $start + 1);
$writehtml_e = mb_substr($writehtml_e, 0, $start + 1);
$writehtml_a[$writehtml_i] = $writehtml_e = mb_substr($writehtml_e, 0, $start + 1);
array_splice($writehtml_a, $writehtml_i + 1, 0, ['', $cont]);
$this->subPos = $writehtml_i + 1;
return 2;
Expand All @@ -26181,7 +26186,7 @@ function SubstituteCharsMB(&$writehtml_a, &$writehtml_i, &$writehtml_e)
if ($l > 0) {
$patt = mb_substr($writehtml_e, $start, $l);
if (preg_match("/(.*?)(" . preg_quote($patt, '/') . ")(.*)/u", $writehtml_e, $m)) {
$writehtml_e = $m[1];
$writehtml_a[$writehtml_i] = $writehtml_e = $m[1];
array_splice($writehtml_a, $writehtml_i + 1, 0, ['span style="font-family: ' . $font . '"', $m[2], '/span', $m[3]]);
$this->subPos = $writehtml_i + 3;
return 4;
Expand Down Expand Up @@ -27323,7 +27328,9 @@ function AdjustHTML($html, $tabSpaces = 8)
$html = str_replace("<innerpre", "<pre", $html);

$html = preg_replace('/<textarea([^>]*)><\/textarea>/si', '<textarea\\1> </textarea>', $html);
$html = preg_replace('/(<table[^>]*>)\s*(<caption)(.*?<\/caption>)(.*?<\/table>)/si', '\\2 position="top"\\3\\1\\4\\2 position="bottom"\\3', $html); // *TABLES*
// With use_kwt, a caption above its table is kept with it the same way as a heading (mpdf/mpdf#1666)
$kwt = $this->use_kwt ? ' keep-with-table="1"' : '';
$html = preg_replace('/(<table[^>]*>)\s*(<caption)(.*?<\/caption>)(.*?<\/table>)/si', '\\2 position="top"' . $kwt . '\\3\\1\\4\\2 position="bottom"\\3', $html); // *TABLES*

if ($this->use_kwt) {
$returnHtml = preg_replace('/<(h[1-6])([^>]*(?<!\/))(>[^>]*<\/\\1>\s*<table)/si', '<\\1\\2 keep-with-table="1"\\3', $html);
Expand Down
144 changes: 65 additions & 79 deletions src/Tag/BlockTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -478,13 +478,15 @@ public function open($attr, &$ahtml, &$ihtml)

$properties = $this->cssManager->MergeCSS('BLOCK', $tag, $attr); // mPDF 6 - moved to after page-break-before
// mPDF 6 page-break-inside:avoid
// Not while output is buffered (headers, footers, fixed-position blocks): that is not discarded while the
// block is measured, so it would be written twice
if (isset($properties['PAGE-BREAK-INSIDE']) && strtoupper($properties['PAGE-BREAK-INSIDE']) === 'AVOID'
&& !$this->mpdf->ColActive && !$this->mpdf->keep_block_together && !isset($attr['PAGEBREAKAVOIDCHECKED'])) {
&& !$this->mpdf->ColActive && !$this->mpdf->keep_block_together && !isset($attr['PAGEBREAKAVOIDCHECKED'])
&& !$this->mpdf->bufferoutput) {
// avoid re-iterating using PAGEBREAKAVOIDCHECKED; set in CloseTag
$currblk['keep_block_together'] = 1;
$currblk['array_i'] = $ihtml; // mPDF 6
$this->mpdf->kt_y00 = $this->mpdf->y;
$this->mpdf->kt_p00 = $this->mpdf->page;
$currblk['kt_state'] = $this->savePageState();
$this->mpdf->keep_block_together = 1;
}
if ($lastbottommargin && !empty($properties['MARGIN-TOP']) && empty($properties['FLOAT'])) {
Expand Down Expand Up @@ -584,12 +586,8 @@ public function open($attr, &$ahtml, &$ihtml)
}

/* -- CSS-FLOAT -- */
if (isset($properties['FLOAT']) && strtoupper($properties['FLOAT']) === 'RIGHT' && !$this->mpdf->ColActive) {

// Cancel Keep-Block-together
$currblk['keep_block_together'] = false;
$this->mpdf->kt_y00 = 0;
$this->mpdf->keep_block_together = 0;
$float = isset($properties['FLOAT']) ? strtoupper($properties['FLOAT']) : '';
if ($float === 'RIGHT' && !$this->mpdf->ColActive) {

$this->mpdf->blockContext++;
$currblk['blockContext'] = $this->mpdf->blockContext;
Expand Down Expand Up @@ -636,12 +634,7 @@ public function open($attr, &$ahtml, &$ihtml)
$currblk['float_width'] = ($currblk['css_set_width'] + $bdl + $pdl + $bdr + $pdr + $currblk['margin_right']);
}

} elseif (isset($properties['FLOAT']) && strtoupper($properties['FLOAT']) === 'LEFT' && !$this->mpdf->ColActive) {
// Cancel Keep-Block-together
$currblk['keep_block_together'] = false;
$this->mpdf->kt_y00 = 0;
$this->mpdf->keep_block_together = 0;

} elseif ($float === 'LEFT' && !$this->mpdf->ColActive) {
$this->mpdf->blockContext++;
$currblk['blockContext'] = $this->mpdf->blockContext;

Expand Down Expand Up @@ -937,10 +930,7 @@ public function open($attr, &$ahtml, &$ihtml)

// Save x,y coords in case we need to print borders...
$currblk['y0'] = $this->mpdf->y;
$currblk['initial_y0'] = $this->mpdf->y; // mPDF 6
$currblk['x0'] = $this->mpdf->x;
$currblk['initial_x0'] = $this->mpdf->x; // mPDF 6
$currblk['initial_startpage'] = $this->mpdf->page;
$currblk['startpage'] = $this->mpdf->page; // mPDF 6
$this->mpdf->oldy = $this->mpdf->y;

Expand Down Expand Up @@ -1284,42 +1274,10 @@ public function close(&$ahtml, &$ihtml)
}

/* -- CSS-FLOAT -- */
if ($this->mpdf->blk[$this->mpdf->blklvl]['float'] === 'R') {
// If width not set, here would need to adjust and output buffer
$s = $this->mpdf->PrintPageBackgrounds();
// Writes after the marker so not overwritten later by page background etc.
$this->mpdf->pages[$this->mpdf->page] = preg_replace('/(___BACKGROUND___PATTERNS' . $this->mpdf->uniqstr . ')/', '\\1' . "\n" . $s . "\n", $this->mpdf->pages[$this->mpdf->page]);
$this->mpdf->pageBackgrounds = [];
$this->mpdf->Reset();
$this->mpdf->pageoutput[$this->mpdf->page] = [];

for ($i = ($this->mpdf->blklvl - 1); $i >= 0; $i--) {
if (isset($this->mpdf->blk[$i]['float_endpos'])) {
$this->mpdf->blk[$i]['float_endpos'] = max($this->mpdf->blk[$i]['float_endpos'], $this->mpdf->page * 1000 + $this->mpdf->y);
} else {
$this->mpdf->blk[$i]['float_endpos'] = $this->mpdf->page * 1000 + $this->mpdf->y;
}
}

$this->mpdf->addFloatDiv([
'side' => 'R',
'startpage' => $this->mpdf->blk[$this->mpdf->blklvl]['startpage'],
'y0' => $this->mpdf->blk[$this->mpdf->blklvl]['float_start_y'],
'startpos' => $this->mpdf->blk[$this->mpdf->blklvl]['startpage'] * 1000 + $this->mpdf->blk[$this->mpdf->blklvl]['float_start_y'],
'endpage' => $this->mpdf->page,
'y1' => $this->mpdf->y,
'endpos' => $this->mpdf->page * 1000 + $this->mpdf->y,
'w' => $this->mpdf->blk[$this->mpdf->blklvl]['float_width'],
'blklvl' => $this->mpdf->blklvl,
'blockContext' => $this->mpdf->blk[$this->mpdf->blklvl - 1]['blockContext']
]);

$this->mpdf->y = $this->mpdf->blk[$this->mpdf->blklvl]['float_start_y'];
$this->mpdf->page = $this->mpdf->blk[$this->mpdf->blklvl]['startpage'];
$this->mpdf->ResetMargins();
$this->mpdf->pageoutput[$this->mpdf->page] = [];
}
if ($this->mpdf->blk[$this->mpdf->blklvl]['float'] === 'L') {
// A block measured for page-break-inside:avoid leaves nothing behind, so its own float close is skipped;
// that also leaves the position where the float ended for the check below
$float = $this->mpdf->blk[$this->mpdf->blklvl]['float'];
if (($float === 'R' || $float === 'L') && !$this->mpdf->blk[$this->mpdf->blklvl]['keep_block_together']) {
// If width not set, here would need to adjust and output buffer
$s = $this->mpdf->PrintPageBackgrounds();
// Writes after the marker so not overwritten later by page background etc.
Expand All @@ -1337,7 +1295,7 @@ public function close(&$ahtml, &$ihtml)
}

$this->mpdf->addFloatDiv([
'side' => 'L',
'side' => $float,
'startpage' => $this->mpdf->blk[$this->mpdf->blklvl]['startpage'],
'y0' => $this->mpdf->blk[$this->mpdf->blklvl]['float_start_y'],
'startpos' => $this->mpdf->blk[$this->mpdf->blklvl]['startpage'] * 1000 + $this->mpdf->blk[$this->mpdf->blklvl]['float_start_y'],
Expand Down Expand Up @@ -1374,44 +1332,23 @@ public function close(&$ahtml, &$ihtml)

// mPDF 6 page-break-inside:avoid
if ($this->mpdf->blk[$this->mpdf->blklvl]['keep_block_together']) {
$start = $this->mpdf->blk[$this->mpdf->blklvl]['kt_state']['props'];
$movepage = false;
// If page-break-inside:avoid section has broken to new page but fits on one side - then move:
if (($this->mpdf->page - $this->mpdf->kt_p00) == 1 && $this->mpdf->y < $this->mpdf->kt_y00) {
if (($this->mpdf->page - $start['page']) == 1 && $this->mpdf->y < $start['y']) {
$movepage = true;
}
if (($this->mpdf->page - $this->mpdf->kt_p00) > 0) {
for ($i = $this->mpdf->page; $i > $this->mpdf->kt_p00; $i--) {
unset($this->mpdf->pages[$i]);
if (isset($this->mpdf->blk[$this->mpdf->blklvl]['bb_painted'][$i])) {
unset($this->mpdf->blk[$this->mpdf->blklvl]['bb_painted'][$i]);
}
if (isset($this->mpdf->blk[$this->mpdf->blklvl]['marginCorrected'][$i])) {
unset($this->mpdf->blk[$this->mpdf->blklvl]['marginCorrected'][$i]);
}
if (isset($this->mpdf->pageoutput[$i])) {
unset($this->mpdf->pageoutput[$i]);
}
}
$this->mpdf->page = $this->mpdf->kt_p00;
}
$this->mpdf->keep_block_together = 0;
$this->restorePageState($this->mpdf->blk[$this->mpdf->blklvl]['kt_state']);
$this->mpdf->pageoutput[$this->mpdf->page] = [];

$this->mpdf->y = $this->mpdf->kt_y00;

$ihtml = $this->mpdf->blk[$this->mpdf->blklvl]['array_i'] - 1;

$ahtml[$ihtml + 1] .= ' pagebreakavoidchecked="true";'; // avoid re-iterating; read in OpenTag()

unset($this->mpdf->blk[$this->mpdf->blklvl]);
$this->mpdf->blklvl--;

for ($blklvl = 1; $blklvl <= $this->mpdf->blklvl; $blklvl++) {
$this->mpdf->blk[$blklvl]['y0'] = $this->mpdf->blk[$blklvl]['initial_y0'];
$this->mpdf->blk[$blklvl]['x0'] = $this->mpdf->blk[$blklvl]['initial_x0'];
$this->mpdf->blk[$blklvl]['startpage'] = $this->mpdf->blk[$blklvl]['initial_startpage'];
}

if (isset($this->mpdf->blk[$this->mpdf->blklvl]['x0'])) {
$this->mpdf->x = $this->mpdf->blk[$this->mpdf->blklvl]['x0'];
} else {
Expand Down Expand Up @@ -1491,4 +1428,53 @@ public function close(&$ahtml, &$ihtml)
}
}

/**
* What the measuring pass of a page-break-inside:avoid block has to put back when it is unwound; see
* mpdf/mpdf#533, #1131, #1805. A curated list rather than Mpdf::getStateSnapshot(): the second parse builds on
* what the measuring pass left behind, such as the list counter, which open() advances only on the first pass
* (tests/Issues/Issue339Test)
*/
private static $pageState = [
// Where the block started
'page', 'y',
// What _beginpage() selects for the next page
'HTMLHeader', 'HTMLHeaderE', 'HTMLFooter', 'HTMLFooterE',
'firstPageBoxHeader', 'firstPageBoxHeaderEven', 'firstPageBoxFooter', 'firstPageBoxFooterEven',
'page_box', 'show_marks', 'bodyBackgroundColor', 'bodyBackgroundGradient', 'bodyBackgroundImage',
'CurOrientation', 'DefOrientation', 'w', 'h', 'wPt', 'hPt', 'fw', 'fh', 'fwPt', 'fhPt',
'pgwidth', 'PageBreakTrigger', 'tMargin', 'bMargin', 'lMargin', 'rMargin', 'DeflMargin', 'DefrMargin',
'margin_header', 'margin_footer', 'MarginCorrection', 'y0',
// What AddPage() finishes the current page with (it switches the measuring discard off while it runs); the
// page-keyed arrays also carry the pages the measuring pass made, so restoring them discards those too
'pages', 'pageoutput', 'saveHTMLHeader', 'saveHTMLFooter', 'PageLinks', 'PageAnnots', 'pageDim',
'OrientationChanges', 'pageBackgrounds', 'patterns', 'PageNumSubstitutions',
// Float bookkeeping
'blockContext', 'floatmargins', 'floatbuffer', 'floatDivs',
];

private function savePageState()
{
$state = ['props' => [], 'blk' => []];

foreach (self::$pageState as $name) {
$state['props'][$name] = $this->mpdf->$name;
}

// Level by level: open() holds the level below by reference, and a copy of the whole array would share it
for ($i = 1; $i < $this->mpdf->blklvl; $i++) {
$state['blk'][$i] = $this->mpdf->blk[$i];
}

return $state;
}

private function restorePageState($state)
{
$this->mpdf->restoreStateSnapshot($state['props']);

foreach ($state['blk'] as $i => $blk) {
$this->mpdf->blk[$i] = $blk;
}
}

}
Loading
Loading