diff --git a/src/Mpdf.php b/src/Mpdf.php index 4ab0d0f7d..f82e3c5a0 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -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']) ) ) @@ -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']); @@ -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']; @@ -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) { @@ -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 = []; } @@ -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; @@ -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; @@ -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; @@ -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; @@ -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. $this->subPos = $writehtml_i + 3; return 4; @@ -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; @@ -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; @@ -27323,7 +27328,9 @@ function AdjustHTML($html, $tabSpaces = 8) $html = str_replace("]*)><\/textarea>/si', ' ', $html); - $html = preg_replace('/(]*>)\s*()(.*?<\/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('/(]*>)\s*()(.*?<\/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*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'])) { @@ -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; @@ -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; @@ -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; @@ -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. @@ -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'], @@ -1374,31 +1332,16 @@ 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() @@ -1406,12 +1349,6 @@ public function close(&$ahtml, &$ihtml) 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 { @@ -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; + } + } + } diff --git a/tests/Mpdf/PageBreakInsideAvoidFloatTest.php b/tests/Mpdf/PageBreakInsideAvoidFloatTest.php new file mode 100644 index 000000000..f4e1aad2f --- /dev/null +++ b/tests/Mpdf/PageBreakInsideAvoidFloatTest.php @@ -0,0 +1,289 @@ +' . $inner . '

Tail

'; + } + + private function floatingText($side, $lines) + { + return '
' . str_repeat('

Float

', $lines) . '
'; + } + + private function floatingImage($side) + { + return ''; + } + + private function floatedKept($lines, $side = 'left', $style = '', $inner = '') + { + return '
' . $inner . str_repeat('

Kept

', $lines) . '
'; + } + + private function beside($lines) + { + return str_repeat('

Beside

', $lines) . '
'; + } + + private function besideAndTail() + { + return $this->beside(2) . '

Tail

'; + } + + private function assertMovedWhole(array $pages, array $onPage2) + { + $this->assertCount(2, $pages); + foreach ($onPage2 as $text => $count) { + $this->assertTextCount(0, $text, $pages[0]); + $this->assertTextCount($count, $text, $pages[1]); + } + } + + public function testAFloatInsideTheBlockIsLaidOutOnce() + { + $html = $this->filler(5) . $this->kept('

Signed

' . $this->floatingText('left', 1) . $this->floatingText('left', 1)); + + $pages = $this->pages($this->render($html)); + + $this->assertCount(1, $pages); + foreach (['Filler' => 5, 'Signed' => 1, 'Float' => 2, 'Tail' => 1] as $text => $count) { + $this->assertTextCount($count, $text, $pages[0]); + } + } + + public function testAFloatInsideTheBlockMovesWithIt() + { + $html = $this->filler(22) . $this->kept('

Head

' . $this->floatingText('left', 1) . $this->floatingText('right', 1) . str_repeat('

Kept

', 10)); + + $this->assertMovedWhole($this->pages($this->render($html)), ['Head' => 1, 'Float' => 2, 'Kept' => 10, 'Tail' => 1]); + } + + /** + * Text beside the float keeps its indent after the move; the float's end position on the enclosing + * blocks is part of what is rolled back + */ + public function testTextStillWrapsBesideTheFloatAfterTheMove() + { + $html = $this->filler(22) . $this->kept($this->floatingText('left', 3) . $this->beside(3) . str_repeat('

Kept

', 8)); + + $pages = $this->pages($this->render($html)); + + $this->assertCount(2, $pages); + preg_match('/([\d.]+) [\d.]+ Td \(Beside\)/', $pages[1], $beside); + preg_match('/([\d.]+) [\d.]+ Td \(Kept\)/', $pages[1], $kept); + $this->assertGreaterThan((float) $kept[1], (float) $beside[1], 'Text beside the float should be indented past it'); + } + + /** + * The float alone does not fit on what is left of the page, so the whole block moves + */ + public function testAFloatTallerThanTheSpaceLeftMovesWithTheBlock() + { + $html = $this->filler(22) . $this->kept($this->floatingText('left', 20) . $this->beside(5) . '

Kept

'); + + $this->assertMovedWhole($this->pages($this->render($html)), ['Float' => 20, 'Beside' => 5, 'Kept' => 1, 'Tail' => 1]); + } + + /** + * A float taller than a page makes the block taller than a page, so it cannot be kept together and + * breaks like any other block. Nothing is written twice on the way + */ + public function testABlockAFloatMakesTallerThanAPageIsLaidOutOnce() + { + $html = $this->filler(5) . $this->kept($this->floatingText('left', 60) . $this->beside(5) . '

Kept

'); + + $pages = $this->pages($this->render($html)); + $all = implode('', $pages); + + $this->assertGreaterThan(1, count($pages)); + $this->assertTextCount(5, 'Filler', $pages[0]); + foreach (['Float' => 60, 'Beside' => 5, 'Kept' => 1, 'Tail' => 1] as $text => $count) { + $this->assertTextCount($count, $text, $all); + } + } + + /** + * Floated images go through their own buffer, which is rolled back too + */ + public function testFloatedImagesMoveWithTheBlock() + { + $html = $this->filler(22) . $this->kept($this->floatingImage('left') . $this->floatingImage('right') . $this->beside(8) . str_repeat('

Kept

', 4)); + + $pages = $this->pages($this->render($html)); + + $this->assertMovedWhole($pages, ['Beside' => 8, 'Kept' => 4]); + $this->assertSame(0, $this->images($pages[0])); + $this->assertSame(2, $this->images($pages[1])); + } + + public function testAFloatedImageInABlockThatStaysPutIsDrawnOnce() + { + $html = $this->filler(5) . $this->kept($this->floatingImage('left') . $this->beside(8) . '

Kept

'); + + $pages = $this->pages($this->render($html)); + + $this->assertCount(1, $pages); + $this->assertSame(1, $this->images($pages[0])); + $this->assertTextCount(8, 'Beside', $pages[0]); + } + + /** + * A float still open when the block starts used to disappear with the measuring pass. Where mPDF puts + * it is the float engine's decision, so only that it is drawn once is asserted + */ + public function testAFloatOpenedBeforeTheBlockIsNotLost() + { + $html = $this->filler(28) . $this->floatingText('left', 6) . $this->kept(str_repeat('

Kept

', 10)); + + $pages = $this->pages($this->render($html)); + + $this->assertCount(2, $pages); + $this->assertTextCount(6, 'Float', implode('', $pages)); + $this->assertTextCount(10, 'Kept', $pages[1]); + } + + /** + * A floated block used to be excluded from keep-together and split at the foot of the page like any float. + * A float's close puts the position back to where it started, so a block being measured skips its own and + * the move is decided from where the float ended + */ + public function testAFloatedBlockIsKeptTogether() + { + $pages = $this->pages($this->render($this->filler(26) . $this->floatedKept(6) . $this->besideAndTail())); + + $this->assertMovedWhole($pages, ['Kept' => 6, 'Beside' => 2, 'Tail' => 1]); + $this->assertTextCount(26, 'Filler', $pages[0]); + } + + public function testAFloatedBlockThatFitsStaysPut() + { + $pages = $this->pages($this->render($this->filler(5) . $this->floatedKept(6) . $this->besideAndTail())); + + $this->assertCount(1, $pages); + foreach (['Kept' => 6, 'Beside' => 2, 'Tail' => 1] as $text => $count) { + $this->assertTextCount($count, $text, $pages[0]); + } + } + + public function testAFloatedBlockTallerThanAPageStillBreaks() + { + $pages = $this->pages($this->render($this->filler(26) . $this->floatedKept(40) . $this->besideAndTail())); + + $this->assertCount(3, $pages); + $this->assertGreaterThan(0, substr_count($pages[0], '(Kept)'), 'A block that fits on no page starts where it is'); + $this->assertTextCount(40, 'Kept', implode('', $pages)); + $this->assertTextCount(1, 'Tail', $pages[2]); + } + + public function testARightFloatedBlockIsKeptTogether() + { + $pages = $this->pages($this->render($this->filler(26) . $this->floatedKept(6, 'right') . $this->besideAndTail())); + + $this->assertMovedWhole($pages, ['Kept' => 6, 'Beside' => 2, 'Tail' => 1]); + } + + public function testLeftAndRightFloatedBlocksBothMove() + { + $right = str_replace('

Kept

', '

Right

', $this->floatedKept(6, 'right')); + + $pages = $this->pages($this->render($this->filler(26) . $this->floatedKept(6) . $right . $this->besideAndTail())); + + $this->assertMovedWhole($pages, ['Kept' => 6, 'Right' => 6, 'Beside' => 2, 'Tail' => 1]); + } + + /** + * Of two floated blocks, only the one that does not fit moves + */ + public function testAFloatedBlockThatFitsStaysWhileOneThatDoesNotMoves() + { + $right = str_replace('

Kept

', '

Right

', $this->floatedKept(12, 'right')); + + $pages = $this->pages($this->render($this->filler(22) . $this->floatedKept(3) . $right . $this->besideAndTail())); + + $this->assertCount(2, $pages); + $this->assertTextCount(3, 'Kept', $pages[0]); + $this->assertTextCount(0, 'Right', $pages[0]); + $this->assertTextCount(12, 'Right', $pages[1]); + $this->assertTextCount(1, 'Tail', $pages[1]); + } + + /** + * The border and background are painted where the block ends up, and only there + */ + public function testAFloatedBlockWithABorderAndBackgroundMovesWhole() + { + $html = $this->filler(26) . $this->floatedKept(6, 'left', ' border: 1mm solid #000; background-color: red; padding: 2mm') . $this->besideAndTail(); + + $pages = $this->pages($this->render($html)); + + $this->assertMovedWhole($pages, ['Kept' => 6, 'Tail' => 1]); + $this->assertSame(0, substr_count($pages[0], self::RED)); + $this->assertSame(1, substr_count($pages[1], self::RED)); + $this->assertDoesNotMatchRegularExpression('/^S$/m', $pages[0]); + $this->assertMatchesRegularExpression('/^S$/m', $pages[1]); + } + + public function testAFloatInsideAFloatedBlockMovesWithIt() + { + $inner = '
' . str_repeat('

Inner

', 3) . '
'; + + $pages = $this->pages($this->render($this->filler(26) . $this->floatedKept(6, 'left', '', $inner) . $this->besideAndTail())); + + $this->assertMovedWhole($pages, ['Inner' => 3, 'Kept' => 6, 'Beside' => 2, 'Tail' => 1]); + } + + public function testAnImageAndATableInsideAFloatedBlockMoveWithIt() + { + $inner = '
Cell
'; + + $pages = $this->pages($this->render($this->filler(26) . $this->floatedKept(4, 'left', '', $inner) . $this->besideAndTail())); + + $this->assertMovedWhole($pages, ['Cell' => 1, 'Kept' => 4, 'Tail' => 1]); + $this->assertSame(0, $this->images($pages[0])); + $this->assertSame(1, $this->images($pages[1])); + $this->assertSame(0, substr_count($pages[0], self::RED)); + $this->assertSame(1, substr_count($pages[1], self::RED)); + } + + /** + * The container's border runs to the foot of page 1 and continues on page 2 around the moved block + */ + public function testAFloatedBlockInsideABorderedContainerMovesWhole() + { + $html = '
' . $this->filler(25) . $this->floatedKept(6) . $this->besideAndTail() . '
'; + + $pages = $this->pages($this->render($html)); + + $this->assertMovedWhole($pages, ['Kept' => 6, 'Beside' => 2, 'Tail' => 1]); + $this->assertMatchesRegularExpression('/^S$/m', $pages[0]); + $this->assertMatchesRegularExpression('/^S$/m', $pages[1]); + } + + /** + * Keep-together is off inside columns, and a float there is unaffected + */ + public function testAFloatInsideTheBlockInsideColumnsIsLaidOutOnce() + { + $html = '' . $this->filler(22) . $this->kept($this->floatingText('left', 3) . $this->beside(3) . '

Kept

'); + + $pages = $this->pages($this->render($html)); + + $this->assertCount(1, $pages); + foreach (['Filler' => 22, 'Float' => 3, 'Beside' => 3, 'Kept' => 1, 'Tail' => 1] as $text => $count) { + $this->assertTextCount($count, $text, $pages[0]); + } + } +} diff --git a/tests/Mpdf/PageBreakInsideAvoidTest.php b/tests/Mpdf/PageBreakInsideAvoidTest.php new file mode 100644 index 000000000..625cab066 --- /dev/null +++ b/tests/Mpdf/PageBreakInsideAvoidTest.php @@ -0,0 +1,223 @@ +filler(22) . '
' . str_repeat('

Kept

', 12) . '
'; + } + + /** + * mpdf/mpdf#533: the footer is written when the measuring pass reaches the end of the starting page, and + * that page is the one the unwind kept, so the watermark on it was drawn again when the block was laid + * out for real + */ + public function testAWatermarkIsDrawnOncePerPage() + { + $mpdf = $this->mpdf(); + $mpdf->setWatermarkText('TESTING'); + $mpdf->showWatermarkText = true; + $mpdf->WriteHTML('

Outside

' . str_repeat('

Inside

', 100) . '
'); + + $pages = $this->pages($this->output($mpdf)); + + $this->assertCount(4, $pages); + foreach ($pages as $i => $page) { + $this->assertTextCount(1, 'TESTING', $page, 'Page ' . ($i + 1) . ' should carry one watermark'); + } + } + + /** + * mpdf/mpdf#1131: the measuring pass starts page 2, which selects the @page rules for it, and the + * unwind did not put page 1's back. So page 1 was finished with page 2's footer and background + */ + public function testTheFirstPageKeepsItsOwnPageRulesWhenABlockMovesOffIt() + { + $mpdf = $this->mpdf(); + $mpdf->WriteHTML(''); + $mpdf->WriteHTML('This is Footer 1'); + $mpdf->WriteHTML('This is Footer 2'); + $mpdf->WriteHTML('

Hello

' . str_repeat('

' . str_repeat('Lorem ipsum dolor sit amet. ', 12) . '

', 15)); + + $pages = $this->pages($this->output($mpdf)); + + $this->assertCount(2, $pages); + + $this->assertSame(1, substr_count($pages[0], self::RED)); + $this->assertSame(0, substr_count($pages[0], self::GREEN)); + $this->assertTextCount(1, 'This is Footer 1', $pages[0]); + $this->assertTextCount(0, 'This is Footer 2', $pages[0]); + + $this->assertSame(0, substr_count($pages[1], self::RED)); + $this->assertSame(1, substr_count($pages[1], self::GREEN)); + $this->assertTextCount(0, 'This is Footer 1', $pages[1]); + $this->assertTextCount(1, 'This is Footer 2', $pages[1]); + } + + /** + * mpdf/mpdf#1805: the measuring pass painted the @page background onto the starting page a second time, + * on top of the block backgrounds already collected for it, so every page but the last lost them + */ + public function testAPageBackgroundIsPaintedOnceAndUnderTheBlockBackgrounds() + { + $html = ''; + for ($i = 1; $i <= 14; $i++) { + $html .= '
Card ' . $i . '

' . str_repeat('Body text. ', 60) . '

'; + } + + $pages = $this->pages($this->render($html)); + + $this->assertCount(3, $pages); + foreach ($pages as $i => $page) { + $this->assertSame(1, substr_count($page, self::WHITE), 'Page ' . ($i + 1) . ' should paint its background once'); + $this->assertGreaterThan(strrpos($page, self::WHITE), strpos($page, self::RED), 'Page ' . ($i + 1) . ' should paint the card heads over the page background'); + } + } + + /** + * The backgrounds collected for the starting page before the block are flushed onto it by the measuring + * pass. Putting the page back as it was has to put them back too, or they would never be painted + */ + public function testABackgroundCollectedBeforeTheBlockIsStillPainted() + { + $pages = $this->pages($this->render('
Red box
' . $this->movingBlock())); + + $this->assertCount(2, $pages); + $this->assertSame(1, substr_count($pages[0], self::RED)); + $this->assertTextCount(0, 'Kept', $pages[0]); + $this->assertTextCount(12, 'Kept', $pages[1]); + } + + /** + * The border of a block enclosing the moved one is stroked down to the foot of the starting page during + * the measuring pass, and the block remembers having done so. Putting the page back as it was without + * also putting the block back would leave page 1 with no border at all + */ + public function testAnEnclosingBorderIsPaintedOnTheFirstPageAfterTheBlockMoves() + { + $html = '
' . $this->movingBlock() . '

Tail

'; + + $pages = $this->pages($this->render($html)); + + $this->assertCount(2, $pages); + $this->assertMatchesRegularExpression('/^S$/m', $pages[0], 'Page 1 should carry the border down to its foot'); + $this->assertMatchesRegularExpression('/^S$/m', $pages[1]); + } + + /** + * mpdf/mpdf#1131 (comments): a header, footer or fixed-position block is buffered, not discarded, while + * being measured, so a kept-together paragraph inside one was written twice + */ + public function testAKeptTogetherBlockInsideAFixedPositionBlockIsWrittenOnce() + { + $html = '

Hello

Body

'; + + $pages = $this->pages($this->render($html)); + + $this->assertCount(1, $pages); + $this->assertTextCount(1, 'Hello', $pages[0]); + } + + public function testAKeptTogetherBlockInsideAFooterIsWrittenOnce() + { + $mpdf = $this->mpdf(); + $mpdf->SetHTMLFooter('

Footer para

'); + $mpdf->WriteHTML('

Body

'); + + $pages = $this->pages($this->output($mpdf)); + + $this->assertCount(1, $pages); + $this->assertTextCount(1, 'Footer para', $pages[0]); + } + + /** + * mpdf/mpdf#2075: a substituted character is spliced into the token stream as a span, and the text + * before it trimmed from the current token. The trim was lost on the token itself, so the second pass + * printed the whole text and then the span and the rest of it again. Text in a TrueType font is written + * as UTF-16 + */ + public function testASubstitutedCharacterIsNotDuplicatedWhenTheBlockIsParsedAgain() + { + $html = '' . $this->filler(5) . '

HÄẞLICH

'; + + $pdf = $this->render($html, ['mode' => 'utf-8', 'useSubstitutions' => true, 'backupSubsFont' => ['dejavusans']]); + + foreach (['HÄ' => 'before', 'LICH' => 'after'] as $text => $side) { + $this->assertSame(1, substr_count($pdf, mb_convert_encoding($text, 'UTF-16BE', 'UTF-8')), "The letters $side the substituted one should be printed once"); + } + } + + /** + * mpdf/mpdf#1666: a caption is written as a block above its table, so when a table with + * page-break-inside:avoid moved to the next page the caption stayed behind. With keep-with-table on, a + * caption is now carried the same way as a heading + */ + public function testACaptionMovesWithItsKeptTogetherTableWhenKeepWithTableIsOn() + { + $table = '
Table Caption
Cell' . str_repeat('
', 20) . 'Bottom
'; + + foreach ([true, false] as $useKwt) { + $mpdf = $this->mpdf(); + $mpdf->use_kwt = $useKwt; + $mpdf->WriteHTML($this->filler(27) . $table); + + $pages = $this->pages($this->output($mpdf)); + + $this->assertCount(2, $pages); + $this->assertTextCount(1, 'Cell', $pages[1]); + $this->assertTextCount($useKwt ? 0 : 1, 'Table Caption', $pages[0]); + $this->assertTextCount($useKwt ? 1 : 0, 'Table Caption', $pages[1]); + } + } + + /** + * mpdf/mpdf#1801: page-break-after:avoid asks for room for one more line as tall as the block after it. + * A tall image could never have that, on any page, and each check pushed it on to yet another one + */ + public function testATallBlockWithPageBreakAfterAvoidDoesNotPushBlankPages() + { + $html = '

Before

After

'; + + $pages = $this->pages($this->render($html)); + + $this->assertCount(1, $pages); + $this->assertSame(1, $this->images($pages[0])); + $this->assertTextCount(1, 'Before', $pages[0]); + $this->assertTextCount(1, 'After', $pages[0]); + } + + /** + * The look-ahead still applies when a fresh page could satisfy it: a heading with only its own height + * left on the page moves to the next one + */ + public function testAHeadingWithPageBreakAfterAvoidStillMovesToKeepItsNextLine() + { + $html = $this->filler(38) . '

Heading

After

'; + + $pages = $this->pages($this->render($html)); + + $this->assertCount(2, $pages); + $this->assertTextCount(0, 'Heading', $pages[0]); + $this->assertTextCount(1, 'Heading', $pages[1]); + $this->assertTextCount(1, 'After', $pages[1]); + } +} diff --git a/tests/Mpdf/PageStreams.php b/tests/Mpdf/PageStreams.php new file mode 100644 index 000000000..2923e8bac --- /dev/null +++ b/tests/Mpdf/PageStreams.php @@ -0,0 +1,71 @@ + 'c']); + $mpdf->compress = false; + + return $mpdf; + } + + private function render($html, $config = []) + { + $mpdf = $this->mpdf($config); + $mpdf->WriteHTML($html); + + return $this->output($mpdf); + } + + private function output(Mpdf $mpdf) + { + $pdf = $mpdf->Output('', 'S'); + $mpdf->cleanup(); + + return $pdf; + } + + /** + * The content stream of each page, in order + */ + private function pages($pdf) + { + preg_match_all('/\d+ 0 obj\s*<<\/Length \d+>>\s*stream\n(.*?)\nendstream/s', $pdf, $matches); + + return $matches[1]; + } + + /** + * A page takes about twenty-nine of these, so twenty-two leave room for a few more but not for a block + */ + private function filler($paragraphs) + { + return str_repeat('

Filler

', $paragraphs); + } + + /** + * A 5x5 PNG, for an image whose size comes from its style + */ + private function pngImage() + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; + } + + private function images($stream) + { + return preg_match_all('/\/I\d+ Do/', $stream); + } + + private function assertTextCount($expected, $text, $stream, $message = '') + { + $this->assertSame($expected, substr_count($stream, '(' . $text . ')'), $message ?: "'$text' should appear $expected time(s)"); + } +} diff --git a/tests/Mpdf/TableBackgroundTest.php b/tests/Mpdf/TableBackgroundTest.php new file mode 100644 index 000000000..536902906 --- /dev/null +++ b/tests/Mpdf/TableBackgroundTest.php @@ -0,0 +1,106 @@ +Before

', $before) + . '
Normal table cell
' + . '
' + . '
Red table cell
' + . str_repeat('

After

', $after) + . '
'; + } + + /** + * The measuring pass of a page-break-inside:avoid block used to paint the table background on the page + * the block started on, and leave it there when the block moved. See mpdf/mpdf#570. + */ + public function testATableBackgroundInsideAnAvoidBlockThatMovesIsDrawnOnceOnThePageItMovedTo() + { + $pdf = $this->render($this->document(20, 10)); + $pages = $this->pages($pdf); + + $this->assertCount(1, $this->redRectangles($pdf)); + $this->assertCount(2, $pages); + $this->assertStringNotContainsString(self::RED, $pages[0]); + $this->assertStringContainsString(self::RED, $pages[1]); + $this->assertStringContainsString('Red table cell', $pages[1]); + } + + /** + * The block fits where it is, so there is no move to leave anything behind - the background + * still has to be drawn + */ + public function testATableBackgroundInsideAnAvoidBlockThatStaysPutIsStillDrawn() + { + $this->assertCount(1, $this->redRectangles($this->render($this->document(2, 2)))); + } + + public function testATableBackgroundOutsideAnyAvoidBlockIsUnaffected() + { + $html = str_repeat('

Before

', 20) + . '
Red table cell
'; + + $this->assertCount(1, $this->redRectangles($this->render($html))); + } + + /** + * Each table writes a placeholder for its backgrounds to be spliced in behind. A table with no + * background of its own left the placeholder in the page, and the next table's backgrounds went + * behind that one as well as its own. + */ + public function testTablesWithNoBackgroundDoNotLeaveTheNextOnePaintedAgainForEachOfThem() + { + $html = str_repeat('
Plain table cell
', 5) + . '
Red table cell
'; + + $this->assertCount(1, $this->redRectangles($this->render($html))); + } + + /** + * Painting the same rectangle twice is invisible while it is opaque; a translucent one comes out + * darker than it was asked to be + */ + public function testATranslucentBackgroundIsNotDarkenedByBeingPaintedTwice() + { + $table = '
Translucent table cell
'; + + $alone = $this->render($table); + $preceded = $this->render('
Plain table cell
' . $table); + + $this->assertSame( + preg_match_all('/ re f/', $alone), + preg_match_all('/ re f/', $preceded) + ); + } + + /** + * The table with the background comes first here, so its placeholder was always taken out before + * the plain table wrote one - this way round has never been wrong + */ + public function testATableWithNoBackgroundAfterAColouredOneIsStillUnaffected() + { + $html = '
Red table cell
' + . '
Plain table cell
'; + + $this->assertCount(1, $this->redRectangles($this->render($html))); + } + +} diff --git a/tests/Snapshots/PageBreakAvoidFloatsSnapshotTest.php b/tests/Snapshots/PageBreakAvoidFloatsSnapshotTest.php new file mode 100644 index 000000000..d2912708d --- /dev/null +++ b/tests/Snapshots/PageBreakAvoidFloatsSnapshotTest.php @@ -0,0 +1,150 @@ + + + +

mPDF

+

Floats inside kept-together blocks

+ +

Each dashed block below is kept together. The first has a floated column of text beside wrapped + text and does not fit on this page, so it moves whole; the wrapped text keeps its indent on the + next page.

+ + +

Filler line , here to push the first block over the page boundary.

+ + +
+
+ +

Floated column, line .

+ +
+ +

Wrapped text beside the floated column, line . It starts to the right of the + column and returns to the margin once the column ends.

+ +
+

After the column has been cleared, inside the same block.

+
+ + +

Filler line , before the block with the pictures.

+ + +
+ + + +

Text between two floated pictures, line . The block moves whole to the next + page and both pictures move with it, drawn once each.

+ +
+
+ +
+

A column floated before the next block opens.

+

It used to vanish with the measuring pass.

+
+ +
+ +

A block that opens while the column above is still floating, line .

+ +
+
+ +
+
+ +

A column taller than a page, line .

+ +
+

This block cannot be kept together because its floated column is taller than a page, so it + breaks like any other block. Nothing in it is written twice.

+
+

The end of the tall block.

+
+ + +

Filler line , before the floated block that is itself kept together.

+ + +
+ +

A floated block kept together, line .

+ +
+

Text beside a floated block that does not fit on the page it starts on, so the block moves whole and + this text moves with it.

+
+ + +

Filler line , before the pair of kept floated blocks.

+ + +
+ +

A kept floated block with a border, a background and a picture, line 1.

+

Line 2.

+

Line 3.

+
+

Line 4, after the picture.

+
+
+

A float inside.

+

A kept floated block on the right, with a float of its own, line 1.

+

Line 2.

+

Line 3.

+
+

Line 4, after the inner float.

+
+

Text between two kept floated blocks that did not fit on the page they started on; both moved whole, + with everything in them, and this text moved with them.

+
+ +

After every block.

+ mpdf = new \Mpdf\Mpdf(); + $this->mpdf->SetBasePath(__DIR__ . '/../data'); + $this->mpdf->WriteHTML($html); + } +} diff --git a/tests/Snapshots/PageBreakAvoidStateSnapshotTest.php b/tests/Snapshots/PageBreakAvoidStateSnapshotTest.php new file mode 100644 index 000000000..2b0d37ddf --- /dev/null +++ b/tests/Snapshots/PageBreakAvoidStateSnapshotTest.php @@ -0,0 +1,119 @@ + + + + First page footer + Page {PAGENO} footer + +

mPDF

+

What a measured block must not leave behind

+ +

Every card below is kept together. The first page has its own background colour and footer, and + one watermark, all of which stay as they are when the cards that do not fit here move on. The + coloured panels in the last card move with it, and the plain table in front of the cards, which is + what leaves a place on this page for table backgrounds to land, gets nothing behind it.

+ + + +
A table before the cards, with no background of its own
+ + +
+
Card
+ +
Red panel, inside the last card
+
Blue panel, inside the last card
+ + +

Card text, line . Enough of it that the last cards have to move to page 2 whole.

+ +
+ + +
+

A framed section. Its border runs to the foot of this page and on from the top of the next, + around a signature block that is kept together and moves as one.

+ +

Framed text, line .

+ +
+

Signed on behalf of both parties:

+
Me
+
You
+
+ +

Signature block text, line .

+ +
+

After the signature block.

+
+ + +

Filler before the table, line .

+ + + + + + + + +
A caption kept with its table
The table does not fit on this page, so it moves. With keep-with-table on, the caption moves with it.
Row
+ +

After the table.

+ mpdf = new \Mpdf\Mpdf(); + $this->mpdf->use_kwt = true; + $this->mpdf->setWatermarkText('mPDF'); + $this->mpdf->showWatermarkText = true; + $this->mpdf->WriteHTML($html); + } +} diff --git a/tests/Snapshots/TableBackgroundSnapshotTest.php b/tests/Snapshots/TableBackgroundSnapshotTest.php new file mode 100644 index 000000000..3b72e212b --- /dev/null +++ b/tests/Snapshots/TableBackgroundSnapshotTest.php @@ -0,0 +1,71 @@ + + + +

mPDF

+

Table backgrounds

+ +

Each pair below is the same colour written twice. The second of each pair has a + table with no background of its own in front of it, which used to leave the pair's second + half painted on top of itself and so darker than the first.

+ + + +
Translucent red, with nothing in front of it
+ + + +
A table with no background of its own
+ + + +
Translucent red again, and the same shade as the first
+ + + +
Another table with no background of its own
+ + + +
Translucent blue, one table with no background behind it
+ + + +
A third table with no background of its own
+ + + +
A translucent cell rather than a translucent table
+ mpdf = new \Mpdf\Mpdf(); + $this->mpdf->WriteHTML($html); + } +} diff --git a/tests/data/snapshots/page-break-avoid-floats.pdf b/tests/data/snapshots/page-break-avoid-floats.pdf new file mode 100644 index 000000000..c6191b37b Binary files /dev/null and b/tests/data/snapshots/page-break-avoid-floats.pdf differ diff --git a/tests/data/snapshots/page-break-avoid-state.pdf b/tests/data/snapshots/page-break-avoid-state.pdf new file mode 100644 index 000000000..fb122dc17 Binary files /dev/null and b/tests/data/snapshots/page-break-avoid-state.pdf differ diff --git a/tests/data/snapshots/table-background.pdf b/tests/data/snapshots/table-background.pdf new file mode 100644 index 000000000..64ca7fac1 Binary files /dev/null and b/tests/data/snapshots/table-background.pdf differ diff --git a/tests/data/snapshots/table.pdf b/tests/data/snapshots/table.pdf index 9a14e7e07..bd4fb1596 100644 Binary files a/tests/data/snapshots/table.pdf and b/tests/data/snapshots/table.pdf differ