diff --git a/CHANGELOG-GRAVITYPDF.md b/CHANGELOG-GRAVITYPDF.md index a674b3d97..142789ea5 100644 --- a/CHANGELOG-GRAVITYPDF.md +++ b/CHANGELOG-GRAVITYPDF.md @@ -158,6 +158,7 @@ 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 ``, `` and `` 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] +* **`border-radius` on table cells, in standard mode.** In the separated borders model a cell's background, its row's background showing through it, a gradient or a background image are clipped to the cell's rounded border box, and its border follows the corners. A percentage is of the cell's own border box. `double` is drawn as two lines, and `groove`, `ridge`, `inset` and `outset` in the two tones a square cell takes. Under `border-collapse: collapse` the radius is ignored, as CSS says, and so it is in simple tables (`simpleTables`) and inside columns. A table's own `border-radius` is not drawn yet. Set `'cssMode' => \Mpdf\CssMode::LEGACY` to draw every cell square as before. [#552] [#603] Performance ----------- @@ -965,3 +966,4 @@ 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 +[#603]: https://github.com/GravityPDF/mpdf/issues/603 diff --git a/src/Mpdf.php b/src/Mpdf.php index b700f3353..d1d8f0485 100644 --- a/src/Mpdf.php +++ b/src/Mpdf.php @@ -3256,6 +3256,9 @@ function PrintTableBackgrounds($adjustmenty = 0) foreach ($this->tableBackgrounds as $bl => $pbs) { foreach ($pbs as $pb) { if ((!isset($pb['gradient']) || !$pb['gradient']) && (!isset($pb['image_id']) || !$pb['image_id'])) { + if (!empty($pb['clippath'])) { + $s .= $pb['clippath'] . "\n"; + } $s .= 'q ' . $this->SetFColor($pb['col'], true) . "\n"; if ($pb['col'][0] == 5) { // RGBa $s .= $this->SetAlpha(ord($pb['col'][4]) / 100, 'Normal', true, 'F') . "\n"; @@ -3263,6 +3266,9 @@ function PrintTableBackgrounds($adjustmenty = 0) $s .= $this->SetAlpha(ord($pb['col'][5]) / 100, 'Normal', true, 'F') . "\n"; } $s .= sprintf('%.3F %.3F %.3F %.3F re %s Q', $pb['x'] * Mpdf::SCALE, ($this->h - $pb['y']) * Mpdf::SCALE, $pb['w'] * Mpdf::SCALE, -$pb['h'] * Mpdf::SCALE, 'f') . "\n"; + if (!empty($pb['clippath'])) { + $s .= 'Q' . "\n"; + } } if (isset($pb['gradient']) && $pb['gradient']) { if (isset($pb['clippath']) && $pb['clippath']) { @@ -13239,6 +13245,7 @@ function TableHeaderFooter($content = '', $tablestartpage = '', $tablestartcolum $this->y += $h - $mih; } + $cellBox = $paintcell ? $this->roundedCellBox(Arrays::get($tablehf, 'border_radius', null), $table, $level, $x, $y, $w, $h, $border_details) : null; // TABLE ROW OR CELL FILL BGCOLOR $fill = 0; @@ -13345,7 +13352,9 @@ function TableHeaderFooter($content = '', $tablestartpage = '', $tablestartcolum /* -- END BACKGROUNDS -- */ // Cell Border - if ($table['borders_separate'] && $paintcell && $border) { + if ($cellBox && $border) { + $this->paintRoundedCellBorder($cellBox, $border, $border_details); + } elseif ($table['borders_separate'] && $paintcell && $border) { $this->_tableRect($x + ($table['border_spacing_H'] / 2) + ($border_details['L']['w'] / 2), $y + ($table['border_spacing_V'] / 2) + ($border_details['T']['w'] / 2), $w - $table['border_spacing_H'] - ($border_details['L']['w'] / 2) - ($border_details['R']['w'] / 2), $h - $table['border_spacing_V'] - ($border_details['T']['w'] / 2) - ($border_details['B']['w'] / 2), $border, $border_details, false, $table['borders_separate']); } elseif ($paintcell && $border) { $this->_tableRect($x, $y, $w, $h, $border, $border_details, true, $table['borders_separate']); // true causes buffer @@ -13530,6 +13539,10 @@ function TableHeaderFooter($content = '', $tablestartpage = '', $tablestartcolum } /* -- END BACKGROUNDS -- */ + if ($cellBox) { + $this->clipRoundedCellBackgrounds($cellBox); + } + // TABLE BORDER - if separate OR collapsed and only table border if (($table['borders_separate'] || ($this->simpleTables && !$table['simple']['border'])) && $table['border']) { $halfspaceL = $table['padding']['L'] + ($table['border_spacing_H'] / 2); @@ -21855,6 +21868,146 @@ function _tableGetMaxRowHeight($table, $row) } // CHANGED TO ALLOW TABLE BORDER TO BE SPECIFIED CORRECTLY - added border_details + /** + * The rounded border box of a table cell, or null for a square one. Only the separated borders model rounds a + * cell, as in CSS. Simple tables draw every cell with the table's border, and columns move what is drawn by the + * first coordinate of each write, which a curved path does not survive, so both stay square. + * + * The box also counts the backgrounds each of the table level's layers holds, so the cell's own, collected after, + * can be found by clipRoundedCellBackgrounds(). + * + * @param array|null $radius The cell's border_radius, as Tag\Td keeps it + * @param array $table + * @param int $level The table's nesting level + * @param float $x The cell's slot, border-spacing included + * @param float $y + * @param float $w + * @param float $h + * @param array $details The cell's border details + * + * @return array|null Keyed x0, y0, x1, y1 and radii, as RoundedBox::side() takes it, and counts + */ + function roundedCellBox($radius, $table, $level, $x, $y, $w, $h, $details) + { + if (!$radius || !$table['borders_separate'] || $this->simpleTables || $this->ColActive) { + return null; + } + + $x0 = $x + $table['border_spacing_H'] / 2; + $y0 = $y + $table['border_spacing_V'] / 2; + $x1 = $x + $w - $table['border_spacing_H'] / 2; + $y1 = $y + $h - $table['border_spacing_V'] / 2; + + // A table shrunk to fit its page shrinks its radii with it + $radii = BorderRadius::resolve($this->roundedBox->scale($radius['radii'], $this->shrin_k), $radius['percent'], $x1 - $x0, $y1 - $y0); + $radii = $this->roundedBox->fit($x1 - $x0, $y1 - $y0, $radii, ['top' => $details['T']['w'], 'right' => $details['R']['w'], 'bottom' => $details['B']['w'], 'left' => $details['L']['w']]); + + if (max(array_map('max', $radii)) <= 0) { + return null; + } + + $counts = []; + for ($layer = $level * 9; $layer < ($level + 1) * 9; $layer++) { + $counts[$layer] = count(Arrays::get($this->tableBackgrounds, $layer, [])); + } + + return ['x0' => $x0, 'y0' => $y0, 'x1' => $x1, 'y1' => $y1, 'radii' => $radii, 'counts' => $counts]; + } + + /** + * Clip the backgrounds collected for a rounded cell since roundedCellBox() counted them to its rounded border + * box, the row's among them. The clip takes the place of the square one a row background is given in each cell. + * + * @param array $box As roundedCellBox() gives it + */ + function clipRoundedCellBackgrounds($box) + { + $clip = null; + + foreach ($box['counts'] as $layer => $count) { + $total = count(Arrays::get($this->tableBackgrounds, $layer, [])); + for ($n = $count; $n < $total; $n++) { + if ($clip === null) { + $clip = ' q 0 w ' . $this->roundedBox->path($this->h, $box['x0'], $box['y0'], $box['x1'], $box['y1'], $box['radii']) . ' W n '; + } + $this->tableBackgrounds[$layer][$n]['clippath'] = $clip; + } + } + } + + /** + * Draw a rounded cell's border along its corners, each side on its own as PaintImgBorder() does. A double border + * is two strokes a third as wide, and groove, ridge, inset and outset take the two tones _tableRect() gives a + * square cell: the outer half of the top and left in one, the inner half in the other, and the other way round + * on the bottom and right. + * + * @param array $box As roundedCellBox() gives it + * @param int $bord The Border flags of the sides the cell draws + * @param array $details The cell's border details + */ + function paintRoundedCellBorder($box, $bord, $details) + { + $sides = ['top' => ['T', Border::TOP], 'right' => ['R', Border::RIGHT], 'bottom' => ['B', Border::BOTTOM], 'left' => ['L', Border::LEFT]]; + // The box a stroke runs round, by how far in from the outer edge it starts, in parts of each side's width + $boxes = [0 => $box]; + $translucent = false; + + foreach ($sides as $side => $flag) { + $bd = $details[$flag[0]]; + if (!$this->issetBorder($bord, $flag[1]) || !Border::drawsSide($bd)) { + continue; + } + if (!$bd['c']) { + $bd['c'] = $this->colorConverter->convert(0, $this->PDFAXwarnings); + } + $translucent = $translucent || $bd['c'][0] == 5 || $bd['c'][0] == 6; + + // Each stroke as [where it starts, its width, its colour], the first two in parts of the border's width + $strokes = [[0, 1, $bd['c']]]; + if ($bd['style'] === 'double') { + $strokes = [[0, 1 / 3, $bd['c']], [2 / 3, 1 / 3, $bd['c']]]; + } elseif (in_array($bd['style'], ['groove', 'ridge', 'inset', 'outset'], true)) { + $light = $this->colorConverter->lighten($bd['c']); + $dark = $this->colorConverter->darken($bd['c']); + $tones = ($bd['style'] === 'inset' || $bd['style'] === 'ridge') ? [$light, $dark] : [$dark, $light]; + if ($side === 'bottom' || $side === 'right') { + $tones = array_reverse($tones); + } + $strokes = [[0, 1 / 2, $tones[0]], [1 / 2, 1 / 2, $tones[1]]]; + } + + foreach ($strokes as $stroke) { + list($start, $width, $colour) = $stroke; + $key = (string) $start; + if (!isset($boxes[$key])) { + $insets = ['top' => $details['T']['w'] * $start, 'right' => $details['R']['w'] * $start, 'bottom' => $details['B']['w'] * $start, 'left' => $details['L']['w'] * $start]; + $boxes[$key] = [ + 'x0' => $box['x0'] + $insets['left'], + 'y0' => $box['y0'] + $insets['top'], + 'x1' => $box['x1'] - $insets['right'], + 'y1' => $box['y1'] - $insets['bottom'], + 'radii' => $this->roundedBox->inset($box['radii'], $insets), + ]; + } + + $this->_setBorderLine(['w' => $bd['w'] * $width, 'c' => $colour]); + if ($bd['style'] === 'dotted' || $bd['style'] === 'dashed') { + $this->_setDashBorder($bd['style'], '', '', $flag[0]); + } else { + $this->SetLineCap(0); // An arc stops at the diagonal, so nothing may run past it into the next side + } + $this->writer->write($this->roundedBox->side($this->h, $side, $boxes[$key], $bd['w'] * $width) . 'S'); + } + $this->SetLineJoin(2); + $this->SetLineCap(2); + $this->SetDash(); + } + + if ($translucent) { + $this->SetAlpha(1); + } + } + function _tableRect($x, $y, $w, $h, $bord = -1, $details = [], $buffer = false, $bSeparate = false, $cort = 'cell', $tablecorner = '', $bsv = 0, $bsh = 0) { $cellBorderOverlay = []; @@ -23177,6 +23330,7 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl $tablefooter[$i][$js]['border_details'] = $table['simple']['border_details']; } $tablefooter[$i][$js]['bgcolor'] = $cell['bgcolor']; + $tablefooter[$i][$js]['border_radius'] = Arrays::get($cell, 'border_radius', null); $tablefooter[$i][$js]['padding'] = $cell['padding']; if (isset($cell['rowspan'])) { $tablefooter[$i][$js]['rowspan'] = $cell['rowspan']; @@ -23751,6 +23905,8 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl $bord_det = $table['simple']['border_details']; } + $cellBox = $paintcell ? $this->roundedCellBox(Arrays::get($cell, 'border_radius', null), $table, $level, $x, $y, $w, $h, $bord_det) : null; + // TABLE ROW OR CELL FILL BGCOLOR $fill = 0; if (isset($cell['bgcolor']) && $cell['bgcolor'] && $cell['bgcolor'] != 'transparent') { @@ -23919,6 +24075,7 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl $tableheader[$i][$j]['rowspan'] = (isset($cell['rowspan']) ? $cell['rowspan'] : null); $tableheader[$i][$j]['colspan'] = (isset($cell['colspan']) ? $cell['colspan'] : null); $tableheader[$i][$j]['bgcolor'] = $cell['bgcolor']; + $tableheader[$i][$j]['border_radius'] = Arrays::get($cell, 'border_radius', null); if (!$this->simpleTables) { $tableheader[$i][$j]['border'] = $bord; @@ -23944,7 +24101,9 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl // CELL BORDER if ($bord) { - if ($table['borders_separate'] && $paintcell) { + if ($cellBox) { + $this->paintRoundedCellBorder($cellBox, $bord, $bord_det); + } elseif ($table['borders_separate'] && $paintcell) { $this->_tableRect($x + ($table['border_spacing_H'] / 2) + ($bord_det['L']['w'] / 2), $y + ($table['border_spacing_V'] / 2) + ($bord_det['T']['w'] / 2), $w - $table['border_spacing_H'] - ($bord_det['L']['w'] / 2) - ($bord_det['R']['w'] / 2), $h - $table['border_spacing_V'] - ($bord_det['T']['w'] / 2) - ($bord_det['B']['w'] / 2), $bord, $bord_det, false, $table['borders_separate']); } elseif (!$table['borders_separate']) { $this->_tableRect($x, $y, $w, $h, $bord, $bord_det, true, $table['borders_separate']); // true causes buffer @@ -24205,6 +24364,10 @@ function _tableWrite(&$table, $split = false, $startrow = 0, $startcol = 0, $spl /* -- END BACKGROUNDS -- */ + if ($cellBox) { + $this->clipRoundedCellBackgrounds($cellBox); + } + // TABLE BORDER - if separate if (($table['borders_separate'] || ($this->simpleTables && !$table['simple']['border'])) && $table['border']) { $halfspaceL = $table['padding']['L'] + ($table['border_spacing_H'] / 2); diff --git a/src/RoundedBox.php b/src/RoundedBox.php index becabb413..e39d9dd29 100644 --- a/src/RoundedBox.php +++ b/src/RoundedBox.php @@ -135,6 +135,25 @@ public function fit($width, $height, $radii, $borders) return $radii; } + /** + * Every corner, its radii divided by $k, as a table shrunk to fit its page shrinks what it holds. A corner left + * out is square. + * + * @param array $radii Keyed by the corners given a radius + * @param float $k + * + * @return array + */ + public function scale($radii, $k) + { + $scaled = self::SQUARE; + foreach ($radii as $corner => $r) { + $scaled[$corner] = [$r[0] / $k, $r[1] / $k]; + } + + return $scaled; + } + /** * Radii moved in from their edge by what lies along each side, stopping at square: the padding edge's corners * are the border edge's less the borders, and the content edge's are those less the paddings. $insets is keyed @@ -171,11 +190,7 @@ public function imageBox($objattr, $k) $paddings[$side] = $objattr['padding_' . $side] / $k; } - $radii = self::SQUARE; - foreach ($objattr['border_radius'] as $corner => $r) { - $radii[$corner] = [$r[0] / $k, $r[1] / $k]; - } - $radii = $this->fit($x1 - $x0, $y1 - $y0, $radii, $borders); + $radii = $this->fit($x1 - $x0, $y1 - $y0, $this->scale($objattr['border_radius'], $k), $borders); return [ 'x0' => $x0, diff --git a/src/Tag/Td.php b/src/Tag/Td.php index c325325c9..2df6f053e 100644 --- a/src/Tag/Td.php +++ b/src/Tag/Td.php @@ -3,6 +3,7 @@ namespace Mpdf\Tag; use Mpdf\Css\Border; +use Mpdf\Css\BorderRadius; use Mpdf\Css\TextVars; use Mpdf\CssMode; use Mpdf\Utils\Arrays; @@ -210,6 +211,14 @@ public function open($attr, &$ahtml, &$ihtml) } } /* -- END BACKGROUNDS -- */ + + if ($this->mpdf->cssMode === CssMode::STANDARD) { + list($radii, $percent) = BorderRadius::parse($properties, $this->sizeConverter, $this->mpdf->FontSize); + if ($radii) { + $c['border_radius'] = ['radii' => $radii, 'percent' => $percent]; + } + } + if (isset($properties['VERTICAL-ALIGN'])) { $c['va'] = $this->getAlign($properties['VERTICAL-ALIGN']); } elseif (isset($attr['VALIGN'])) { diff --git a/tests/Mpdf/TableCellBorderRadiusTest.php b/tests/Mpdf/TableCellBorderRadiusTest.php new file mode 100644 index 000000000..2099dfa35 --- /dev/null +++ b/tests/Mpdf/TableCellBorderRadiusTest.php @@ -0,0 +1,294 @@ +A'; + + return $this->pages($this->render($html, $config))[0]; + } + + /** + * The clip paths on a page that run through at least one curve + * + * @param string $page + * + * @return string[] + */ + private function roundedClips($page) + { + preg_match_all('/q 0 w ((?:[-\d.]+ [-\d.]+ [ml] |(?:[-\d.]+ ){6}c )+) W n/', $page, $m); + + return array_values(array_filter($m[1], function ($path) { + return strpos($path, ' c ') !== false; + })); + } + + /** + * The radius of each rounded corner of a path, keyed TL/TR/BR/BL as [horizontal, vertical] in millimetres. A + * corner is a run of curves; its radii are how far the run travels along each axis. Square corners are left out. + * + * @param string $path + * + * @return array + */ + private function radii($path) + { + preg_match_all('/((?:[-\d.]+ )+)([mlc]) /', $path, $ops, PREG_SET_ORDER); + + $radii = []; + $corner = 0; + $point = null; + $arcStart = null; + foreach ($ops as $op) { + $numbers = array_map('floatval', explode(' ', trim($op[1]))); + if ($op[2] === 'c') { + if ($arcStart === null) { + $arcStart = $point; + } + } elseif ($arcStart !== null) { + $radii[self::CORNERS[$corner]] = [abs($point[0] - $arcStart[0]) / Mpdf::SCALE, abs($point[1] - $arcStart[1]) / Mpdf::SCALE]; + $arcStart = null; + } + if ($op[2] === 'l') { + $corner++; + } + $point = array_slice($numbers, -2); + } + + return $radii; + } + + /** + * The width and height of the box a path runs round, in millimetres + * + * @param string $path + * + * @return float[] + */ + private function sizeOf($path) + { + preg_match_all('/([-\d.]+) ([-\d.]+) [ml] /', $path, $m); + $xs = array_map('floatval', $m[1]); + $ys = array_map('floatval', $m[2]); + + return [(max($xs) - min($xs)) / Mpdf::SCALE, (max($ys) - min($ys)) / Mpdf::SCALE]; + } + + /** + * The one rounded clip on a page, asserting there is exactly one + * + * @param string $page + * + * @return string + */ + private function onlyClip($page) + { + $clips = $this->roundedClips($page); + $this->assertCount(1, $clips, 'one rounded clip'); + + return $clips[0]; + } + + /** + * A length rounds every corner of the cell's background by that much + */ + public function testALengthRoundsTheBackgroundOfACell() + { + $radii = $this->radii($this->onlyClip($this->cellPage('background: #0f0; border-radius: 3mm'))); + + $this->assertSame(self::CORNERS, array_keys($radii)); + foreach ($radii as $corner => $radius) { + $this->assertEqualsWithDelta([3, 3], $radius, 0.01, $corner); + } + } + + /** + * The clip runs round the box the cell's background fills when it is square, inside the border-spacing + */ + public function testTheClipIsTheBoxASquareBackgroundFills() + { + $this->assertSame(1, preg_match('/[-\d.]+ [-\d.]+ ([-\d.]+) -([-\d.]+) re f/', $this->cellPage('background: #0f0; border: 1mm solid #00f'), $square)); + + $size = $this->sizeOf($this->onlyClip($this->cellPage('background: #0f0; border: 1mm solid #00f; border-radius: 3mm'))); + + $this->assertEqualsWithDelta([$square[1] / Mpdf::SCALE, $square[2] / Mpdf::SCALE], $size, 0.01); + } + + /** + * A percentage is of the cell's own border box: horizontal of its width, vertical of its height + */ + public function testAPercentageIsOfTheCellsOwnBox() + { + $clip = $this->onlyClip($this->cellPage('background: #0f0; border-radius: 50%')); + $size = $this->sizeOf($clip); + + foreach ($this->radii($clip) as $corner => $radius) { + $this->assertEqualsWithDelta([$size[0] / 2, $size[1] / 2], $radius, 0.01, $corner); + } + } + + /** + * A corner longhand rounds that corner alone + */ + public function testACornerLonghandRoundsThatCornerAlone() + { + $radii = $this->radii($this->onlyClip($this->cellPage('background: #0f0; border-bottom-right-radius: 4mm 2mm'))); + + $this->assertSame(['BR'], array_keys($radii)); + $this->assertEqualsWithDelta([4, 2], $radii['BR'], 0.01); + } + + /** + * The row's background shows through a cell with none of its own, and is clipped to the cell's corners too + */ + public function testTheRowBackgroundIsClippedToARoundedCell() + { + $html = '
A
'; + + $this->onlyClip($this->pages($this->render($html))[0]); + } + + /** + * A gradient background is clipped to the rounded box as a colour is + */ + public function testAGradientIsClippedToARoundedCell() + { + $page = $this->cellPage('background-image: linear-gradient(#f00, #00f); border-radius: 3mm'); + + $this->onlyClip($page); + $this->assertMatchesRegularExpression('/c [-\d. ]+ l\s+W n\s+q [-\d. ]+ re W n\s+[-\d. ]+ cm\s+\/Sh\d+ sh/', $page, 'the gradient is painted inside the rounded clip'); + } + + /** + * The border is stroked side by side along the curve of each corner + */ + public function testTheBorderFollowsTheCorners() + { + $page = $this->cellPage('border: 1mm solid #00f; border-radius: 3mm'); + + $this->assertSame(4, preg_match_all('/c S/', $page), 'four sides end in a curve'); + } + + /** + * A double border is two strokes per side, and groove two of different colours + */ + public function testDoubleAndGrooveBordersAreTwoStrokesPerSide() + { + $this->assertSame(8, preg_match_all('/c S/', $this->cellPage('border: 3mm double #00f; border-radius: 5mm'))); + + $groove = $this->cellPage('border: 3mm groove #36c; border-radius: 5mm'); + $this->assertSame(8, preg_match_all('/c S/', $groove)); + preg_match_all('/([\d.]+ [\d.]+ [\d.]+) RG/', $groove, $m); + $this->assertCount(2, array_unique($m[1]), 'groove takes two tones'); + } + + /** + * A side the cell has no border on is not drawn + */ + public function testOnlyTheSidesWithABorderAreDrawn() + { + $this->assertSame(1, preg_match_all('/ S/', $this->cellPage('border-top: 1mm solid #00f; border-radius: 3mm'))); + } + + /** + * CSS ignores border-radius on a cell under border-collapse: collapse; the cell is drawn as if it had none + */ + public function testACollapsedCellStaysSquare() + { + $collapse = 'border-collapse: collapse'; + + $this->assertSame( + $this->cellPage('background: #0f0; border: 1mm solid #00f', $collapse), + $this->cellPage('background: #0f0; border: 1mm solid #00f; border-radius: 3mm', $collapse) + ); + } + + /** + * The legacy CSS mode draws the cell square, as before + */ + public function testTheLegacyModeKeepsTheCellSquare() + { + $legacy = ['cssMode' => CssMode::LEGACY]; + $table = 'border-collapse: separate; border-spacing: 2mm'; + + $this->assertSame( + $this->cellPage('background: #0f0; border: 1mm solid #00f', $table, $legacy), + $this->cellPage('background: #0f0; border: 1mm solid #00f; border-radius: 3mm', $table, $legacy) + ); + } + + /** + * A radius of nothing draws the cell exactly as one without + */ + public function testAZeroRadiusDrawsTheCellAsBefore() + { + $this->assertSame( + $this->cellPage('background: #0f0; border: 1mm solid #00f'), + $this->cellPage('background: #0f0; border: 1mm solid #00f; border-radius: 0') + ); + } + + /** + * Simple tables draw every cell with the table's own border, and leave the radius out with the cell's border + */ + public function testSimpleTablesKeepTheCellSquare() + { + $this->assertSame([], $this->roundedClips($this->cellPage('background: #0f0; border-radius: 3mm', 'border-collapse: separate', ['simpleTables' => true]))); + } + + /** + * A header row repeated on the next page keeps its rounded corners + */ + public function testARepeatedHeaderRowStaysRounded() + { + $rows = str_repeat('Row', 80); + $html = '' . $rows . '
Head
'; + + $pages = $this->pages($this->render($html)); + + $this->assertGreaterThan(1, count($pages)); + foreach ($pages as $n => $page) { + $this->assertCount(1, $this->roundedClips($page), 'page ' . ($n + 1)); + } + } + + /** + * A cell in a nested table is clipped by its own table, not the outer one's rounded cell + */ + public function testANestedTableKeepsItsOwnClips() + { + $inner = '
In
'; + $html = '
' . $inner . '
'; + + $clips = $this->roundedClips($this->pages($this->render($html))[0]); + + $this->assertCount(2, $clips); + $this->assertNotSame($clips[0], $clips[1]); + } +} diff --git a/tests/Snapshots/TableCellBorderRadiusSnapshotTest.php b/tests/Snapshots/TableCellBorderRadiusSnapshotTest.php new file mode 100644 index 000000000..d7a062836 --- /dev/null +++ b/tests/Snapshots/TableCellBorderRadiusSnapshotTest.php @@ -0,0 +1,76 @@ + + body { font-size: 8pt; } + h1 { font-size: 13pt; margin: 0 0 1mm 0; } + p { margin: 1.5mm 0 0.5mm 0; } + table { border-collapse: separate; border-spacing: 1mm; } + td { width: 60mm; height: 10mm; } + tr { background: #fcc; } + +

border-radius on table cells

'; + + foreach ($this->cases() as $case) { + $html .= '

' . $case[2] . '

' . $case[1] . '
'; + } + + $html .= '

border-collapse: collapse ignores the radius: a square green cell with a blue border.

' + . '
collapse
'; + + $this->mpdf = $this->createMpdf(); + + $this->mpdf->WriteHTML($html); + } + +} diff --git a/tests/data/snapshots/table-cell-border-radius.pdf b/tests/data/snapshots/table-cell-border-radius.pdf new file mode 100644 index 000000000..94d1fd278 Binary files /dev/null and b/tests/data/snapshots/table-cell-border-radius.pdf differ