New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

pdf-lib-simple-tables

Package Overview
Dependencies
Maintainers
0
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pdf-lib-simple-tables - npm Package Compare versions

Comparing version

to
1.1.0

build/index.cjs.js

0

.eslintrc.js

@@ -0,0 +0,0 @@ module.exports = {

@@ -0,0 +0,0 @@ import { PdfTable } from '../src';

@@ -0,0 +0,0 @@ module.exports = {

18

package.json
{
"name": "pdf-lib-simple-tables",
"version": "1.0.0",
"description": "TS Bibliothek zur Erstellung von Tabellen auf Basis von pdf-lib",
"main": "dist/index.js",
"version": "1.1.0",
"description": "TS library for creating tables based on pdf-lib",
"main": "build/index.cjs",
"module": "build/index.js",
"browser": {
"./build/index.cjs": "./build/index.browser.js"
},
"scripts": {
"build": "tsc",
"build": "vite build",
"build:browser": "vite build --format iife",
"test": "jest",

@@ -26,3 +31,5 @@ "lint": "eslint . --ext .ts",

"eslint-config-prettier": "^9.1.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"lint-staged": "^13.2.2",
"prettier": "^2.8.8",

@@ -32,4 +39,3 @@ "ts-jest": "^29.2.6",

"typescript": "^4.9.5",
"husky": "^8.0.3",
"lint-staged": "^13.2.2"
"vite": "^6.2.0"
},

@@ -36,0 +42,0 @@ "keywords": [

@@ -160,2 +160,55 @@ # PDF-lib Table Library

## Architecture
The library uses a modular architecture to separate concerns and improve maintainability:
### Core Modules
- **TableDataManager**: Manages the table data (cell content, styles, rows/columns)
- **MergeCellManager**: Handles cell merging operations
- **FontManager**: Manages custom fonts and font embedding
- **TableStyleManager**: Applies and combines styles from different sources
- **BorderRenderer**: Renders different types of borders (solid, dashed, dotted)
- **TableRenderer**: Renders the complete table with all its elements
- **ImageEmbedder**: Handles embedding tables as images in PDFs
### Module Relationships
The `PdfTable` class serves as a facade, coordinating these modules to provide a simple API for users. Each module has a specific responsibility:
```
┌───────────────────┐ ┌──────────────────┐
│ PdfTable │ uses │ TableRenderer │
│ (Facade Class) │ ◄────────┤ │
└───────────────────┘ └────────┬─────────┘
│ │
│ delegates │ uses
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ TableDataManager │ │ BorderRenderer │
└───────────────────┘ └──────────────────┘
│ │
│ │ uses
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ MergeCellManager │ │ TableStyleManager │
└───────────────────┘ └──────────────────┘
┌───────────────────┐ ┌──────────────────┐
│ FontManager │ │ ImageEmbedder │
└───────────────────┘ └──────────────────┘
```
## Node.js and Browser Support
This project now fully supports both Node.js and browser environments.
For browsers, use the bundle in `build/index.browser.js` – this is created using Vite.
You can generate the browser build with the following npm script:
```bash
npm run build:browser
```
For more details and options, see the API documentation below.

@@ -188,2 +241,34 @@

### Core Modules (For Developers/Contributors)
These modules are used internally by the `PdfTable` class:
#### `TableDataManager`
Manages table data and structure.
#### `TableRenderer`
Handles the rendering of tables to PDF.
#### `BorderRenderer`
Specializes in rendering different border styles.
#### `TableStyleManager`
Applies and combines cell styles.
#### `MergeCellManager`
Manages merged cells functionality.
#### `FontManager`
Handles font embedding and custom fonts.
#### `ImageEmbedder`
Embeds tables as images in PDFs.
### Interfaces

@@ -229,2 +314,16 @@

### Build
Use Vite to create the bundle:
```bash
npm run build
```
For the browser build (IIFE format):
```bash
npm run build:browser
```
### Running Tests

@@ -231,0 +330,0 @@

@@ -1,7 +0,13 @@

import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import { CustomFont } from '../models/CustomFont';
import { defaultDesignConfig, DesignConfig } from '../config/DesignConfig';
import { MergedCell } from '../interfaces/MergedCell';
import { TableCellStyle } from '../interfaces/TableCellStyle';
import { TableOptions } from '../interfaces/TableOptions';
import { BorderRenderer } from '../renderers/BorderRenderer';
import { TableStyleManager } from '../managers/TableStyleManager';
import { TableRenderer } from '../renderers/TableRenderer';
import { TableDataManager } from '../managers/TableDataManager';
import { MergeCellManager } from '../managers/MergeCellManager';
import { FontManager } from '../managers/FontManager';
import { ImageEmbedder } from '../embedders/ImageEmbedder';

@@ -15,12 +21,16 @@ /**

export class PdfTable {
private options: TableOptions;
private data: string[][] = [];
private cellStyles: TableCellStyle[][] = []; // Matrix for cell styles
private mergedCells: MergedCell[] = [];
private customFont?: CustomFont;
private designConfig: DesignConfig; // new property
private designConfig: DesignConfig;
// Module für verschiedene Funktionalitäten
private borderRenderer: BorderRenderer;
private styleManager: TableStyleManager;
private tableRenderer: TableRenderer;
private dataManager: TableDataManager;
private mergeCellManager: MergeCellManager;
private fontManager: FontManager;
private imageEmbedder: ImageEmbedder;
constructor(options: TableOptions) {
// Set default values if not present and merge design config
this.options = {
const completeOptions = {
rowHeight: 20,

@@ -31,246 +41,88 @@ colWidth: 80,

this.designConfig = { ...defaultDesignConfig, ...options.designConfig };
this.initData();
}
private initData(): void {
// Ändere cellStyles, um separate Objekte pro Zelle zu erzeugen
this.data = Array.from({ length: this.options.rows }, () =>
Array(this.options.columns).fill(''),
);
this.cellStyles = Array.from({ length: this.options.rows }, () =>
Array.from({ length: this.options.columns }, () => ({})),
);
// Initialisierung der Module
this.borderRenderer = new BorderRenderer();
this.styleManager = new TableStyleManager(this.designConfig);
this.tableRenderer = new TableRenderer(this.borderRenderer, this.styleManager);
this.dataManager = new TableDataManager(completeOptions);
this.mergeCellManager = new MergeCellManager();
this.fontManager = new FontManager();
this.imageEmbedder = new ImageEmbedder();
}
// Method to fill a cell
// Validierung und Delegate an den DataManager
setCell(row: number, col: number, value: string): void {
if (row < this.options.rows && col < this.options.columns) {
this.data[row][col] = value;
}
this.dataManager.setCell(row, col, value);
}
// Angepasste setCellStyle-Methode: nur den übergebenen Stil speichern
setCellStyle(row: number, col: number, style: TableCellStyle): void {
if (row < this.options.rows && col < this.options.columns) {
// Direkte Zuweisung statt Merging mit Default-Stilen
this.cellStyles[row][col] = style;
} else {
throw new Error('Invalid cell coordinates');
}
this.dataManager.setCellStyle(row, col, style);
}
// New method to merge cells with validation
// Delegate an den MergeCellManager
mergeCells(startRow: number, startCol: number, endRow: number, endCol: number): void {
// Validation: start coordinates must be less than or equal to end coordinates
if (startRow > endRow || startCol > endCol) {
throw new Error('Invalid cell coordinates for mergeCells');
}
// ...further validations could be done here...
this.mergedCells.push({ startRow, startCol, endRow, endCol });
this.dataManager.validateCellIndices(startRow, startCol);
this.dataManager.validateCellIndices(endRow, endCol);
this.mergeCellManager.mergeCells(startRow, startCol, endRow, endCol);
}
// Method to set a custom font
// Delegate an den FontManager
setCustomFont(font: CustomFont): void {
if (!this.isValidBase64(font.base64)) {
throw new Error('Invalid Base64 data');
}
this.customFont = font;
this.fontManager.setCustomFont(font);
}
// Method to read the content of a cell
// Data access delegates
getCell(row: number, col: number): string {
if (row < this.options.rows && col < this.options.columns) {
return this.data[row][col];
}
throw new Error('Invalid cell coordinates');
return this.dataManager.getCell(row, col);
}
// Method to read the style of a cell
getCellStyle(row: number, col: number): TableCellStyle {
if (row < this.options.rows && col < this.options.columns) {
return this.cellStyles[row][col];
}
throw new Error('Invalid cell coordinates');
return this.dataManager.getCellStyle(row, col);
}
// Method to remove a cell
removeCell(row: number, col: number): void {
if (row < this.options.rows && col < this.options.columns) {
this.data[row][col] = '';
this.cellStyles[row][col] = {};
} else {
throw new Error('Invalid cell coordinates');
}
this.dataManager.removeCell(row, col);
}
// Method to add a new row
addRow(): void {
this.options.rows += 1;
this.data.push(Array(this.options.columns).fill(''));
this.cellStyles.push(Array(this.options.columns).fill({}));
this.dataManager.addRow();
}
// Method to add a new column
addColumn(): void {
this.options.columns += 1;
this.data.forEach((row) => row.push(''));
this.cellStyles.forEach((row) => row.push({}));
this.dataManager.addColumn();
}
// Method to remove a row
removeRow(row: number): void {
if (row < this.options.rows) {
this.data.splice(row, 1);
this.cellStyles.splice(row, 1);
this.options.rows -= 1;
} else {
throw new Error('Invalid row coordinate');
}
this.dataManager.removeRow(row);
}
// Method to remove a column
removeColumn(col: number): void {
if (col < this.options.columns) {
this.data.forEach((row) => row.splice(col, 1));
this.cellStyles.forEach((row) => row.splice(col, 1));
this.options.columns -= 1;
} else {
throw new Error('Invalid column coordinate');
}
this.dataManager.removeColumn(col);
}
// Helper function to validate Base64 data
private isValidBase64(base64: string): boolean {
const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
return base64Regex.test(base64);
}
// Helper function to convert Base64 to Uint8Array
private base64ToUint8Array(base64: string): Uint8Array {
if (!this.isValidBase64(base64)) {
throw new Error('Invalid Base64 data');
}
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
// New method: normalize color values
private normalizeColor(color: { r: number; g: number; b: number }): {
r: number;
g: number;
b: number;
} {
return {
r: color.r > 1 ? color.r / 255 : color.r,
g: color.g > 1 ? color.g / 255 : color.g,
b: color.b > 1 ? color.b / 255 : color.b,
};
}
// Create a PDF document with the table including cell styling
// Rendering methods
async toPDF(): Promise<PDFDocument> {
const pdfDoc = await PDFDocument.create();
let pdfFont; // Will be set either by CustomFont or as a fallback
if (this.customFont) {
const fontData = this.base64ToUint8Array(this.customFont.base64);
pdfFont = await pdfDoc.embedFont(fontData, { customName: this.customFont.name });
} else {
// Fallback to a standard font
pdfFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
}
let page = pdfDoc.addPage();
const { height } = page.getSize();
const pdfFont = await this.fontManager.embedFont(pdfDoc);
const opts = this.dataManager.getOptions();
const tableOptions = {
rowHeight: opts.rowHeight ?? 20,
colWidth: opts.colWidth ?? 80,
rows: opts.rows,
columns: opts.columns,
};
// Start position for the table
const startX = 50;
let currentY = height - 50;
const { rowHeight = 20, colWidth = 80 } = this.options;
await this.tableRenderer.drawTable(
pdfDoc,
pdfFont,
this.dataManager.getData(),
this.dataManager.getCellStyles(),
this.mergeCellManager.getMergedCells(),
tableOptions,
);
// Iterate over each row and column
for (let row = 0; row < this.options.rows; row++) {
// Create a new page if there is not enough space
if (currentY - rowHeight < 50) {
page = pdfDoc.addPage();
currentY = page.getSize().height - 50;
}
let x = startX;
for (let col = 0; col < this.options.columns; col++) {
// Check if this cell is part of a merged cell
const merged = this.mergedCells.find((mc) => mc.startRow === row && mc.startCol === col);
// If merged, calculate total height and width
let cellWidth = colWidth;
let cellHeight = rowHeight;
if (merged) {
cellWidth = colWidth * (merged.endCol - merged.startCol + 1);
cellHeight = rowHeight * (merged.endRow - merged.startRow + 1);
}
// Merge the individual cell style with the design defaults
const style = { ...this.designConfig, ...this.cellStyles[row][col] };
// Draw background, text, border, etc. only for non-skipped cells
if (!merged || (merged && row === merged.startRow && col === merged.startCol)) {
if (style.backgroundColor) {
const bg = this.normalizeColor(style.backgroundColor);
page.drawRectangle({
x,
y: currentY - cellHeight,
width: cellWidth,
height: cellHeight,
color: rgb(bg.r, bg.g, bg.b),
});
}
const fontSize = style.fontSize || 12;
const textColor = style.fontColor || { r: 0, g: 0, b: 0 };
const normTextColor = this.normalizeColor(textColor);
const text = this.data[row][col];
// Calculate text width if possible
let textWidth = text.length * fontSize * 0.6;
if (pdfFont.widthOfTextAtSize) {
textWidth = pdfFont.widthOfTextAtSize(text, fontSize);
}
// Determine the x value based on alignment
let textX = x + 5;
if (style.alignment === 'center') {
textX = x + (cellWidth - textWidth) / 2;
} else if (style.alignment === 'right') {
textX = x + cellWidth - textWidth - 5;
}
// If a CustomFont is available, it will be used
page.drawText(text, {
x: textX,
y: currentY - cellHeight + (cellHeight - fontSize) / 2,
size: fontSize,
color: rgb(normTextColor.r, normTextColor.g, normTextColor.b),
font: pdfFont, // undefined if no CustomFont is set
});
if (style.borderColor && style.borderWidth) {
const normBorderColor = this.normalizeColor(style.borderColor);
page.drawRectangle({
x,
y: currentY - cellHeight,
width: cellWidth,
height: cellHeight,
borderColor: rgb(normBorderColor.r, normBorderColor.g, normBorderColor.b),
borderWidth: style.borderWidth,
opacity: 0,
});
}
}
x += colWidth;
}
currentY -= rowHeight;
}
return pdfDoc;
}
// New method: Embed table in an existing PDF document (as a real table)
// PDF embedding
async embedInPDF(existingDoc: PDFDocument, startX: number, startY: number): Promise<PDFDocument> {

@@ -280,11 +132,11 @@ if (startX < 0 || startY < 0) {

}
// For simplicity, use a new page addition
let page = existingDoc.addPage();
let currentY = startY; // Use the passed Y coordinate
const rowHeight = this.options.rowHeight || 20;
const colWidth = this.options.colWidth || 80;
let currentY = startY;
const options = this.dataManager.getOptions();
const rowHeight = options.rowHeight || 20;
const colWidth = options.colWidth || 80;
const pdfFont = await existingDoc.embedFont(StandardFonts.Helvetica);
for (let row = 0; row < this.options.rows; row++) {
// If there is not enough space, add a new page and restore the top margin.
for (let row = 0; row < options.rows; row++) {
if (currentY - rowHeight < 50) {

@@ -294,6 +146,5 @@ page = existingDoc.addPage();

}
let x = startX; // Use the passed X coordinate
for (let col = 0; col < this.options.columns; col++) {
// Draw cell contents – additional styles can be integrated here.
const text = this.data[row][col];
let x = startX;
for (let col = 0; col < options.columns; col++) {
const text = this.dataManager.getCell(row, col);
page.drawText(text, {

@@ -310,6 +161,7 @@ x: x + 5,

}
return existingDoc;
}
// Angepasste embedTableAsImage-Methode: Fehlerbehandlung für ungültige Bilddaten
// Delegate an den ImageEmbedder
async embedTableAsImage(

@@ -320,41 +172,4 @@ existingDoc: PDFDocument,

): Promise<PDFDocument> {
if (!(imageBytes instanceof Uint8Array) || imageBytes.length === 0) {
throw new Error('Invalid image data');
}
// Neue Validierung: PNG-Header prüfen
const PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
if (imageBytes.length < 8 || !PNG_SIGNATURE.every((b, i) => imageBytes[i] === b)) {
throw new Error('Invalid image data');
}
let pngImage;
try {
pngImage = await existingDoc.embedPng(imageBytes);
} catch (error) {
throw new Error('Invalid image data');
}
const page = existingDoc.addPage();
page.drawImage(pngImage, {
x: options.x,
y: options.y,
width: options.width,
height: options.height,
});
return existingDoc;
return this.imageEmbedder.embedTableAsImage(existingDoc, imageBytes, options);
}
private getEffectiveCellStyle(
row: number,
col: number,
userStyle: TableCellStyle,
): TableCellStyle {
let effectiveStyle: TableCellStyle = { ...userStyle };
if (row === 0 && this.designConfig.headingRowStyle) {
effectiveStyle = { ...this.designConfig.headingRowStyle, ...effectiveStyle };
}
if (col === 0 && this.designConfig.headingColumnStyle) {
effectiveStyle = { ...this.designConfig.headingColumnStyle, ...effectiveStyle };
}
return effectiveStyle;
}
}

@@ -12,19 +12,7 @@ /**

* @property {Partial<DesignConfig>} [headingColumnStyle] - Heading column style
* @default
* {
* fontFamily: 'Helvetica, Arial, sans-serif',
* fontSize: 12,
* fontColor: { r: 0, g: 0, b: 0 },
* backgroundColor: { r: 255, g: 255, b: 255 },
* borderColor: { r: 200, g: 200, b: 200 },
* borderWidth: 1,
* headingRowStyle: {
* backgroundColor: { r: 220, g: 220, b: 220 },
* fontSize: 13,
* },
* headingColumnStyle: {
* backgroundColor: { r: 240, g: 240, b: 240 },
* fontSize: 13,
* }
* }
* @property {BorderStyle} [defaultTopBorder] - Default top border style
* @property {BorderStyle} [defaultRightBorder] - Default right border style
* @property {BorderStyle} [defaultBottomBorder] - Default bottom border style
* @property {BorderStyle} [defaultLeftBorder] - Default left border style
* @property {AdditionalBorder[]} [additionalBorders] - Additional borders
*/

@@ -42,4 +30,17 @@ export interface DesignConfig {

headingColumnStyle?: Partial<DesignConfig>;
// New default border style options
defaultTopBorder?: BorderStyle;
defaultRightBorder?: BorderStyle;
defaultBottomBorder?: BorderStyle;
defaultLeftBorder?: BorderStyle;
// New option for additional borders (e.g., for invoices)
additionalBorders?: AdditionalBorder[];
}
// Import BorderStyle and AdditionalBorder definitions
import { BorderStyle } from '../interfaces/TableCellStyle';
import { AdditionalBorder } from '../interfaces/AdditionalBorder';
/**

@@ -84,2 +85,29 @@ * Default design config

},
// Default border configurations
defaultTopBorder: {
display: true,
color: { r: 200, g: 200, b: 200 },
width: 1,
style: 'solid',
},
defaultRightBorder: {
display: true,
color: { r: 200, g: 200, b: 200 },
width: 1,
style: 'solid',
},
defaultBottomBorder: {
display: true,
color: { r: 200, g: 200, b: 200 },
width: 1,
style: 'solid',
},
defaultLeftBorder: {
display: true,
color: { r: 200, g: 200, b: 200 },
width: 1,
style: 'solid',
},
// Default: no additional borders
additionalBorders: [],
};

@@ -86,0 +114,0 @@

/**
* Border style definition for table cells
* @interface BorderStyle
* @property {boolean} [display] - Whether to display this border
* @property {{ r: number; g: number; b: number }} [color] - Border color
* @property {number} [width] - Border width
* @property {'solid' | 'dashed' | 'dotted'} [style] - Border style
* @property {number} [dashArray] - Custom dash array for custom border patterns (for 'dashed' style)
* @property {number} [dashPhase] - Dash phase for custom border patterns
*/
export interface BorderStyle {
display?: boolean;
color?: { r: number; g: number; b: number };
width?: number;
style?: 'solid' | 'dashed' | 'dotted';
dashArray?: number[];
dashPhase?: number;
}
import { AdditionalBorder } from './AdditionalBorder';
/**
* Table cell style

@@ -7,5 +28,10 @@ * @interface TableCellStyle

* @property {{ r: number; g: number; b: number }} [backgroundColor] - Background color
* @property {{ r: number; g: number; b: number }} [borderColor] - Border color
* @property {number} [borderWidth] - Border width
* @property {{ r: number; g: number; b: number }} [borderColor] - Border color (legacy, applies to all borders)
* @property {number} [borderWidth] - Border width (legacy, applies to all borders)
* @property {'left' | 'center' | 'right'} [alignment] - Text alignment
* @property {BorderStyle} [topBorder] - Top border style
* @property {BorderStyle} [rightBorder] - Right border style
* @property {BorderStyle} [bottomBorder] - Bottom border style
* @property {BorderStyle} [leftBorder] - Left border style
* @property {AdditionalBorder[]} [additionalBorders] - Neue Option für zusätzliche interne Rahmenlinien
*/

@@ -19,2 +45,11 @@ export interface TableCellStyle {

alignment?: 'left' | 'center' | 'right';
// Individual border styling
topBorder?: BorderStyle;
rightBorder?: BorderStyle;
bottomBorder?: BorderStyle;
leftBorder?: BorderStyle;
// Neue Option für zusätzliche interne Rahmenlinien
additionalBorders?: AdditionalBorder[];
}

@@ -0,1 +1,3 @@

import { isValidBase64 } from '../utils/validateBase64';
/**

@@ -13,7 +15,6 @@ * Custom font model

constructor(public name: string, public base64: string, public extension?: string) {
const base64Regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
if (!base64Regex.test(base64)) {
throw new Error('Invalid Base64 data');
if (!isValidBase64(base64)) {
throw new Error(`Invalid Base64 data for font "${name}"`);
}
}
}
import { CustomFont } from '../src/models/CustomFont';
describe('CustomFont', () => {
test('sollte Instanz mit Name, Base64 und optionaler Extension erstellen', () => {
test('should create an instance with name, base64, and optional extension', () => {
const name = 'TestFont';

@@ -15,3 +15,3 @@ const base64 = 'dGVzdGJhc2U2NA==';

test('sollte auch ohne Extension instanziiert werden können', () => {
test('should be instantiated without extension', () => {
const name = 'TestFont';

@@ -26,5 +26,5 @@ const base64 = 'dGVzdGJhc2U2NA==';

test('sollte Fehler für ungültige Base64-Daten werfen', () => {
test('should throw error for invalid Base64 data', () => {
expect(() => new CustomFont('TestFont', 'invalid_base64')).toThrowError('Invalid Base64 data');
});
});

@@ -0,0 +0,0 @@ import { PdfTable } from '../src/classes/Table';

import { PdfTable, CustomFont } from '../src/index';
describe('Index Exports', () => {
test('sollte PdfTable exportieren', () => {
test('should export PdfTable', () => {
expect(PdfTable).toBeDefined();
});
test('sollte CustomFont exportieren', () => {
test('should export CustomFont', () => {
expect(CustomFont).toBeDefined();
});
});

@@ -0,0 +0,0 @@ import { PdfTable } from '../src/classes/Table';

{
"compilerOptions": {
/* Language and Environment */
"target": "es6" /* Set the JavaScript language version for emitted JavaScr
"target": "es6" /* Set the JavaScript language version for emitted JavaScript. */,
/* Modules */,
/* Modules */
"module": "commonjs",

@@ -8,0 +8,0 @@ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,

Sorry, the diff of this file is not supported yet