Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6a4aeed3e1 | ||
|
a4c3415838 | ||
f535eacd97 | |||
9908897aa2 |
30
changelog.md
30
changelog.md
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-02 - 4.0.0 - BREAKING CHANGE(smartpdf)
|
||||
Improve image generation quality and API consistency
|
||||
|
||||
- BREAKING: Renamed `convertPDFToWebpPreviews` to `convertPDFToWebpBytes` for API consistency
|
||||
- Added configurable scale options to `convertPDFToPngBytes` method
|
||||
- Changed default scale from 1.0 to 3.0 for PNG generation (216 DPI)
|
||||
- Changed default scale from 0.5 to 3.0 for WebP generation (216 DPI)
|
||||
- Added DPI helper methods: `getScaleForDPI()` and scale constants (SCALE_SCREEN, SCALE_HIGH, SCALE_PRINT)
|
||||
- Added maxWidth/maxHeight constraints for both PNG and WebP generation
|
||||
- Improved test file organization with clear naming conventions
|
||||
- Updated documentation with DPI/scale guidance and examples
|
||||
|
||||
## 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
|
||||
|
||||
|
34
package.json
34
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartpdf",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.0",
|
||||
"private": false,
|
||||
"description": "A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -9,32 +9,31 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --web)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"test": "(tstest test/ --verbose --timeout 60)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.2.1",
|
||||
"@git.zone/tsdoc": "^1.4.3",
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsdoc": "^1.5.0",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^1.0.96",
|
||||
"@push.rocks/tapbundle": "^5.5.6",
|
||||
"@types/node": "^22.13.5"
|
||||
"@git.zone/tstest": "^2.3.2",
|
||||
"@types/node": "^24.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartbuffer": "^3.0.4",
|
||||
"@push.rocks/smartbuffer": "^3.0.5",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartfile": "^11.2.0",
|
||||
"@push.rocks/smartnetwork": "^3.0.0",
|
||||
"@push.rocks/smartpath": "^5.0.18",
|
||||
"@push.rocks/smartfile": "^11.2.5",
|
||||
"@push.rocks/smartnetwork": "^4.1.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartpuppeteer": "^2.0.5",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@tsclass/tsclass": "^4.4.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"express": "^4.21.2",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@types/express": "^5.0.3",
|
||||
"express": "^5.1.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdf2json": "3.1.5"
|
||||
"pdf2json": "3.2.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -69,5 +68,6 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartpdf.git"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
||||
}
|
||||
|
3799
pnpm-lock.yaml
generated
3799
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
380
readme.md
380
readme.md
@@ -1,8 +1,8 @@
|
||||
# @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
|
||||
To install `@push.rocks/smartpdf`, use the following command with npm:
|
||||
To install `@push.rocks/smartpdf`, use npm or yarn:
|
||||
|
||||
```bash
|
||||
npm install @push.rocks/smartpdf --save
|
||||
@@ -14,87 +14,407 @@ Or with yarn:
|
||||
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
|
||||
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
|
||||
First, ensure you have the package installed and you can import it into your TypeScript project:
|
||||
First, import the necessary classes:
|
||||
|
||||
```typescript
|
||||
import { SmartPdf, IPdf } from '@push.rocks/smartpdf';
|
||||
```
|
||||
|
||||
### Creating a PDF from an HTML String
|
||||
To create a PDF from a simple HTML string, you’ll need to instantiate `SmartPdf` and call `getA4PdfResultForHtmlString`.
|
||||
### Basic Setup with Automatic Port Allocation
|
||||
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
|
||||
async function createPdfFromHtml() {
|
||||
const smartPdf = await SmartPdf.create();
|
||||
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);
|
||||
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();
|
||||
}
|
||||
createPdfFromHtml();
|
||||
```
|
||||
|
||||
### Generating a PDF from a Website
|
||||
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.
|
||||
### Generating PDFs from Websites
|
||||
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
|
||||
async function createA4PdfFromWebsite() {
|
||||
const smartPdf = await SmartPdf.create();
|
||||
await smartPdf.start();
|
||||
|
||||
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();
|
||||
}
|
||||
createA4PdfFromWebsite();
|
||||
```
|
||||
|
||||
#### Full Webpage as a Single PDF
|
||||
#### Full Webpage as Single PDF
|
||||
Captures the entire webpage in a single PDF, regardless of length:
|
||||
|
||||
```typescript
|
||||
async function createFullPdfFromWebsite() {
|
||||
const smartPdf = await SmartPdf.create();
|
||||
await smartPdf.start();
|
||||
|
||||
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();
|
||||
}
|
||||
createFullPdfFromWebsite();
|
||||
```
|
||||
|
||||
### 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
|
||||
async function mergePdfs() {
|
||||
const smartPdf = await SmartPdf.create();
|
||||
// Assume pdf1 and pdf2 are objects of type IPdf that you want to merge
|
||||
const mergedPdf: IPdf = await smartPdf.mergePdfs([pdf1, pdf2]);
|
||||
console.log(mergedPdf.buffer); // Buffer of the merged PDF
|
||||
await smartPdf.start();
|
||||
|
||||
// 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
|
||||
To read a PDF from the disk and extract its text content:
|
||||
### Reading PDFs and Extracting Text
|
||||
Extract text content from existing PDFs:
|
||||
|
||||
```typescript
|
||||
async function readAndExtractFromPdf() {
|
||||
async function extractTextFromPdf() {
|
||||
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);
|
||||
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 with configurable quality:
|
||||
|
||||
```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 with default high quality (216 DPI)
|
||||
const pngImages: Uint8Array[] = await smartPdf.convertPDFToPngBytes(pdf.buffer);
|
||||
|
||||
// Or specify custom scale/DPI
|
||||
const highResPngs = await smartPdf.convertPDFToPngBytes(pdf.buffer, {
|
||||
scale: SmartPdf.SCALE_PRINT, // 6.0 scale = ~432 DPI
|
||||
maxWidth: 3000, // Optional: limit maximum width
|
||||
maxHeight: 4000 // Optional: limit maximum height
|
||||
});
|
||||
|
||||
// Save each page as a PNG
|
||||
pngImages.forEach((pngBuffer, index) => {
|
||||
fs.writeFileSync(`page-${index + 1}.png`, pngBuffer);
|
||||
});
|
||||
|
||||
await smartPdf.stop();
|
||||
}
|
||||
```
|
||||
|
||||
#### Understanding Scale and DPI
|
||||
PDF.js renders at 72 DPI by default. Use these scale factors for different quality levels:
|
||||
- `SmartPdf.SCALE_SCREEN` (2.0): ~144 DPI - Good for screen display
|
||||
- `SmartPdf.SCALE_HIGH` (3.0): ~216 DPI - High quality (default)
|
||||
- `SmartPdf.SCALE_PRINT` (6.0): ~432 DPI - Print quality
|
||||
- Custom DPI: `scale = SmartPdf.getScaleForDPI(300)` for 300 DPI
|
||||
|
||||
### Converting PDF to WebP Images
|
||||
Generate web-optimized images using WebP format. WebP provides 25-35% better compression than PNG/JPEG while maintaining quality:
|
||||
|
||||
```typescript
|
||||
async function createWebPImages() {
|
||||
const smartPdf = await SmartPdf.create();
|
||||
await smartPdf.start();
|
||||
|
||||
// Load a PDF
|
||||
const pdf = await smartPdf.readFileToPdfObject('./document.pdf');
|
||||
|
||||
// Create high-quality WebP images (default: 3.0 scale = 216 DPI, 85% quality)
|
||||
const webpImages = await smartPdf.convertPDFToWebpBytes(pdf.buffer);
|
||||
|
||||
// Save WebP images
|
||||
webpImages.forEach((webpBuffer, index) => {
|
||||
fs.writeFileSync(`page-${index + 1}.webp`, webpBuffer);
|
||||
});
|
||||
|
||||
await smartPdf.stop();
|
||||
}
|
||||
```
|
||||
|
||||
#### Creating Thumbnails
|
||||
Generate small thumbnail images for PDF galleries or document lists:
|
||||
|
||||
```typescript
|
||||
async function createThumbnails() {
|
||||
const smartPdf = await SmartPdf.create();
|
||||
await smartPdf.start();
|
||||
|
||||
const pdf = await smartPdf.readFileToPdfObject('./document.pdf');
|
||||
|
||||
// Create small thumbnails (0.5 scale = ~36 DPI, 70% quality)
|
||||
const thumbnails = await smartPdf.convertPDFToWebpBytes(pdf.buffer, {
|
||||
scale: 0.5, // Small readable thumbnails
|
||||
quality: 70 // Lower quality for smaller files
|
||||
});
|
||||
|
||||
// Save thumbnails
|
||||
thumbnails.forEach((thumb, index) => {
|
||||
fs.writeFileSync(`thumb-${index + 1}.webp`, thumb);
|
||||
});
|
||||
|
||||
await smartPdf.stop();
|
||||
}
|
||||
```
|
||||
|
||||
#### Constrained Dimensions
|
||||
Create previews with maximum width/height constraints, useful for responsive layouts:
|
||||
|
||||
```typescript
|
||||
async function createConstrainedPreviews() {
|
||||
const smartPdf = await SmartPdf.create();
|
||||
await smartPdf.start();
|
||||
|
||||
const pdf = await smartPdf.readFileToPdfObject('./document.pdf');
|
||||
|
||||
// Create previews that fit within 800x600 pixels
|
||||
const previews = await smartPdf.convertPDFToWebpBytes(pdf.buffer, {
|
||||
scale: 1.0, // Start with full size
|
||||
quality: 90, // High quality
|
||||
maxWidth: 800, // Maximum 800px wide
|
||||
maxHeight: 600 // Maximum 600px tall
|
||||
});
|
||||
|
||||
// The method automatically scales down to fit within constraints
|
||||
previews.forEach((preview, index) => {
|
||||
fs.writeFileSync(`preview-constrained-${index + 1}.webp`, preview);
|
||||
});
|
||||
|
||||
await smartPdf.stop();
|
||||
}
|
||||
```
|
||||
|
||||
#### WebP Options
|
||||
The `convertPDFToWebpBytes` method accepts these options:
|
||||
|
||||
- `scale`: Scale factor for preview size (default: 3.0 for ~216 DPI)
|
||||
- `quality`: WebP compression quality (default: 85, range: 0-100)
|
||||
- `maxWidth`: Maximum width in pixels (optional)
|
||||
- `maxHeight`: Maximum height in pixels (optional)
|
||||
|
||||
Common scale values:
|
||||
- `0.5`: Thumbnails (~36 DPI)
|
||||
- `2.0`: Screen display (~144 DPI)
|
||||
- `3.0`: High quality (~216 DPI, default)
|
||||
- `6.0`: Print quality (~432 DPI)
|
||||
|
||||
### 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
|
||||
|
||||
@@ -113,4 +433,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.
|
||||
|
||||
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
97
test/test.port.ts
Normal 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();
|
106
test/test.ts
106
test/test.ts
@@ -1,4 +1,4 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartpdf from '../ts/index.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@@ -15,6 +15,13 @@ function ensureDir(dirPath: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Clean test results directory at start
|
||||
const testResultsDir = path.join('.nogit', 'testresults');
|
||||
if (fs.existsSync(testResultsDir)) {
|
||||
fs.rmSync(testResultsDir, { recursive: true, force: true });
|
||||
}
|
||||
ensureDir(testResultsDir);
|
||||
|
||||
tap.test('should create a valid instance of SmartPdf', async () => {
|
||||
testSmartPdf = new smartpdf.SmartPdf();
|
||||
expect(testSmartPdf).toBeInstanceOf(smartpdf.SmartPdf);
|
||||
@@ -65,19 +72,108 @@ tap.test('should create PNG images from combined PDF using Puppeteer conversion'
|
||||
});
|
||||
|
||||
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`);
|
||||
const filePath = path.join(testResultsDir, `png_combined_page${index + 1}.png`);
|
||||
fs.writeFileSync(filePath, Buffer.from(img));
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should create WebP preview images from PDF', async () => {
|
||||
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||
const webpPreviews = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer);
|
||||
expect(webpPreviews.length).toBeGreaterThan(0);
|
||||
console.log('WebP preview sizes:', webpPreviews.map(img => img.length));
|
||||
|
||||
// Also create PNG previews for comparison
|
||||
const pngPreviews = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer);
|
||||
console.log('PNG preview sizes:', pngPreviews.map(img => img.length));
|
||||
|
||||
// Save the first page as both WebP and PNG preview
|
||||
fs.writeFileSync(path.join(testResultsDir, 'webp_default_page1.webp'), Buffer.from(webpPreviews[0]));
|
||||
fs.writeFileSync(path.join(testResultsDir, 'png_default_page1.png'), Buffer.from(pngPreviews[0]));
|
||||
});
|
||||
|
||||
tap.test('should create WebP previews with custom scale and quality', async () => {
|
||||
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||
|
||||
// Create smaller previews with lower quality for thumbnails
|
||||
const thumbnails = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer, {
|
||||
scale: 0.5, // Create readable thumbnails at ~36 DPI
|
||||
quality: 70
|
||||
});
|
||||
|
||||
expect(thumbnails.length).toBeGreaterThan(0);
|
||||
console.log('Thumbnail sizes:', thumbnails.map(img => img.length));
|
||||
|
||||
// Save thumbnails
|
||||
thumbnails.forEach((thumb, index) => {
|
||||
fs.writeFileSync(path.join(testResultsDir, `webp_thumbnail_page${index + 1}.webp`), Buffer.from(thumb));
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should create WebP previews with max dimensions', async () => {
|
||||
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||
|
||||
// Create previews with maximum dimensions (will use high scale but constrain to max size)
|
||||
const constrainedPreviews = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer, {
|
||||
scale: smartpdf.SmartPdf.SCALE_HIGH, // Start with high quality
|
||||
quality: 90,
|
||||
maxWidth: 800,
|
||||
maxHeight: 1000
|
||||
});
|
||||
|
||||
expect(constrainedPreviews.length).toBeGreaterThan(0);
|
||||
console.log('Constrained preview sizes:', constrainedPreviews.map(img => img.length));
|
||||
|
||||
// Save constrained preview
|
||||
fs.writeFileSync(path.join(testResultsDir, 'webp_constrained_page1.webp'), Buffer.from(constrainedPreviews[0]));
|
||||
});
|
||||
|
||||
tap.test('should verify WebP files are smaller than PNG', async () => {
|
||||
const pdfObject = await testSmartPdf.readFileToPdfObject('.nogit/3.pdf');
|
||||
|
||||
// Generate both PNG and WebP versions at the same scale for fair comparison
|
||||
const comparisonScale = smartpdf.SmartPdf.SCALE_HIGH; // Both use 3.0 scale
|
||||
|
||||
const pngImages = await testSmartPdf.convertPDFToPngBytes(pdfObject.buffer, {
|
||||
scale: comparisonScale
|
||||
});
|
||||
const webpImages = await testSmartPdf.convertPDFToWebpBytes(pdfObject.buffer, {
|
||||
scale: comparisonScale,
|
||||
quality: 85
|
||||
});
|
||||
|
||||
expect(pngImages.length).toEqual(webpImages.length);
|
||||
|
||||
// Compare sizes
|
||||
let totalPngSize = 0;
|
||||
let totalWebpSize = 0;
|
||||
|
||||
pngImages.forEach((png, index) => {
|
||||
const pngSize = png.length;
|
||||
const webpSize = webpImages[index].length;
|
||||
totalPngSize += pngSize;
|
||||
totalWebpSize += webpSize;
|
||||
|
||||
const reduction = ((pngSize - webpSize) / pngSize * 100).toFixed(1);
|
||||
console.log(`Page ${index + 1}: PNG=${pngSize} bytes, WebP=${webpSize} bytes, Reduction=${reduction}%`);
|
||||
|
||||
// Save comparison files
|
||||
fs.writeFileSync(path.join(testResultsDir, `comparison_png_page${index + 1}.png`), Buffer.from(png));
|
||||
fs.writeFileSync(path.join(testResultsDir, `comparison_webp_page${index + 1}.webp`), Buffer.from(webpImages[index]));
|
||||
});
|
||||
|
||||
const totalReduction = ((totalPngSize - totalWebpSize) / totalPngSize * 100).toFixed(1);
|
||||
console.log(`Total size reduction: ${totalReduction}% (PNG: ${totalPngSize} bytes, WebP: ${totalWebpSize} bytes)`);
|
||||
|
||||
// WebP should be smaller
|
||||
expect(totalWebpSize).toBeLessThan(totalPngSize);
|
||||
});
|
||||
|
||||
tap.test('should close the SmartPdf instance properly', async () => {
|
||||
await testSmartPdf.stop();
|
||||
});
|
||||
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartpdf',
|
||||
version: '3.2.1',
|
||||
version: '3.2.2',
|
||||
description: 'A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.'
|
||||
}
|
||||
|
@@ -7,10 +7,29 @@ import { execFile } from 'child_process';
|
||||
|
||||
declare const document: any;
|
||||
|
||||
export interface ISmartPdfOptions {
|
||||
port?: number;
|
||||
portRangeStart?: number;
|
||||
portRangeEnd?: number;
|
||||
}
|
||||
|
||||
export class SmartPdf {
|
||||
// STATIC SCALE CONSTANTS
|
||||
public static readonly SCALE_SCREEN = 2.0; // ~144 DPI - Good for screen display
|
||||
public static readonly SCALE_HIGH = 3.0; // ~216 DPI - High quality (default)
|
||||
public static readonly SCALE_PRINT = 6.0; // ~432 DPI - Print quality
|
||||
|
||||
/**
|
||||
* Calculate scale factor for desired DPI
|
||||
* PDF.js default is 72 DPI, so scale = desiredDPI / 72
|
||||
*/
|
||||
public static getScaleForDPI(dpi: number): number {
|
||||
return dpi / 72;
|
||||
}
|
||||
|
||||
// STATIC
|
||||
public static async create() {
|
||||
const smartpdfInstance = new SmartPdf();
|
||||
public static async create(optionsArg?: ISmartPdfOptions) {
|
||||
const smartpdfInstance = new SmartPdf(optionsArg);
|
||||
return smartpdfInstance;
|
||||
}
|
||||
|
||||
@@ -21,9 +40,15 @@ export class SmartPdf {
|
||||
externalBrowserBool: boolean = false;
|
||||
private _readyDeferred: plugins.smartpromise.Deferred<void>;
|
||||
private _candidates: { [key: string]: PdfCandidate } = {};
|
||||
private _options: ISmartPdfOptions;
|
||||
|
||||
constructor() {
|
||||
constructor(optionsArg?: ISmartPdfOptions) {
|
||||
this._readyDeferred = new plugins.smartpromise.Deferred();
|
||||
this._options = {
|
||||
portRangeStart: 20000,
|
||||
portRangeEnd: 30000,
|
||||
...optionsArg
|
||||
};
|
||||
}
|
||||
|
||||
async start(headlessBrowserArg?: plugins.smartpuppeteer.puppeteer.Browser) {
|
||||
@@ -35,11 +60,41 @@ export class SmartPdf {
|
||||
this.externalBrowserBool = true;
|
||||
} else {
|
||||
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();
|
||||
app.get('/:pdfId', (req, res) => {
|
||||
const wantedCandidate = this._candidates[req.params.pdfId];
|
||||
@@ -51,10 +106,10 @@ export class SmartPdf {
|
||||
res.send(wantedCandidate.htmlString);
|
||||
});
|
||||
this.htmlServerInstance = plugins.http.createServer(app);
|
||||
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
||||
const portAvailable = smartnetworkInstance.isLocalPortUnused(3210);
|
||||
this.htmlServerInstance.listen(3210, 'localhost');
|
||||
|
||||
this.htmlServerInstance.listen(this.serverPort, 'localhost');
|
||||
this.htmlServerInstance.on('listening', () => {
|
||||
console.log(`SmartPdf server listening on port ${this.serverPort}`);
|
||||
this._readyDeferred.resolve();
|
||||
done.resolve();
|
||||
});
|
||||
@@ -87,7 +142,7 @@ export class SmartPdf {
|
||||
width: 794,
|
||||
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',
|
||||
});
|
||||
const headers = response.headers();
|
||||
@@ -104,6 +159,8 @@ export class SmartPdf {
|
||||
printBackground: true,
|
||||
displayHeaderFooter: false,
|
||||
});
|
||||
// Convert Uint8Array to Node Buffer
|
||||
const nodePdfBuffer = Buffer.from(pdfBuffer);
|
||||
await page.close();
|
||||
delete this._candidates[pdfCandidate.pdfId];
|
||||
pdfCandidate.doneDeferred.resolve();
|
||||
@@ -112,9 +169,9 @@ export class SmartPdf {
|
||||
id: pdfCandidate.pdfId,
|
||||
name: `${pdfCandidate.pdfId}.js`,
|
||||
metadata: {
|
||||
textExtraction: await this.extractTextFromPdfBuffer(pdfBuffer),
|
||||
textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
|
||||
},
|
||||
buffer: pdfBuffer,
|
||||
buffer: nodePdfBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,14 +196,16 @@ export class SmartPdf {
|
||||
printBackground: true,
|
||||
displayHeaderFooter: false,
|
||||
});
|
||||
// Convert Uint8Array to Node Buffer
|
||||
const nodePdfBuffer = Buffer.from(pdfBuffer);
|
||||
await page.close();
|
||||
return {
|
||||
id: pdfId,
|
||||
name: `${pdfId}.js`,
|
||||
metadata: {
|
||||
textExtraction: await this.extractTextFromPdfBuffer(pdfBuffer),
|
||||
textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
|
||||
},
|
||||
buffer: pdfBuffer,
|
||||
buffer: nodePdfBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,12 +218,20 @@ export class SmartPdf {
|
||||
await page.emulateMediaType('screen');
|
||||
const response = await page.goto(websiteUrl, { waitUntil: 'networkidle2' });
|
||||
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(() => {
|
||||
return {
|
||||
documentHeight: document.body.scrollHeight,
|
||||
documentWidth: document.body.clientWidth,
|
||||
documentHeight: Math.max(
|
||||
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({
|
||||
width: 1920,
|
||||
height: documentHeight,
|
||||
@@ -177,14 +244,16 @@ export class SmartPdf {
|
||||
scale: 1,
|
||||
pageRanges: '1',
|
||||
});
|
||||
// Convert Uint8Array to Node Buffer
|
||||
const nodePdfBuffer = Buffer.from(pdfBuffer);
|
||||
await page.close();
|
||||
return {
|
||||
id: pdfId,
|
||||
name: `${pdfId}.js`,
|
||||
metadata: {
|
||||
textExtraction: await this.extractTextFromPdfBuffer(pdfBuffer),
|
||||
textExtraction: await this.extractTextFromPdfBuffer(nodePdfBuffer),
|
||||
},
|
||||
buffer: pdfBuffer,
|
||||
buffer: nodePdfBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,7 +281,7 @@ export class SmartPdf {
|
||||
};
|
||||
}
|
||||
|
||||
public async extractTextFromPdfBuffer(pdfBufferArg: Uint8Array): Promise<string> {
|
||||
public async extractTextFromPdfBuffer(pdfBufferArg: Buffer): Promise<string> {
|
||||
const deferred = plugins.smartpromise.defer<string>();
|
||||
const pdfParser: any = new plugins.pdf2json();
|
||||
pdfParser.on('pdfParser_dataReady', (pdfData: any) => {
|
||||
@@ -262,10 +331,14 @@ export class SmartPdf {
|
||||
*/
|
||||
public async convertPDFToPngBytes(
|
||||
pdfBytes: Uint8Array,
|
||||
options: { width?: number; height?: number; quality?: number } = {}
|
||||
options: {
|
||||
scale?: number; // Scale factor for output size (default: 3.0 for 216 DPI)
|
||||
maxWidth?: number; // Maximum width in pixels (optional)
|
||||
maxHeight?: number; // Maximum height in pixels (optional)
|
||||
} = {}
|
||||
): Promise<Uint8Array[]> {
|
||||
// Note: options.width, options.height, and options.quality are not applied here,
|
||||
// as the rendered canvas size is determined by the PDF page dimensions.
|
||||
// Set default scale for higher quality output (3.0 = ~216 DPI)
|
||||
const scale = options.scale || 3.0;
|
||||
|
||||
// Create a new page using the headless browser.
|
||||
const page = await this.headlessBrowser.newPage();
|
||||
@@ -298,12 +371,31 @@ export class SmartPdf {
|
||||
const numPages = pdf.numPages;
|
||||
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||
const page = await pdf.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
// Apply scale factor to viewport
|
||||
const viewport = page.getViewport({ scale: ${scale} });
|
||||
|
||||
// Apply max width/height constraints if specified
|
||||
let finalScale = ${scale};
|
||||
${options.maxWidth ? `
|
||||
if (viewport.width > ${options.maxWidth}) {
|
||||
finalScale = ${options.maxWidth} / (viewport.width / ${scale});
|
||||
}` : ''}
|
||||
${options.maxHeight ? `
|
||||
if (viewport.height > ${options.maxHeight}) {
|
||||
const heightScale = ${options.maxHeight} / (viewport.height / ${scale});
|
||||
finalScale = Math.min(finalScale, heightScale);
|
||||
}` : ''}
|
||||
|
||||
// Get final viewport with adjusted scale
|
||||
const finalViewport = page.getViewport({ scale: finalScale });
|
||||
|
||||
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;
|
||||
canvas.width = finalViewport.width;
|
||||
canvas.height = finalViewport.height;
|
||||
canvas.setAttribute('data-page', pageNum);
|
||||
|
||||
await page.render({ canvasContext: context, viewport: finalViewport }).promise;
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
window.renderComplete = true;
|
||||
@@ -335,4 +427,115 @@ export class SmartPdf {
|
||||
await page.close();
|
||||
return pngBuffers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a PDF to WebP bytes for each page.
|
||||
* This method creates web-optimized images using WebP format.
|
||||
* WebP provides 25-35% better compression than JPEG/PNG while maintaining quality.
|
||||
*/
|
||||
public async convertPDFToWebpBytes(
|
||||
pdfBytes: Uint8Array,
|
||||
options: {
|
||||
scale?: number; // Scale factor for preview size (default: 3.0 for 216 DPI)
|
||||
quality?: number; // WebP quality 0-100 (default: 85)
|
||||
maxWidth?: number; // Maximum width in pixels (optional)
|
||||
maxHeight?: number; // Maximum height in pixels (optional)
|
||||
} = {}
|
||||
): Promise<Uint8Array[]> {
|
||||
// Set default options for higher quality output (3.0 = ~216 DPI)
|
||||
const scale = options.scale || 3.0;
|
||||
const quality = options.quality || 85;
|
||||
|
||||
// Create a new page using the headless browser
|
||||
const page = await this.headlessBrowser.newPage();
|
||||
|
||||
// Prepare PDF data as a base64 string
|
||||
const base64Pdf: string = Buffer.from(pdfBytes).toString('base64');
|
||||
|
||||
// HTML template that loads PDF.js and renders the PDF with scaling
|
||||
const htmlTemplate: string = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>PDF to WebP Preview 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);
|
||||
// Apply scale factor to viewport
|
||||
const viewport = page.getViewport({ scale: ${scale} });
|
||||
|
||||
// Apply max width/height constraints if specified
|
||||
let finalScale = ${scale};
|
||||
${options.maxWidth ? `
|
||||
if (viewport.width > ${options.maxWidth}) {
|
||||
finalScale = ${options.maxWidth} / (viewport.width / ${scale});
|
||||
}` : ''}
|
||||
${options.maxHeight ? `
|
||||
if (viewport.height > ${options.maxHeight}) {
|
||||
const heightScale = ${options.maxHeight} / (viewport.height / ${scale});
|
||||
finalScale = Math.min(finalScale, heightScale);
|
||||
}` : ''}
|
||||
|
||||
// Get final viewport with adjusted scale
|
||||
const finalViewport = page.getViewport({ scale: finalScale });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = finalViewport.width;
|
||||
canvas.height = finalViewport.height;
|
||||
canvas.setAttribute('data-page', pageNum);
|
||||
|
||||
await page.render({ canvasContext: context, viewport: finalViewport }).promise;
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
window.renderComplete = true;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Replace the placeholder with the actual base64 PDF data
|
||||
const htmlContent: string = htmlTemplate.replace("__PDF_DATA__", base64Pdf);
|
||||
|
||||
// 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 webpBuffers: Uint8Array[] = [];
|
||||
|
||||
for (const canvasElement of canvasElements) {
|
||||
// Screenshot the canvas element as WebP
|
||||
const screenshotBuffer = (await canvasElement.screenshot({
|
||||
type: 'webp',
|
||||
quality: quality,
|
||||
encoding: 'binary'
|
||||
})) as Buffer;
|
||||
webpBuffers.push(new Uint8Array(screenshotBuffer));
|
||||
}
|
||||
|
||||
await page.close();
|
||||
return webpBuffers;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user