pdfkit/lib/table/render.js
Jake Holland 033ba3426b
Add support for tables (#1577)
* Add page size utilities

- Added page.contentWidth
- Added page.contentHeight

* Add table support

- Tables support cell customization (including colors)
- Tables also support rotatable text (with alignment support)
- Tables have accessibility support

* chore: fix code generation context

- code generation now respects the current document positioning to allow use of page dependent operations

* chore: remove comments from build

* removed unnecessary config optimisations

* Optimize table minification

* Performance improvements to tables

* Improve font handling in tables
2025-02-24 07:49:25 -03:00

223 lines
5.3 KiB
JavaScript

import { accessibleCell, accessibleRow } from './accessibility';
/**
* Render a cell
*
* @this PDFTable
* @memberOf PDFTable
* @param {SizedNormalizedTableCellStyle[]} row
* @param {number} rowIndex
* @private
*/
export function renderRow(row, rowIndex) {
if (this._tableStruct) {
accessibleRow.call(this, row, rowIndex, renderCell.bind(this));
} else {
row.forEach((cell) => renderCell.call(this, cell));
}
return this._rowYPos[rowIndex] + this._rowHeights[rowIndex];
}
/**
* Render a cell
*
* @this PDFTable
* @memberOf PDFTable
* @param {SizedNormalizedTableCellStyle} cell
* @param {PDFStructureElement} rowStruct
* @private
*/
function renderCell(cell, rowStruct) {
const cellRenderer = () => {
// Render cell background
if (cell.backgroundColor != null) {
this.document
.save()
.rect(cell.x, cell.y, cell.width, cell.height)
.fill(cell.backgroundColor)
.restore();
}
// Render border
renderBorder.call(
this,
cell.border,
cell.borderColor,
cell.x,
cell.y,
cell.width,
cell.height,
);
// Debug cell borders
if (cell.debug) {
this.document.save();
this.document.dash(1, { space: 1 }).lineWidth(1).strokeOpacity(0.3);
// Debug cell bounds
this.document
.rect(cell.x, cell.y, cell.width, cell.height)
.stroke('green');
this.document.restore();
}
// Render text
if (cell.text) renderCellText.call(this, cell);
};
if (rowStruct) accessibleCell.call(this, cell, rowStruct, cellRenderer);
else cellRenderer();
}
/**
* @this PDFTable
* @memberOf PDFTable
* @param {SizedNormalizedTableCellStyle} cell
*/
function renderCellText(cell) {
const doc = this.document;
// Configure fonts
const rollbackFont = doc._fontSource;
const rollbackFontSize = doc._fontSize;
const rollbackFontFamily = doc._fontFamily;
if (cell.customFont) {
if (cell.font.src) doc.font(cell.font.src, cell.font.family);
if (cell.font.size) doc.fontSize(cell.font.size);
}
const x = cell.textX;
const y = cell.textY;
const Ah = cell.textAllocatedHeight;
const Aw = cell.textAllocatedWidth;
const Cw = cell.textBounds.width;
const Ch = cell.textBounds.height;
const Ox = -cell.textBounds.x;
const Oy = -cell.textBounds.y;
const PxScale =
cell.align.x === 'right' ? 1 : cell.align.x === 'center' ? 0.5 : 0;
const Px = (Aw - Cw) * PxScale;
const PyScale =
cell.align.y === 'bottom' ? 1 : cell.align.y === 'center' ? 0.5 : 0;
const Py = (Ah - Ch) * PyScale;
const dx = Px + Ox;
const dy = Py + Oy;
if (cell.debug) {
doc.save();
doc.dash(1, { space: 1 }).lineWidth(1).strokeOpacity(0.3);
// Debug actual text bounds
if (cell.text) {
doc
.moveTo(x + Px, y)
.lineTo(x + Px, y + Ah)
.moveTo(x + Px + Cw, y)
.lineTo(x + Px + Cw, y + Ah)
.stroke('blue')
.moveTo(x, y + Py)
.lineTo(x + Aw, y + Py)
.moveTo(x, y + Py + Ch)
.lineTo(x + Aw, y + Py + Ch)
.stroke('green');
}
// Debug allocated text bounds
doc.rect(x, y, Aw, Ah).stroke('orange');
doc.restore();
}
// Create text mask to cut off any overflowing text
// Mask cuts off at the padding not the actual cell, this is intentional!
doc.save().rect(x, y, Aw, Ah).clip();
doc.fillColor(cell.textColor).strokeColor(cell.textStrokeColor);
if (cell.textStroke > 0) doc.lineWidth(cell.textStroke);
// Render the text
doc.text(cell.text, x + dx, y + dy, cell.textOptions);
// Cleanup
doc.restore();
if (cell.font) doc.font(rollbackFont, rollbackFontFamily, rollbackFontSize);
}
/**
* @this PDFTable
* @memberOf PDFTable
* @param {ExpandedSideDefinition<number>} border
* @param {ExpandedSideDefinition<PDFColor>} borderColor
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @param {number[]} [mask]
* @private
*/
function renderBorder(border, borderColor, x, y, width, height, mask) {
border = Object.fromEntries(
Object.entries(border).map(([k, v]) => [k, mask && !mask[k] ? 0 : v]),
);
const doc = this.document;
if (
[border.right, border.bottom, border.left].every(
(val) => val === border.top,
)
) {
if (border.top > 0) {
doc
.save()
.lineWidth(border.top)
.rect(x, y, width, height)
.stroke(borderColor.top)
.restore();
}
} else {
// Top
if (border.top > 0) {
doc
.save()
.lineWidth(border.top)
.moveTo(x, y)
.lineTo(x + width, y)
.stroke(borderColor.top)
.restore();
}
// Right
if (border.right > 0) {
doc
.save()
.lineWidth(border.right)
.moveTo(x + width, y)
.lineTo(x + width, y + height)
.stroke(borderColor.right)
.restore();
}
// Bottom
if (border.bottom > 0) {
doc
.save()
.lineWidth(border.bottom)
.moveTo(x + width, y + height)
.lineTo(x, y + height)
.stroke(borderColor.bottom)
.restore();
}
// Left
if (border.left > 0) {
doc
.save()
.lineWidth(border.left)
.moveTo(x, y + height)
.lineTo(x, y)
.stroke(borderColor.left)
.restore();
}
}
}