Compare commits

...

9 Commits

9 changed files with 6863 additions and 2757 deletions

View File

@@ -1,5 +1,44 @@
# Changelog # Changelog
## 2025-08-01 - 3.3.0 - feat(smartpdf)
Add automatic port allocation and multi-instance support
- Added ISmartPdfOptions interface with port configuration options
- Implemented automatic port allocation between 20000-30000 by default
- Added support for custom port ranges via portRangeStart/portRangeEnd options
- Added support for specific port assignment via port option
- Fixed resource cleanup when port allocation fails
- Multiple SmartPdf instances can now run simultaneously without port conflicts
- Updated readme with comprehensive documentation for all features
## 2025-02-25 - 3.2.2 - fix(SmartPdf)
Fix buffer handling for PDF conversion and text extraction
- Ensure Uint8Array is converted to Node Buffer for PDF conversion.
- Correct the PDF page viewport handling by using document dimensions.
- Fix extractTextFromPdfBuffer argument type from Uint8Array to Buffer.
## 2025-02-25 - 3.2.1 - fix(SmartPdf)
Fix type for extractTextFromPdfBuffer function
- Corrected the parameter type from Buffer to Uint8Array for extractTextFromPdfBuffer function.
## 2025-02-25 - 3.2.0 - feat(smartpdf)
Improve dependency versions and optimize PDF to PNG conversion.
- Update several dependencies to newer versions for better stability and performance.
- Refactor tests to enhance readability and add directory creation validations.
- Optimize PDF to PNG conversion by switching to a more efficient Puppeteer and PDF.js-based method.
- Add checks for presence of required dependencies (GraphicsMagick and Ghostscript).
- Fix media emulation issue by properly awaiting the emulateMediaType function.
## 2024-11-30 - 3.1.8 - fix(core)
Fix candidate handling in PDF generation
- Added error handling for missing PDF candidates in server requests.
- Updated devDependencies and dependencies to latest versions for better stability and new features.
- Patched header retrieval logic during PDF generation for security check.
## 2024-09-27 - 3.1.7 - fix(dependencies) ## 2024-09-27 - 3.1.7 - fix(dependencies)
Update dependencies to latest versions Update dependencies to latest versions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartpdf", "name": "@push.rocks/smartpdf",
"version": "3.1.7", "version": "3.3.0",
"private": false, "private": false,
"description": "A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.", "description": "A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -9,33 +9,31 @@
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"test": "(tstest test/ --web)", "test": "(tstest test/ --verbose --timeout 60)",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild tsfolders --allowimplicitany)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.84", "@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsdoc": "^1.3.12", "@git.zone/tsdoc": "^1.5.0",
"@git.zone/tsrun": "^1.2.49", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.77", "@git.zone/tstest": "^2.3.2",
"@push.rocks/tapbundle": "^5.3.0", "@types/node": "^24.1.0"
"@types/node": "^22.7.4"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartbuffer": "^3.0.4", "@push.rocks/smartbuffer": "^3.0.5",
"@push.rocks/smartdelay": "^3.0.5", "@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^11.0.21", "@push.rocks/smartfile": "^11.2.5",
"@push.rocks/smartnetwork": "^3.0.0", "@push.rocks/smartnetwork": "^4.1.2",
"@push.rocks/smartpath": "^5.0.18", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.0.4", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartpuppeteer": "^2.0.2", "@push.rocks/smartpuppeteer": "^2.0.5",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^4.1.2", "@tsclass/tsclass": "^9.2.0",
"@types/express": "^4.17.21", "@types/express": "^5.0.3",
"express": "^4.21.0", "express": "^5.1.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdf2json": "3.0.5", "pdf2json": "3.2.0"
"pdf2pic": "^3.1.3"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@@ -70,5 +68,6 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://code.foss.global/push.rocks/smartpdf.git" "url": "https://code.foss.global/push.rocks/smartpdf.git"
} },
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
} }

8836
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

277
readme.md
View File

@@ -1,8 +1,8 @@
# @push.rocks/smartpdf # @push.rocks/smartpdf
Create PDFs on the fly Create PDFs on the fly from HTML, websites, or existing PDFs with advanced features like text extraction, PDF merging, and PNG conversion.
## Install ## Install
To install `@push.rocks/smartpdf`, use the following command with npm: To install `@push.rocks/smartpdf`, use npm or yarn:
```bash ```bash
npm install @push.rocks/smartpdf --save npm install @push.rocks/smartpdf --save
@@ -14,87 +14,304 @@ Or with yarn:
yarn add @push.rocks/smartpdf yarn add @push.rocks/smartpdf
``` ```
## Requirements
This package requires a Chrome or Chromium installation to be available on the system, as it uses Puppeteer for rendering. The package will automatically detect and use the appropriate executable.
## Usage ## Usage
This documentation will guide you through using `@push.rocks/smartpdf` to create PDFs in various ways, such as from HTML strings or full web pages, and provides examples on how to merge multiple PDFs into one. Remember, all examples provided here use ESM syntax and TypeScript. `@push.rocks/smartpdf` provides a powerful interface for PDF generation and manipulation. All examples use ESM syntax and TypeScript.
### Getting Started ### Getting Started
First, ensure you have the package installed and you can import it into your TypeScript project: First, import the necessary classes:
```typescript ```typescript
import { SmartPdf, IPdf } from '@push.rocks/smartpdf'; import { SmartPdf, IPdf } from '@push.rocks/smartpdf';
``` ```
### Creating a PDF from an HTML String ### Basic Setup with Automatic Port Allocation
To create a PDF from a simple HTML string, youll need to instantiate `SmartPdf` and call `getA4PdfResultForHtmlString`. SmartPdf automatically finds an available port between 20000-30000 for its internal server:
```typescript
async function setupSmartPdf() {
const smartPdf = await SmartPdf.create();
await smartPdf.start();
// Your PDF operations here
await smartPdf.stop();
}
```
### Advanced Setup with Custom Port Configuration
You can specify custom port settings to avoid conflicts or meet specific requirements:
```typescript
// Use a specific port
const smartPdf = await SmartPdf.create({ port: 3000 });
// Use a custom port range
const smartPdf = await SmartPdf.create({
portRangeStart: 4000,
portRangeEnd: 5000
});
// The server will find an available port in your specified range
await smartPdf.start();
console.log(`Server running on port: ${smartPdf.serverPort}`);
```
### Creating PDFs from HTML Strings
Generate PDFs from HTML content with full CSS support:
```typescript ```typescript
async function createPdfFromHtml() { async function createPdfFromHtml() {
const smartPdf = await SmartPdf.create(); const smartPdf = await SmartPdf.create();
await smartPdf.start(); await smartPdf.start();
const htmlString = `<h1>Hello World</h1>`;
const htmlString = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
.highlight { background-color: yellow; }
</style>
</head>
<body>
<h1>Professional PDF Document</h1>
<p>This PDF was generated from <span class="highlight">HTML content</span>.</p>
</body>
</html>
`;
const pdf: IPdf = await smartPdf.getA4PdfResultForHtmlString(htmlString); const pdf: IPdf = await smartPdf.getA4PdfResultForHtmlString(htmlString);
console.log(pdf.buffer); // This is your PDF buffer
// pdf.buffer contains the PDF data
// pdf.id contains a unique identifier
// pdf.name contains the filename
// pdf.metadata contains additional information like extracted text
await smartPdf.stop(); await smartPdf.stop();
} }
createPdfFromHtml();
``` ```
### Generating a PDF from a Website ### Generating PDFs from Websites
You may want to capture a full webpage as a PDF. `SmartPdf` provides two methods to accomplish this. One captures the viewable area as an A4 pdf, and the other captures the entire webpage. Capture web pages as PDFs with two different approaches:
#### A4 PDF from a Website #### A4 Format PDF from Website
Captures the viewable area formatted for A4 paper:
```typescript ```typescript
async function createA4PdfFromWebsite() { async function createA4PdfFromWebsite() {
const smartPdf = await SmartPdf.create(); const smartPdf = await SmartPdf.create();
await smartPdf.start(); await smartPdf.start();
const pdf: IPdf = await smartPdf.getPdfResultForWebsite('https://example.com'); const pdf: IPdf = await smartPdf.getPdfResultForWebsite('https://example.com');
console.log(pdf.buffer); // PDF buffer of the webpage
// Save to file
await fs.writeFile('website-a4.pdf', pdf.buffer);
await smartPdf.stop(); await smartPdf.stop();
} }
createA4PdfFromWebsite();
``` ```
#### Full Webpage as a Single PDF #### Full Webpage as Single PDF
Captures the entire webpage in a single PDF, regardless of length:
```typescript ```typescript
async function createFullPdfFromWebsite() { async function createFullPdfFromWebsite() {
const smartPdf = await SmartPdf.create(); const smartPdf = await SmartPdf.create();
await smartPdf.start(); await smartPdf.start();
const pdf: IPdf = await smartPdf.getFullWebsiteAsSinglePdf('https://example.com'); const pdf: IPdf = await smartPdf.getFullWebsiteAsSinglePdf('https://example.com');
console.log(pdf.buffer); // PDF buffer with the full webpage
// This captures the entire scrollable area
await fs.writeFile('website-full.pdf', pdf.buffer);
await smartPdf.stop(); await smartPdf.stop();
} }
createFullPdfFromWebsite();
``` ```
### Merging Multiple PDFs ### Merging Multiple PDFs
If you have multiple PDF objects (`IPdf`) that you wish to merge into a single PDF file, you can use the `mergePdfs` method. Combine multiple PDF files into a single document:
```typescript ```typescript
async function mergePdfs() { async function mergePdfs() {
const smartPdf = await SmartPdf.create(); const smartPdf = await SmartPdf.create();
// Assume pdf1 and pdf2 are objects of type IPdf that you want to merge await smartPdf.start();
const mergedPdf: IPdf = await smartPdf.mergePdfs([pdf1, pdf2]);
console.log(mergedPdf.buffer); // Buffer of the merged PDF // Create or load your PDFs
const pdf1 = await smartPdf.getA4PdfResultForHtmlString('<h1>Document 1</h1>');
const pdf2 = await smartPdf.getA4PdfResultForHtmlString('<h1>Document 2</h1>');
const pdf3 = await smartPdf.readFileToPdfObject('./existing-document.pdf');
// Merge PDFs - order matters!
const mergedPdf: Uint8Array = await smartPdf.mergePdfs([
pdf1.buffer,
pdf2.buffer,
pdf3.buffer
]);
// Save the merged PDF
await fs.writeFile('merged-document.pdf', mergedPdf);
await smartPdf.stop();
} }
mergePdfs();
``` ```
### Reading PDF from Disk and Extracting Text ### Reading PDFs and Extracting Text
To read a PDF from the disk and extract its text content: Extract text content from existing PDFs:
```typescript ```typescript
async function readAndExtractFromPdf() { async function extractTextFromPdf() {
const smartPdf = await SmartPdf.create(); const smartPdf = await SmartPdf.create();
const pdf: IPdf = await smartPdf.readFileToPdfObject('/path/to/your/pdf/file.pdf');
// Read PDF from disk
const pdf: IPdf = await smartPdf.readFileToPdfObject('/path/to/document.pdf');
// Extract all text
const extractedText = await smartPdf.extractTextFromPdfBuffer(pdf.buffer); const extractedText = await smartPdf.extractTextFromPdfBuffer(pdf.buffer);
console.log(extractedText); // Extracted text from the PDF console.log('Extracted text:', extractedText);
// The pdf object also contains metadata with text extraction
console.log('Metadata:', pdf.metadata);
} }
readAndExtractFromPdf();
``` ```
This guide provides a comprehensive overview of generating PDFs using `@push.rocks/smartpdf`. Remember to start and stop your `SmartPdf` instance to properly initialize and clean up resources, especially when working with server-side rendering or capturing web pages. ### Converting PDF to PNG Images
Convert each page of a PDF into PNG images:
```typescript
async function convertPdfToPng() {
const smartPdf = await SmartPdf.create();
await smartPdf.start();
// Load a PDF
const pdf = await smartPdf.readFileToPdfObject('./document.pdf');
// Convert to PNG images (one per page)
const pngImages: Uint8Array[] = await smartPdf.convertPDFToPngBytes(pdf.buffer);
// Save each page as a PNG
pngImages.forEach((pngBuffer, index) => {
fs.writeFileSync(`page-${index + 1}.png`, pngBuffer);
});
await smartPdf.stop();
}
```
### Using External Browser Instance
For advanced use cases, you can provide your own Puppeteer browser instance:
```typescript
import puppeteer from 'puppeteer';
async function useExternalBrowser() {
// Create your own browser instance with custom options
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const smartPdf = await SmartPdf.create();
await smartPdf.start(browser);
// Use SmartPdf normally
const pdf = await smartPdf.getA4PdfResultForHtmlString('<h1>Hello</h1>');
// SmartPdf will not close the browser when stopping
await smartPdf.stop();
// You control the browser lifecycle
await browser.close();
}
```
### Running Multiple Instances
Thanks to automatic port allocation, you can run multiple SmartPdf instances simultaneously:
```typescript
async function runMultipleInstances() {
// Each instance automatically finds its own free port
const instance1 = await SmartPdf.create();
const instance2 = await SmartPdf.create();
const instance3 = await SmartPdf.create();
// Start all instances
await Promise.all([
instance1.start(),
instance2.start(),
instance3.start()
]);
console.log(`Instance 1 running on port: ${instance1.serverPort}`);
console.log(`Instance 2 running on port: ${instance2.serverPort}`);
console.log(`Instance 3 running on port: ${instance3.serverPort}`);
// Use instances independently
const pdfs = await Promise.all([
instance1.getA4PdfResultForHtmlString('<h1>PDF 1</h1>'),
instance2.getA4PdfResultForHtmlString('<h1>PDF 2</h1>'),
instance3.getA4PdfResultForHtmlString('<h1>PDF 3</h1>')
]);
// Clean up all instances
await Promise.all([
instance1.stop(),
instance2.stop(),
instance3.stop()
]);
}
```
### Error Handling
Always wrap SmartPdf operations in try-catch blocks and ensure proper cleanup:
```typescript
async function safePdfGeneration() {
let smartPdf: SmartPdf;
try {
smartPdf = await SmartPdf.create();
await smartPdf.start();
const pdf = await smartPdf.getA4PdfResultForHtmlString('<h1>Hello</h1>');
// Process PDF...
} catch (error) {
console.error('PDF generation failed:', error);
// Handle error appropriately
} finally {
// Always cleanup
if (smartPdf) {
await smartPdf.stop();
}
}
}
```
### IPdf Interface
The `IPdf` interface represents a PDF with its metadata:
```typescript
interface IPdf {
name: string; // Filename of the PDF
buffer: Buffer; // PDF content as buffer
id: string | null; // Unique identifier
metadata?: {
textExtraction?: string; // Extracted text content
};
}
```
## Best Practices
1. **Always start and stop**: Initialize with `start()` and cleanup with `stop()` to properly manage resources.
2. **Port management**: Use the automatic port allocation feature to avoid conflicts when running multiple instances.
3. **Error handling**: Always implement proper error handling as PDF generation can fail due to various reasons.
4. **Resource cleanup**: Ensure `stop()` is called even if an error occurs to prevent memory leaks.
5. **HTML optimization**: When creating PDFs from HTML, ensure your HTML is well-formed and CSS is embedded or inlined.
## License and Legal Information ## License and Legal Information
@@ -113,4 +330,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc. For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

97
test/test.port.ts Normal file
View File

@@ -0,0 +1,97 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartpdf from '../ts/index.js';
tap.test('should create multiple SmartPdf instances with automatic port allocation', async () => {
const instance1 = new smartpdf.SmartPdf();
const instance2 = new smartpdf.SmartPdf();
const instance3 = new smartpdf.SmartPdf();
// Start all instances
await instance1.start();
await instance2.start();
await instance3.start();
// Verify all instances have different ports
expect(instance1.serverPort).toBeGreaterThanOrEqual(20000);
expect(instance1.serverPort).toBeLessThanOrEqual(30000);
expect(instance2.serverPort).toBeGreaterThanOrEqual(20000);
expect(instance2.serverPort).toBeLessThanOrEqual(30000);
expect(instance3.serverPort).toBeGreaterThanOrEqual(20000);
expect(instance3.serverPort).toBeLessThanOrEqual(30000);
// Ensure all ports are different
expect(instance1.serverPort).not.toEqual(instance2.serverPort);
expect(instance1.serverPort).not.toEqual(instance3.serverPort);
expect(instance2.serverPort).not.toEqual(instance3.serverPort);
console.log(`Instance 1 port: ${instance1.serverPort}`);
console.log(`Instance 2 port: ${instance2.serverPort}`);
console.log(`Instance 3 port: ${instance3.serverPort}`);
// Test that all instances work correctly
const pdf1 = await instance1.getA4PdfResultForHtmlString('<h1>Instance 1</h1>');
const pdf2 = await instance2.getA4PdfResultForHtmlString('<h1>Instance 2</h1>');
const pdf3 = await instance3.getA4PdfResultForHtmlString('<h1>Instance 3</h1>');
expect(pdf1.buffer).toBeInstanceOf(Buffer);
expect(pdf2.buffer).toBeInstanceOf(Buffer);
expect(pdf3.buffer).toBeInstanceOf(Buffer);
// Clean up
await instance1.stop();
await instance2.stop();
await instance3.stop();
});
tap.test('should create SmartPdf instance with custom port range', async () => {
const customInstance = new smartpdf.SmartPdf({
portRangeStart: 25000,
portRangeEnd: 26000
});
await customInstance.start();
expect(customInstance.serverPort).toBeGreaterThanOrEqual(25000);
expect(customInstance.serverPort).toBeLessThanOrEqual(26000);
console.log(`Custom range instance port: ${customInstance.serverPort}`);
await customInstance.stop();
});
tap.test('should create SmartPdf instance with specific port', async () => {
const specificPortInstance = new smartpdf.SmartPdf({
port: 28888
});
await specificPortInstance.start();
expect(specificPortInstance.serverPort).toEqual(28888);
console.log(`Specific port instance: ${specificPortInstance.serverPort}`);
await specificPortInstance.stop();
});
tap.test('should throw error when specific port is already in use', async () => {
const instance1 = new smartpdf.SmartPdf({ port: 29999 });
await instance1.start();
const instance2 = new smartpdf.SmartPdf({ port: 29999 });
let errorThrown = false;
try {
await instance2.start();
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('already in use');
}
expect(errorThrown).toBeTrue();
await instance1.stop();
});
export default tap.start();

View File

@@ -1,66 +1,85 @@
import { expect, tap } from '@push.rocks/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartpdf from '../ts/index.js'; import * as smartpdf from '../ts/index.js';
import * as fs from 'fs';
import * as path from 'path';
let testSmartPdf: smartpdf.SmartPdf; let testSmartPdf: smartpdf.SmartPdf;
tap.test('should create a valid instance of smartpdf', async () => { /**
* Ensures that a directory exists.
* @param dirPath - The directory path to ensure.
*/
function ensureDir(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
tap.test('should create a valid instance of SmartPdf', async () => {
testSmartPdf = new smartpdf.SmartPdf(); testSmartPdf = new smartpdf.SmartPdf();
expect(testSmartPdf).toBeInstanceOf(smartpdf.SmartPdf); expect(testSmartPdf).toBeInstanceOf(smartpdf.SmartPdf);
}); });
tap.test('should start the instance', async () => { tap.test('should start the SmartPdf instance', async () => {
await testSmartPdf.start(); await testSmartPdf.start();
}); });
tap.test('should create a pdf from html string', async () => { tap.test('should create PDFs from HTML string', async () => {
await testSmartPdf.getA4PdfResultForHtmlString('hi'); const pdf1 = await testSmartPdf.getA4PdfResultForHtmlString('hi');
const pdf2 = await testSmartPdf.getA4PdfResultForHtmlString('hello');
expect(pdf1.buffer).toBeInstanceOf(Buffer);
expect(pdf2.buffer).toBeInstanceOf(Buffer);
}); });
tap.test('should create a pdf from html string', async () => { tap.test('should create PDFs from websites', async () => {
await testSmartPdf.getA4PdfResultForHtmlString('hi'); const pdfA4 = await testSmartPdf.getPdfResultForWebsite('https://www.wikipedia.org');
const pdfSingle = await testSmartPdf.getFullWebsiteAsSinglePdf('https://www.wikipedia.org');
expect(pdfA4.buffer).toBeInstanceOf(Buffer);
expect(pdfSingle.buffer).toBeInstanceOf(Buffer);
}); });
tap.test('should create a pdf from website as A4', async () => { tap.test('should create valid PDF results and write them to disk', async () => {
await testSmartPdf.getPdfResultForWebsite('https://www.wikipedia.org'); const writePdfToDisk = async (urlArg: string, fileName: string) => {
});
tap.test('should create a pdf from website as single page PDF', async () => {
await testSmartPdf.getFullWebsiteAsSinglePdf('https://www.wikipedia.org');
});
tap.test('should create a valid PDFResult', async () => {
const writePDfToDisk = async (urlArg: string, fileName: string) => {
const pdfResult = await testSmartPdf.getFullWebsiteAsSinglePdf(urlArg); const pdfResult = await testSmartPdf.getFullWebsiteAsSinglePdf(urlArg);
expect(pdfResult.buffer).toBeInstanceOf(Buffer); expect(pdfResult.buffer).toBeInstanceOf(Buffer);
const fs = await import('fs'); ensureDir('.nogit');
fs.writeFileSync(path.join('.nogit', fileName), pdfResult.buffer as Buffer);
if (!fs.existsSync('.nogit/')) {
fs.mkdirSync('.nogit/');
}
fs.writeFileSync(`.nogit/${fileName}`, pdfResult.buffer as Buffer);
}; };
await writePDfToDisk('https://lossless.com/', '1.pdf'); await writePdfToDisk('https://lossless.com/', '1.pdf');
await writePDfToDisk('https://layer.io', '2.pdf'); await writePdfToDisk('https://layer.io', '2.pdf');
}); });
tap.test('should merge pdfs', async () => { tap.test('should merge PDFs into a combined PDF', async () => {
const fs = await import('fs');
const pdf1 = await testSmartPdf.readFileToPdfObject('.nogit/1.pdf'); const pdf1 = await testSmartPdf.readFileToPdfObject('.nogit/1.pdf');
const pdf2 = await testSmartPdf.readFileToPdfObject('.nogit/2.pdf'); const pdf2 = await testSmartPdf.readFileToPdfObject('.nogit/2.pdf');
fs.writeFileSync( const mergedBuffer = await testSmartPdf.mergePdfs([pdf1.buffer, pdf2.buffer]);
`.nogit/combined.pdf`, ensureDir('.nogit');
await testSmartPdf.mergePdfs([pdf1.buffer, pdf2.buffer]) fs.writeFileSync(path.join('.nogit', 'combined.pdf'), mergedBuffer);
);
}); });
tap.test('should create images from an pdf', async () => { tap.test('should create PNG images from combined PDF using Puppeteer conversion', async () => {
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/combined.pdf'); const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/combined.pdf');
const images = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer); const images = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer);
console.log(images.map((val) => val.length)); expect(images.length).toBeGreaterThan(0);
console.log('Puppeteer-based conversion image sizes:', images.map(img => img.length));
}); });
tap.test('should be able to close properly', async () => { tap.test('should store PNG results from both conversion functions in .nogit/testresults', async () => {
const testResultsDir = path.join('.nogit', 'testresults');
ensureDir(testResultsDir);
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/combined.pdf');
// Convert using Puppeteer-based function and store images
const imagesPuppeteer = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer);
imagesPuppeteer.forEach((img, index) => {
const filePath = path.join(testResultsDir, `puppeteer_method_page_${index + 1}.png`);
fs.writeFileSync(filePath, Buffer.from(img));
});
});
tap.test('should close the SmartPdf instance properly', async () => {
await testSmartPdf.stop(); await testSmartPdf.stop();
}); });
tap.start(); tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartpdf', name: '@push.rocks/smartpdf',
version: '3.1.7', version: '3.2.2',
description: 'A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.' description: 'A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.'
} }

View File

@@ -3,13 +3,20 @@ import * as paths from './smartpdf.paths.js';
import { Server } from 'http'; import { Server } from 'http';
import { PdfCandidate } from './smartpdf.classes.pdfcandidate.js'; import { PdfCandidate } from './smartpdf.classes.pdfcandidate.js';
import { type IPdf } from '@tsclass/tsclass/dist_ts/business/pdf.js'; import { type IPdf } from '@tsclass/tsclass/dist_ts/business/pdf.js';
import { execFile } from 'child_process';
declare const document: any; declare const document: any;
export interface ISmartPdfOptions {
port?: number;
portRangeStart?: number;
portRangeEnd?: number;
}
export class SmartPdf { export class SmartPdf {
// STATIC // STATIC
public static async create() { public static async create(optionsArg?: ISmartPdfOptions) {
const smartpdfInstance = new SmartPdf(); const smartpdfInstance = new SmartPdf(optionsArg);
return smartpdfInstance; return smartpdfInstance;
} }
@@ -20,9 +27,15 @@ export class SmartPdf {
externalBrowserBool: boolean = false; externalBrowserBool: boolean = false;
private _readyDeferred: plugins.smartpromise.Deferred<void>; private _readyDeferred: plugins.smartpromise.Deferred<void>;
private _candidates: { [key: string]: PdfCandidate } = {}; private _candidates: { [key: string]: PdfCandidate } = {};
private _options: ISmartPdfOptions;
constructor() { constructor(optionsArg?: ISmartPdfOptions) {
this._readyDeferred = new plugins.smartpromise.Deferred(); this._readyDeferred = new plugins.smartpromise.Deferred();
this._options = {
portRangeStart: 20000,
portRangeEnd: 30000,
...optionsArg
};
} }
async start(headlessBrowserArg?: plugins.smartpuppeteer.puppeteer.Browser) { async start(headlessBrowserArg?: plugins.smartpuppeteer.puppeteer.Browser) {
@@ -34,21 +47,56 @@ export class SmartPdf {
this.externalBrowserBool = true; this.externalBrowserBool = true;
} else { } else {
this.headlessBrowser = await plugins.smartpuppeteer.getEnvAwareBrowserInstance({ this.headlessBrowser = await plugins.smartpuppeteer.getEnvAwareBrowserInstance({
forceNoSandbox: true, forceNoSandbox: false,
}); });
} }
// setup server // Find an available port BEFORE creating server
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
if (this._options.port) {
// If a specific port is requested, check if it's available
const isPortAvailable = await smartnetworkInstance.isLocalPortUnused(this._options.port);
if (isPortAvailable) {
this.serverPort = this._options.port;
} else {
// Clean up browser if we created one
if (!this.externalBrowserBool && this.headlessBrowser) {
await this.headlessBrowser.close();
}
throw new Error(`Requested port ${this._options.port} is already in use`);
}
} else {
// Find a free port in the specified range
this.serverPort = await smartnetworkInstance.findFreePort(
this._options.portRangeStart,
this._options.portRangeEnd
);
if (!this.serverPort) {
// Clean up browser if we created one
if (!this.externalBrowserBool && this.headlessBrowser) {
await this.headlessBrowser.close();
}
throw new Error(`No free ports available in range ${this._options.portRangeStart}-${this._options.portRangeEnd}`);
}
}
// Now setup server after we know we have a valid port
const app = plugins.express(); const app = plugins.express();
app.get('/:pdfId', (req, res) => { app.get('/:pdfId', (req, res) => {
res.setHeader('PDF-ID', this._candidates[req.params.pdfId].pdfId); const wantedCandidate = this._candidates[req.params.pdfId];
res.send(this._candidates[req.params.pdfId].htmlString); if (!wantedCandidate) {
console.log(`${req.url} not attached to a candidate`);
return;
}
res.setHeader('pdf-id', wantedCandidate.pdfId);
res.send(wantedCandidate.htmlString);
}); });
this.htmlServerInstance = plugins.http.createServer(app); this.htmlServerInstance = plugins.http.createServer(app);
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
const portAvailable = smartnetworkInstance.isLocalPortUnused(3210); this.htmlServerInstance.listen(this.serverPort, 'localhost');
this.htmlServerInstance.listen(3210, 'localhost');
this.htmlServerInstance.on('listening', () => { this.htmlServerInstance.on('listening', () => {
console.log(`SmartPdf server listening on port ${this.serverPort}`);
this._readyDeferred.resolve(); this._readyDeferred.resolve();
done.resolve(); done.resolve();
}); });
@@ -70,7 +118,7 @@ export class SmartPdf {
} }
/** /**
* returns a pdf for a given html string; * Returns a PDF for a given HTML string.
*/ */
async getA4PdfResultForHtmlString(htmlStringArg: string): Promise<plugins.tsclass.business.IPdf> { async getA4PdfResultForHtmlString(htmlStringArg: string): Promise<plugins.tsclass.business.IPdf> {
await this._readyDeferred.promise; await this._readyDeferred.promise;
@@ -81,10 +129,9 @@ export class SmartPdf {
width: 794, width: 794,
height: 1122, height: 1122,
}); });
const response = await page.goto(`http://localhost:3210/${pdfCandidate.pdfId}`, { const response = await page.goto(`http://localhost:${this.serverPort}/${pdfCandidate.pdfId}`, {
waitUntil: 'networkidle2', waitUntil: 'networkidle2',
}); });
// await plugins.smartdelay.delayFor(1000);
const headers = response.headers(); const headers = response.headers();
if (headers['pdf-id'] !== pdfCandidate.pdfId) { if (headers['pdf-id'] !== pdfCandidate.pdfId) {
console.log('Error! Headers do not match. For security reasons no pdf is being emitted!'); console.log('Error! Headers do not match. For security reasons no pdf is being emitted!');
@@ -99,6 +146,8 @@ export class SmartPdf {
printBackground: true, printBackground: true,
displayHeaderFooter: false, displayHeaderFooter: false,
}); });
// Convert Uint8Array to Node Buffer
const nodePdfBuffer = Buffer.from(pdfBuffer);
await page.close(); await page.close();
delete this._candidates[pdfCandidate.pdfId]; delete this._candidates[pdfCandidate.pdfId];
pdfCandidate.doneDeferred.resolve(); pdfCandidate.doneDeferred.resolve();
@@ -107,9 +156,9 @@ export class SmartPdf {
id: pdfCandidate.pdfId, id: pdfCandidate.pdfId,
name: `${pdfCandidate.pdfId}.js`, name: `${pdfCandidate.pdfId}.js`,
metadata: { metadata: {
textExtraction: await this.extractTextFromPdfBuffer(pdfBuffer), textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
}, },
buffer: pdfBuffer, buffer: nodePdfBuffer,
}; };
} }
@@ -134,14 +183,16 @@ export class SmartPdf {
printBackground: true, printBackground: true,
displayHeaderFooter: false, displayHeaderFooter: false,
}); });
// Convert Uint8Array to Node Buffer
const nodePdfBuffer = Buffer.from(pdfBuffer);
await page.close(); await page.close();
return { return {
id: pdfId, id: pdfId,
name: `${pdfId}.js`, name: `${pdfId}.js`,
metadata: { metadata: {
textExtraction: await this.extractTextFromPdfBuffer(pdfBuffer), textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
}, },
buffer: pdfBuffer, buffer: nodePdfBuffer,
}; };
} }
@@ -151,15 +202,23 @@ export class SmartPdf {
width: 1920, width: 1920,
height: 1200, height: 1200,
}); });
page.emulateMediaType('screen'); await page.emulateMediaType('screen');
const response = await page.goto(websiteUrl, { waitUntil: 'networkidle2' }); const response = await page.goto(websiteUrl, { waitUntil: 'networkidle2' });
const pdfId = plugins.smartunique.shortId(); const pdfId = plugins.smartunique.shortId();
// Use both document.body and document.documentElement to ensure we have a valid height and width.
const { documentHeight, documentWidth } = await page.evaluate(() => { const { documentHeight, documentWidth } = await page.evaluate(() => {
return { return {
documentHeight: document.body.scrollHeight, documentHeight: Math.max(
documentWidth: document.body.clientWidth, document.body.scrollHeight,
document.documentElement.scrollHeight
) || 1200,
documentWidth: Math.max(
document.body.clientWidth,
document.documentElement.clientWidth
) || 1920,
}; };
}); });
// Update viewport height to the full document height.
await page.setViewport({ await page.setViewport({
width: 1920, width: 1920,
height: documentHeight, height: documentHeight,
@@ -172,14 +231,16 @@ export class SmartPdf {
scale: 1, scale: 1,
pageRanges: '1', pageRanges: '1',
}); });
// Convert Uint8Array to Node Buffer
const nodePdfBuffer = Buffer.from(pdfBuffer);
await page.close(); await page.close();
return { return {
id: pdfId, id: pdfId,
name: `${pdfId}.js`, name: `${pdfId}.js`,
metadata: { metadata: {
textExtraction: await this.extractTextFromPdfBuffer(pdfBuffer), textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
}, },
buffer: pdfBuffer, buffer: nodePdfBuffer,
}; };
} }
@@ -196,9 +257,9 @@ export class SmartPdf {
} }
public async readFileToPdfObject(pathArg: string): Promise<plugins.tsclass.business.IPdf> { public async readFileToPdfObject(pathArg: string): Promise<plugins.tsclass.business.IPdf> {
const path = plugins.smartpath.transform.makeAbsolute(pathArg); const absolutePath = plugins.smartpath.transform.makeAbsolute(pathArg);
const parsedPath = plugins.path.parse(path); const parsedPath = plugins.path.parse(absolutePath);
const buffer = await plugins.smartfile.fs.toBuffer(path); const buffer = await plugins.smartfile.fs.toBuffer(absolutePath);
return { return {
name: parsedPath.base, name: parsedPath.base,
buffer, buffer,
@@ -225,40 +286,109 @@ export class SmartPdf {
return deferred.promise; return deferred.promise;
} }
/**
* Checks for the presence of required dependencies: GraphicsMagick and Ghostscript.
*/
private async checkDependencies(): Promise<void> {
await Promise.all([
this.checkCommandExists('gm', ['version']),
this.checkCommandExists('gs', ['--version']),
]);
}
/**
* Checks if a given command exists by trying to execute it.
*/
private checkCommandExists(command: string, args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Dependency check failed: ${command} is not installed or not in the PATH. ${error.message}`));
} else {
resolve();
}
});
});
}
/**
* Converts a PDF to PNG bytes for each page using Puppeteer and PDF.js.
* This method creates a temporary HTML page that loads PDF.js from a CDN,
* renders each PDF page to a canvas, and then screenshots each canvas element.
*/
public async convertPDFToPngBytes( public async convertPDFToPngBytes(
pdfBytes: Uint8Array, pdfBytes: Uint8Array,
options: { options: { width?: number; height?: number; quality?: number } = {}
width?: number; ): Promise<Uint8Array[]> {
height?: number; // Note: options.width, options.height, and options.quality are not applied here,
quality?: number; // as the rendered canvas size is determined by the PDF page dimensions.
} = {}
) {
const { width = 1024, height = 768, quality = 100 } = options;
// Load the PDF document // Create a new page using the headless browser.
const pdfDoc = await plugins.pdfLib.PDFDocument.load(pdfBytes); const page = await this.headlessBrowser.newPage();
const converter = plugins.pdf2pic.fromBuffer(Buffer.from(pdfBytes), { // Prepare PDF data as a base64 string.
density: 100, // Image density (DPI) const base64Pdf: string = Buffer.from(pdfBytes).toString('base64');
format: 'png', // Image format
width, // Output image width
height, // Output image height
quality, // Output image quality
});
// Get array promises that resolve to JPG buffers // HTML template that loads PDF.js and renders the PDF.
const imagePromises: Promise<Buffer>[] = []; const htmlTemplate: string = `
const numPages = pdfDoc.getPageCount(); <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>PDF to PNG Converter</title>
<style>
body { margin: 0; }
canvas { display: block; margin: 10px auto; }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
</head>
<body>
<script>
(async function() {
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
const pdfData = "__PDF_DATA__";
const raw = atob(pdfData);
const pdfArray = new Uint8Array([...raw].map(c => c.charCodeAt(0)));
const loadingTask = pdfjsLib.getDocument({data: pdfArray});
const pdf = await loadingTask.promise;
const numPages = pdf.numPages;
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale: 1.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport: viewport }).promise;
document.body.appendChild(canvas);
}
window.renderComplete = true;
})();
</script>
</body>
</html>
`;
for (let i = 0; i < numPages; i++) { // Replace the placeholder with the actual base64 PDF data.
imagePromises.push(converter(i + 1, { const htmlContent: string = htmlTemplate.replace("__PDF_DATA__", base64Pdf);
responseType: 'buffer',
}).then((output) => output.buffer)); // Set the page content.
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
// Wait until the PDF.js rendering is complete.
await page.waitForFunction(() => (window as any).renderComplete === true, { timeout: 30000 });
// Query all canvas elements (each representing a rendered PDF page).
const canvasElements = await page.$$('canvas');
const pngBuffers: Uint8Array[] = [];
for (const canvasElement of canvasElements) {
// Screenshot the canvas element. The screenshot will be a PNG buffer.
const screenshotBuffer = (await canvasElement.screenshot({ encoding: 'binary' })) as Buffer;
pngBuffers.push(new Uint8Array(screenshotBuffer));
} }
// Resolve all promises and return the array of buffers await page.close();
const imageBuffers = await Promise.all(imagePromises); return pngBuffers;
const imageUint8Arrays = imageBuffers.map((buffer) => buffer);
return imageUint8Arrays;
} }
} }

View File

@@ -33,7 +33,6 @@ export { tsclass };
// thirdparty // thirdparty
import express from 'express'; import express from 'express';
import pdf2json from 'pdf2json'; import pdf2json from 'pdf2json';
import pdf2pic from 'pdf2pic';
import pdfLib from 'pdf-lib'; import pdfLib from 'pdf-lib';
export { express, pdf2json, pdf2pic, pdfLib, }; export { express, pdf2json, pdfLib, };