Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG-GRAVITYPDF.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<barcode>`, `<dottab>` and `<textcircle>` 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
-----------
Expand Down Expand Up @@ -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
167 changes: 165 additions & 2 deletions src/Mpdf.php
Original file line number Diff line number Diff line change
Expand Up @@ -3256,13 +3256,19 @@ 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";
} elseif ($pb['col'][0] == 6) { // CMYKa
$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']) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 20 additions & 5 deletions src/RoundedBox.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions src/Tag/Td.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'])) {
Expand Down
Loading