Skip to content

Commit

Permalink
Enable accessibility in tables
Browse files Browse the repository at this point in the history
  • Loading branch information
hollandjake committed Jan 30, 2025
1 parent 0429c7d commit 388106c
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 34 deletions.
1 change: 1 addition & 0 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class PDFDocument extends stream.Readable {
this.initImages();
this.initOutline();
this.initMarkings(options);
this.initTables();
this.initSubset(options);

// Initialize the metadata
Expand Down
3 changes: 3 additions & 0 deletions lib/mixins/table.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import PDFTable from '../table/index';

export default {
initTables() {
this._tableIndex = 0;
},
/**
* @param {Table} [opts]
* @returns {PDFTable}
Expand Down
143 changes: 143 additions & 0 deletions lib/table/accessibility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import PDFStructureElement from '../structure_element';
import PDFDocument from '../document';

/**
* Add accessibility to a table
*
* @this PDFTable
* @memberOf PDFTable
* @private
*/
export function accommodateTable() {
if (this.opts.structParent) {
this._tableStruct = this.document.struct('Table');
this._tableStruct.dictionary.data.ID = this._id;
if (this.opts.structParent instanceof PDFStructureElement) {
this.opts.structParent.add(this._tableStruct);
} else if (this.opts.structParent instanceof PDFDocument) {
this.opts.structParent.addStructure(this._tableStruct);
}
this._headerRowLookup = {};
this._headerColumnLookup = {};
}
}

/**
* Cleanup accessibility on a table
*
* @this PDFTable
* @memberOf PDFTable
* @private
*/
export function accommodateCleanup() {
if (this._tableStruct) this._tableStruct.end();
}

/**
* Render a row with all its accessibility features
*
* @this PDFTable
* @memberOf PDFTable
* @param {SizedNormalizedTableCellStyle[]} row
* @param {number} rowIndex
* @param {Function} renderCell
* @private
*/
export function accessibleRow(row, rowIndex, renderCell) {
const rowStruct = this.document.struct('TR');
rowStruct.dictionary.data.ID = new String(`${this._id}-${rowIndex}`);
this._tableStruct.add(rowStruct);
row.forEach((cell) => renderCell(cell, rowStruct));
rowStruct.end();
}

/**
* Render a cell with all its accessibility features
*
* @this PDFTable
* @memberOf PDFTable
* @param {SizedNormalizedTableCellStyle} cell
* @param {PDFStructureElement} rowStruct
* @param {Function} callback
* @private
*/
export function accessibleCell(cell, rowStruct, callback) {
const cellStruct = this.document.struct(cell.type, { title: cell.title });
cellStruct.dictionary.data.ID = cell.id;

rowStruct.add(cellStruct);

const padding = cell.padding;
const border = cell.border;
const attributes = {
O: 'Table',
Width: cell.width,
Height: cell.height,
Padding: [padding.top, padding.bottom, padding.left, padding.right],
RowSpan: cell.rowSpan > 1 ? cell.rowSpan : undefined,
ColSpan: cell.colSpan > 1 ? cell.colSpan : undefined,
BorderThickness: [border.top, border.bottom, border.left, border.right],
};

// Claim row Headers
if (cell.type === 'TH') {
if (cell.scope === 'Row' || cell.scope === 'Both') {
for (let i = 0; i < cell.rowSpan; i++) {
if (!this._headerRowLookup[cell.rowIndex + i]) {
this._headerRowLookup[cell.rowIndex + i] = [];
}
this._headerRowLookup[cell.rowIndex + i].push(cell.id);
}
attributes.Scope = cell.scope;
}
if (cell.scope === 'Column' || cell.scope === 'Both') {
for (let i = 0; i < cell.colSpan; i++) {
if (!this._headerColumnLookup[cell.colIndex + i]) {
this._headerColumnLookup[cell.colIndex + i] = [];
}
this._headerColumnLookup[cell.colIndex + i].push(cell.id);
}
attributes.Scope = cell.scope;
}
}

// Find any cells which are marked as headers for this cell
const Headers = new Set(
[
...Array.from(
{ length: cell.colSpan },
(_, i) => this._headerColumnLookup[cell.colIndex + i],
).flat(),
...Array.from(
{ length: cell.rowSpan },
(_, i) => this._headerRowLookup[cell.rowIndex + i],
).flat(),
].filter(Boolean),
);
if (Headers.size) attributes.Headers = Array.from(Headers);

if (cell.backgroundColor !== undefined) {
attributes.BackgroundColor = this.document._normalizeColor(
cell.backgroundColor,
);
}
const hasBorder = [border.top, border.bottom, border.left, border.right];
if (hasBorder.some((x) => x)) {
const borderColor = cell.borderColor;
attributes.BorderColor = [
hasBorder[0] ? this.document._normalizeColor(borderColor.top) : null,
hasBorder[1] ? this.document._normalizeColor(borderColor.bottom) : null,
hasBorder[2] ? this.document._normalizeColor(borderColor.left) : null,
hasBorder[3] ? this.document._normalizeColor(borderColor.right) : null,
];
}

// Remove any undefined attributes
Object.keys(attributes).forEach(
(key) => attributes[key] === undefined && delete attributes[key],
);
cellStruct.dictionary.data.A = this.document.ref(attributes);
cellStruct.add(callback);
cellStruct.end();
cellStruct.dictionary.data.A.end();
}
8 changes: 3 additions & 5 deletions lib/table/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { normalizeRow, normalizeTable } from './normalize';
import { measure, ensure } from './size';
import { renderRow } from './render';
import { accommodateCleanup, accommodateTable } from './accessibility';

class PDFTable {
/**
Expand All @@ -12,6 +13,7 @@ class PDFTable {
this.opts = Object.freeze(opts);

normalizeTable.call(this);
accommodateTable.call(this);

this._currRowIndex = 0;
this._ended = false;
Expand All @@ -35,11 +37,6 @@ class PDFTable {
throw new Error(`Table was marked as ended on row ${this._currRowIndex}`);
}

if (this._currRowIndex === 0) {
this._struct = this.document.struct('Table');
this.document.addStructure(this._struct);
}

// Convert the iterable into an array
row = Array.from(row);
// Transform row
Expand Down Expand Up @@ -75,6 +72,7 @@ class PDFTable {
// Flush any remaining cells
while (this._rowBuffer?.size) this.row([]);
this._ended = true;
accommodateCleanup.call(this);
return this.document;
}
}
Expand Down
11 changes: 10 additions & 1 deletion lib/table/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { COLUMN_FIELDS, ROW_FIELDS } from './types';
*/
export function normalizeTable() {
// Normalize config
let index = this.document._tableIndex++;
this._id = new String(this.opts.id ?? `table-${index}`);
this._position = {
x: this.document.sizeToPoint(this.opts.position?.x, this.document.x),
y: this.document.sizeToPoint(this.opts.position?.y, this.document.y),
Expand Down Expand Up @@ -137,9 +139,16 @@ export function normalizeCell(cell, rowIndex, colIndex) {
config.textStroke = this.document.sizeToPoint(config.textStroke, 0);
config.textStrokeColor = config.textStrokeColor ?? 'black';
config.textColor = config.textColor ?? 'black';
config.type = config.type === 'TH' ? 'TH' : 'TD';
config.textOptions = config.textOptions ?? {};

// Accessibility settings
config.id = new String(config.id ?? `${this._id}-${rowIndex}-${colIndex}`);
config.type = config.type?.toUpperCase() === 'TH' ? 'TH' : 'TD';
config.scope = config.scope?.toLowerCase();
if (config.scope === "row") config.scope = "Row"
else if (config.scope === "both") config.scope = "Both"
else if (config.scope === "column") config.scope = "Column"

if (this.opts.debug !== undefined) config.debug = this.opts.debug;

this.document.font(rollbackFont, rollbackFontFamily, rollbackFontSize);
Expand Down
41 changes: 13 additions & 28 deletions lib/table/render.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { accessibleCell, accessibleRow } from './accessibility';

/**
* Render a cell
*
Expand All @@ -8,9 +10,11 @@
* @private
*/
export function renderRow(row, rowIndex) {
const rowStruct = this.document.struct('TR');
row.forEach((cell) => renderCell.call(this, cell, rowStruct));
this._struct.add(rowStruct);
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];
}
Expand All @@ -25,23 +29,9 @@ export function renderRow(row, rowIndex) {
* @private
*/
function renderCell(cell, rowStruct) {
const s = this.document.struct(cell.type);
rowStruct.add(s);
s.dictionary.data.Width = cell.width;
s.dictionary.data.Height = cell.height;
s.dictionary.data.Padding = [
cell.padding.top,
cell.padding.bottom,
cell.padding.left,
cell.padding.right,
];
s.dictionary.data.RowSpan = cell.rowSpan;
s.dictionary.data.ColSpan = cell.colSpan;
s.add(() => {
const cellRenderer = () => {
// Render cell background
if (cell.backgroundColor !== undefined) {
s.dictionary.data.BackgroundColor =
this.document._normalizeColor(cell.backgroundColor) ?? undefined;
this.document
.save()
.rect(cell.x, cell.y, cell.width, cell.height)
Expand All @@ -50,14 +40,6 @@ function renderCell(cell, rowStruct) {
}

// Render border
s.dictionary.data.BorderColor =
this.document._normalizeColor(cell.borderColor) ?? undefined;
s.dictionary.data.BorderThickness = [
cell.border.top,
cell.border.bottom,
cell.border.left,
cell.border.right,
];
renderBorder.call(
this,
cell.border,
Expand All @@ -83,13 +65,16 @@ function renderCell(cell, rowStruct) {

// Render text
if (cell.text) renderCellText.call(this, cell);
});
};

if (rowStruct) accessibleCell.call(this, cell, rowStruct, cellRenderer);
else cellRenderer();
}

/**
* @this PDFTable
* @memberOf PDFTable
* @param cell
* @param {SizedNormalizedTableCellStyle} cell
*/
function renderCellText(cell) {
// Configure fonts
Expand Down
17 changes: 17 additions & 0 deletions lib/table/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@
* Same as the options you pass to `doc.text()`
*
* Will override any defaults set by the cell if set
* @property {string} [title]
* Sets the accessible title for the cell
* @property {'Column' | 'Row' | 'Both'} [scope]
* Sets the accessible scope for the cell
* @property {string} [id]
* Sets the accessible id for the cell
*
* Defaults to `<tableId>-<rowIndex>-<colIndex>`
* @property {boolean} [debug]
* Whether to show the debug lines for the cell
*
Expand Down Expand Up @@ -252,6 +260,15 @@
*
* If provided the table will be automatically ended after the last row has been written,
* Otherwise it is up to the user to call `table.end()` or `table.row([], true)`
* @property {PDFStructureElement} [structParent]
* The parent structure to mount to
*
* This will cause the entire table to be enclosed in a Table structure
* with TR and TD/TH for cells
* @property {string} [id]
* Sets the accessible id for the table
*
* Defaults to `table-<number>`
* @property {boolean} [debug]
* Whether to show the debug lines for all the cells
*
Expand Down

0 comments on commit 388106c

Please sign in to comment.