diff --git a/src/Mpdf.php b/src/Mpdf.php
index 280ca36db..73bf3177a 100644
--- a/src/Mpdf.php
+++ b/src/Mpdf.php
@@ -424,6 +424,14 @@ class Mpdf implements \Psr\Log\LoggerAwareInterface
var $floatbuffer;
var $floatmargins;
+ /**
+ * The top and bottom of each floated image drawn in a column since the columns were last laid out, by column,
+ * so balancing does not split a column beside one
+ *
+ * @var array[]
+ */
+ var $columnFloats;
+
var $bullet;
var $bulletarray;
@@ -1305,6 +1313,7 @@ public function __construct(array $config = [], $container = null)
$this->PageAnnots = [];
$this->PageNumSubstitutions = [];
$this->breakpoints = []; // used in columnbuffer
+ $this->columnFloats = [];
$this->tableLevel = 0;
$this->tbctr = []; // counter for nested tables at each level
$this->page_box = new PageBox();
@@ -17386,6 +17395,15 @@ function printbuffer($arrayaux, $blockstate = 0, $is_table = false, $table_draft
/* -- END CSS-IMAGE-FLOAT -- */
list($skipln) = $this->inlineObject($objattr['type'], '', $this->y, $objattr, $this->lMargin, ($this->flowingBlockAttr['contentWidth'] / Mpdf::SCALE), $maxWidth, $this->flowingBlockAttr['height'], false, $is_table);
+ /* -- COLUMNS -- */
+ // A floated image too tall or wide for an empty column is set in its line as before
+ if ($this->ColActive && isset($objattr['float'])) {
+ $columnHeight = $this->PageBreakTrigger - ($this->CurrCol < $this->NbCol - 1 ? $this->y0 : $this->tMargin);
+ if (abs($objattr['height']) > $columnHeight || $objattr['width'] > $this->blk[$this->blklvl]['inner_width']) {
+ unset($objattr['float']);
+ }
+ }
+ /* -- END COLUMNS -- */
// 1 -> New line needed because of width
// -1 -> Will fit width on line but NEW PAGE REQUIRED because of height
// -2 -> Will not fit on line therefore needs new line but thus NEW PAGE REQUIRED
@@ -17504,6 +17522,12 @@ function printbuffer($arrayaux, $blockstate = 0, $is_table = false, $table_draft
$objattr['OUTER-HEIGHT'] = $h;
$objattr['OUTER-X'] = $fx;
$objattr['OUTER-Y'] = $fy;
+ /* -- COLUMNS -- */
+ if ($this->ColActive) {
+ $objattr['column'] = $this->CurrCol;
+ $objattr['columnbuffer_from'] = count($this->columnbuffer);
+ }
+ /* -- END COLUMNS -- */
if ($objattr['float'] == 'R') {
// If R float already exists at this level
$this->floatmargins['R']['skipline'] = false;
@@ -25280,6 +25304,8 @@ function AcceptPageBreak()
} // *TABLES*
/* -- COLUMNS -- */
if ($this->ColActive == 1) {
+ // A floated image stays in the column it was placed in
+ $this->printfloatbuffer(); // *CSS-IMAGE-FLOAT*
if ($this->CurrCol < $this->NbCol - 1) {
// Go to the next column
$this->CurrCol++;
@@ -25326,6 +25352,10 @@ function AcceptPageBreak()
function SetColumns($NbCol, $vAlign = '', $gap = 5)
{
// NbCol = number of columns
+ // A floated image is drawn in its column before the columns are laid out
+ if ($this->ColActive) {
+ $this->printfloatbuffer(); // *CSS-IMAGE-FLOAT*
+ }
// Anything less than 2 turns columns off
if ($NbCol < 2) { // SET COLUMNS OFF
if ($this->ColActive) {
@@ -25348,6 +25378,7 @@ function SetColumns($NbCol, $vAlign = '', $gap = 5)
$this->col_BMoutlines = [];
$this->col_toc = [];
$this->breakpoints = [];
+ $this->columnFloats = [];
} else { // SET COLUMNS ON
if ($this->ColActive) {
$this->ColActive = 0;
@@ -25372,6 +25403,7 @@ function SetColumns($NbCol, $vAlign = '', $gap = 5)
$this->col_BMoutlines = [];
$this->col_toc = [];
$this->breakpoints = [];
+ $this->columnFloats = [];
if ((strtoupper($vAlign) == 'J') || (strtoupper($vAlign) == 'JUSTIFY')) {
$vAlign = 'J';
} else {
@@ -25428,6 +25460,8 @@ function AddColumn()
function NewColumn()
{
if ($this->ColActive == 1) {
+ // A floated image stays in the column it was placed in
+ $this->printfloatbuffer(); // *CSS-IMAGE-FLOAT*
if ($this->CurrCol < $this->NbCol - 1) {
// Go to the next column
$this->CurrCol++;
@@ -25504,6 +25538,9 @@ function printcolumnbuffer()
$breaks = [];
foreach ($this->breakpoints as $c => $bpa) {
foreach ($bpa as $rely) {
+ if ($this->besideColumnFloat($c, $rely)) {
+ continue;
+ }
$breaks[] = $rely + $this->ColDetails[$c]['add_y'] - $this->y0;
}
}
@@ -25925,6 +25962,7 @@ function printcolumnbuffer()
$this->col_BMoutlines = [];
$this->col_toc = [];
$this->breakpoints = [];
+ $this->columnFloats = [];
}
// mPDF 5.7+
@@ -26362,6 +26400,15 @@ function printkwtbuffer()
function printfloatbuffer()
{
if (count($this->floatbuffer)) {
+ /* -- COLUMNS -- */
+ if ($this->ColActive) {
+ $this->printColumnFloats();
+ $this->floatbuffer = [];
+ $this->floatmargins = [];
+
+ return;
+ }
+ /* -- END COLUMNS -- */
$this->objectbuffer = $this->floatbuffer;
$this->printobjectbuffer(false);
$this->objectbuffer = [];
@@ -26370,6 +26417,72 @@ function printfloatbuffer()
}
}
+ /* -- COLUMNS -- */
+ /**
+ * Draw the floated images into the column buffer, each where balancing the columns will keep it with the lines
+ * beside it
+ *
+ * A float is drawn after the lines that wrap around it, but balancing assigns columns in buffer order, so it is
+ * filed before the first line level with or below its top. The object buffer and current column are put back.
+ */
+ private function printColumnFloats()
+ {
+ $column = $this->CurrCol;
+ $objects = $this->objectbuffer;
+
+ foreach ($this->floatbuffer as $float) {
+ $this->CurrCol = isset($float['column']) ? $float['column'] : $column;
+ $top = $float['OUTER-Y'];
+ $bottom = $top + $float['OUTER-HEIGHT'];
+ $columnBottom = isset($this->ColDetails[$this->CurrCol]['bottom_margin']) ? $this->ColDetails[$this->CurrCol]['bottom_margin'] : 0;
+ $drawnFrom = count($this->columnbuffer);
+
+ $this->objectbuffer = [$float];
+ $this->printobjectbuffer(false);
+
+ $this->ColDetails[$this->CurrCol]['bottom_margin'] = max($columnBottom, $bottom);
+ $this->columnFloats[$this->CurrCol][] = [$top, $bottom];
+
+ // columnbuffer_from may sit before an earlier float's splice: the scan still finds the first level line
+ $drawn = array_splice($this->columnbuffer, $drawnFrom);
+ $at = count($this->columnbuffer);
+ for ($i = isset($float['columnbuffer_from']) ? $float['columnbuffer_from'] : $drawnFrom; $i < $drawnFrom; $i++) {
+ if ($this->columnbuffer[$i]['col'] == $this->CurrCol && $this->columnbuffer[$i]['y'] >= $top - 0.001) {
+ $at = $i;
+ break;
+ }
+ }
+ array_splice($this->columnbuffer, $at, 0, $drawn);
+ }
+
+ $this->CurrCol = $column;
+ $this->objectbuffer = $objects;
+ }
+
+ /**
+ * Whether a column break at this height would split the column beside a floated image
+ *
+ * @param int $column
+ * @param float $y
+ *
+ * @return bool
+ */
+ private function besideColumnFloat($column, $y)
+ {
+ if (empty($this->columnFloats[$column])) {
+ return false;
+ }
+
+ foreach ($this->columnFloats[$column] as $extent) {
+ if ($y > $extent[0] + 0.001 && $y < $extent[1] - 0.001) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ /* -- END COLUMNS -- */
+
function Circle($x, $y, $r, $style = 'S')
{
$this->Ellipse($x, $y, $r, $r, $style);
diff --git a/src/Tag/Img.php b/src/Tag/Img.php
index b2a02d89d..157bb1dd2 100644
--- a/src/Tag/Img.php
+++ b/src/Tag/Img.php
@@ -417,7 +417,7 @@ public function open($attr, &$ahtml, &$ihtml)
}
/* -- END BORDER-RADIUS -- */
/* -- CSS-IMAGE-FLOAT -- */
- if (!$this->mpdf->ColActive && !$this->mpdf->tableLevel && !$this->mpdf->listlvl && !$this->mpdf->kwt) {
+ if (!$this->mpdf->tableLevel && !$this->mpdf->listlvl && !$this->mpdf->kwt) {
if (isset($properties['FLOAT']) && (strtoupper($properties['FLOAT']) === 'RIGHT' || strtoupper($properties['FLOAT']) === 'LEFT')) {
$objattr['float'] = strtoupper(substr($properties['FLOAT'], 0, 1));
}
diff --git a/tests/Mpdf/FloatImageInColumnsTest.php b/tests/Mpdf/FloatImageInColumnsTest.php
new file mode 100644
index 000000000..ac47eaef0
--- /dev/null
+++ b/tests/Mpdf/FloatImageInColumnsTest.php
@@ -0,0 +1,236 @@
+ruler = $this->mpdf();
+ $this->ruler->SetFont('ctimes', '', 11);
+ }
+
+ /**
+ * Wherever balancing splits the columns, the lines level with the image in its column are set clear of it, and
+ * every other line, including those in the other column level with the image, has the column's full width
+ *
+ * @dataProvider layoutProvider
+ *
+ * @param string $side The side the image floats to, L or R
+ * @param string $dir The document's direction
+ * @param int $lead The words ahead of the image's paragraph
+ * @param string $columns The markup that starts the columns
+ * @param string $between Markup between the lead and the image's paragraph
+ * @param array $config
+ */
+ public function testLinesAreNarrowedOnlyBesideTheImage($side, $dir, $lead, $columns = '
' . str_repeat('alpha ', $lead) . '
' . $between + . ''
+ . str_repeat('beta ', 150) . '
' . str_repeat('gamma ', 150) . '
'); + $pages = $this->pageContents($this->output($mpdf)); + + $image = $this->image($pages[0]); + $lines = $this->lines($pages[0]); + $column = $this->columnOf($mpdf, $image['left'] + 1, $image['right'] - 1); + $this->assertNotNull($column, 'The image should lie within a column'); + + $beside = 0; + $levelInOtherColumn = 0; + foreach ($lines as $line) { + $lineColumn = $this->columnOf($mpdf, $line['x0'], $line['x1']); + $this->assertNotNull($lineColumn, 'Each line should lie within a column: ' . $line['text']); + + // A line is set beside the image when the top of its box is, which is some way above its baseline + $level = $line['baseline'] >= $image['top'] + self::LINE && $line['baseline'] <= $image['bottom']; + $edge = !$level && $line['baseline'] > $image['top'] - self::LINE && $line['baseline'] < $image['bottom'] + self::LINE; + if ($lineColumn === $column && $level) { + $beside++; + if ($side === 'L') { + $this->assertGreaterThanOrEqual($image['right'] - 0.01, $line['x0'], 'A line beside the image should start clear of it: ' . $line['text']); + } else { + $this->assertLessThanOrEqual($image['left'] + 0.01, $line['x1'], 'A line beside the image should end clear of it: ' . $line['text']); + } + continue; + } + + if ($lineColumn === $column && $edge) { + continue; + } + + if ($lineColumn !== $column && $level) { + $levelInOtherColumn++; + } + + if ($dir === 'rtl') { + $this->assertEqualsWithDelta($mpdf->ColR[$lineColumn] * Mpdf::SCALE, $line['x1'], 0.05, 'A line away from the image should end at its column\'s edge: ' . $line['text']); + } else { + $this->assertEqualsWithDelta($mpdf->ColL[$lineColumn] * Mpdf::SCALE, $line['x0'], 0.05, 'A line away from the image should start at its column\'s edge: ' . $line['text']); + } + } + + $this->assertGreaterThanOrEqual(4, $beside, 'The lines beside the image should have gone with it into its column'); + if ($between === '' && empty($config['keepColumns'])) { + $this->assertGreaterThan(0, $levelInOtherColumn, 'The other column should have lines level with the image'); + } + } + + /** + * The combinations of side, direction and column breaking, with the lead long enough to put the image near + * the top, the middle and the bottom of the first column, and past it + * + * @return array[] + */ + public function layoutProvider() + { + $cases = []; + foreach ([40, 120, 200, 260, 300, 340, 420] as $lead) { + $cases['left, ltr, balanced, lead ' . $lead] = ['L', 'ltr', $lead]; + $cases['right, rtl, balanced, lead ' . $lead] = ['R', 'rtl', $lead]; + $cases['right, ltr, balanced, lead ' . $lead] = ['R', 'ltr', $lead]; + $cases['left, ltr, vAlign J with no stretch, lead ' . $lead] = ['L', 'ltr', $lead, '' . str_repeat('alpha ', 100)
+ . '' . str_repeat('beta ', 100) . '
One line
' . str_repeat($lorem, 3) . '
+Mid-paragraph, ' . $lorem . '
' . str_repeat($lorem, 3) . '
' . str_repeat($lorem, 3) . '
' . str_repeat($lorem, 2) . '
+After the columns.
+ '); + } +} diff --git a/tests/data/snapshots/float-images-in-columns.pdf b/tests/data/snapshots/float-images-in-columns.pdf new file mode 100644 index 000000000..44432af48 Binary files /dev/null and b/tests/data/snapshots/float-images-in-columns.pdf differ