@polotno/pdf-export 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +15 -3
- package/lib/text/fonts.d.ts +1 -0
- package/lib/text/fonts.js +18 -3
- package/lib/text/render.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,41 @@ await jsonToPDF(json, './output.pdf', {
|
|
|
94
94
|
- Spot colors work best with PDF/X-1a export enabled
|
|
95
95
|
- You can verify spot colors in Adobe Acrobat by checking Output Preview > Separations
|
|
96
96
|
|
|
97
|
+
## DPI Handling
|
|
98
|
+
|
|
99
|
+
The library automatically handles DPI conversion to ensure correct physical dimensions in the output PDF. By default, it uses the `dpi` value from your JSON file (or 72 DPI if not specified).
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
// JSON with dpi specified
|
|
103
|
+
const json = {
|
|
104
|
+
width: 1920,
|
|
105
|
+
height: 1080,
|
|
106
|
+
dpi: 300, // 300 DPI input
|
|
107
|
+
// ... rest of JSON
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Use JSON dpi automatically
|
|
111
|
+
await jsonToPDF(json, './output.pdf');
|
|
112
|
+
|
|
113
|
+
// Override DPI via attrs (takes precedence over JSON dpi)
|
|
114
|
+
await jsonToPDF(json, './output.pdf', {
|
|
115
|
+
dpi: 150, // Override to 150 DPI
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**How it works:**
|
|
120
|
+
|
|
121
|
+
- Input JSON coordinates are in **pixels** at the specified DPI
|
|
122
|
+
- PDF uses **points** (1 point = 1/72 inch)
|
|
123
|
+
- The library converts: `points = pixels × (72 / dpi)`
|
|
124
|
+
- This ensures the PDF has correct physical dimensions for printing
|
|
125
|
+
- All element positions, sizes, and coordinates are automatically scaled
|
|
126
|
+
|
|
127
|
+
**Example:**
|
|
128
|
+
|
|
129
|
+
- A 1920×1080 pixel canvas at 300 DPI = 6.4" × 3.6" in the PDF
|
|
130
|
+
- The same canvas at 72 DPI = 26.67" × 15" in the PDF
|
|
131
|
+
|
|
97
132
|
## Requirements
|
|
98
133
|
|
|
99
134
|
- **GhostScript** must be installed for PDF/X-1a conversion
|
package/lib/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface PolotnoJSON {
|
|
|
10
10
|
background?: string;
|
|
11
11
|
children: any[];
|
|
12
12
|
}>;
|
|
13
|
+
dpi?: number;
|
|
13
14
|
}
|
|
14
15
|
export interface RenderAttrs {
|
|
15
16
|
pdfx1a?: boolean;
|
|
@@ -22,5 +23,6 @@ export interface RenderAttrs {
|
|
|
22
23
|
};
|
|
23
24
|
spotColors?: SpotColorConfig;
|
|
24
25
|
textVerticalResizeEnabled?: boolean;
|
|
26
|
+
dpi?: number;
|
|
25
27
|
}
|
|
26
28
|
export declare function jsonToPDF(json: PolotnoJSON, pdfFileName: string, attrs?: RenderAttrs): Promise<void>;
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { srcToBuffer, parseColor } from './utils.js';
|
|
5
5
|
import { renderImage } from './image.js';
|
|
6
6
|
import { loadFontIfNeeded, renderText } from './text/index.js';
|
|
7
|
+
import { registerFontUrl } from './text/fonts.js';
|
|
7
8
|
import { renderFigure } from './figure.js';
|
|
8
9
|
import { renderGroup } from './group.js';
|
|
9
10
|
import { lineToPDF } from './line.js';
|
|
@@ -51,6 +52,13 @@ async function renderElement({ doc, element, fonts, attrs, cache, }) {
|
|
|
51
52
|
}
|
|
52
53
|
export async function jsonToPDF(json, pdfFileName, attrs = {}) {
|
|
53
54
|
const fonts = {};
|
|
55
|
+
// Compute DPI and scale factor
|
|
56
|
+
// Priority: attrs.dpi (override) > json.dpi > 72 (default)
|
|
57
|
+
const inputDpi = attrs.dpi ?? json.dpi ?? 72;
|
|
58
|
+
// Validate DPI: must be finite and positive
|
|
59
|
+
const validDpi = Number.isFinite(inputDpi) && inputDpi > 0 ? inputDpi : 72;
|
|
60
|
+
// Convert pixels to PDF points: 1 point = 1/72 inch, so points per pixel = 72 / dpi
|
|
61
|
+
const ptPerPx = 72 / validDpi;
|
|
54
62
|
// Create cache for images and processed results
|
|
55
63
|
const cache = {
|
|
56
64
|
images: new Map(), // Cache for loaded Canvas images
|
|
@@ -60,7 +68,7 @@ export async function jsonToPDF(json, pdfFileName, attrs = {}) {
|
|
|
60
68
|
tempDir: null, // Temporary directory for image files
|
|
61
69
|
};
|
|
62
70
|
var doc = new PDFDocument({
|
|
63
|
-
size: [json.width, json.height],
|
|
71
|
+
size: [json.width * ptPerPx, json.height * ptPerPx],
|
|
64
72
|
autoFirstPage: false,
|
|
65
73
|
});
|
|
66
74
|
// Enable spot color support if configured
|
|
@@ -68,11 +76,14 @@ export async function jsonToPDF(json, pdfFileName, attrs = {}) {
|
|
|
68
76
|
enableSpotColorSupport(doc, attrs.spotColors);
|
|
69
77
|
}
|
|
70
78
|
for (const font of json.fonts) {
|
|
71
|
-
|
|
72
|
-
fonts[font.fontFamily] = true;
|
|
79
|
+
registerFontUrl(font.fontFamily, font.url);
|
|
73
80
|
}
|
|
74
81
|
for (const page of json.pages) {
|
|
75
82
|
doc.addPage();
|
|
83
|
+
// Apply scale transform so all pixel-based coordinates work correctly
|
|
84
|
+
// The page size is already in points, but we render in pixel coordinates
|
|
85
|
+
doc.save();
|
|
86
|
+
doc.scale(ptPerPx);
|
|
76
87
|
if (page.background) {
|
|
77
88
|
const isURL = page.background.indexOf('http') >= 0 ||
|
|
78
89
|
page.background.indexOf('.png') >= 0 ||
|
|
@@ -91,6 +102,7 @@ export async function jsonToPDF(json, pdfFileName, attrs = {}) {
|
|
|
91
102
|
for (const element of page.children) {
|
|
92
103
|
await renderElement({ doc, element, fonts, attrs, cache });
|
|
93
104
|
}
|
|
105
|
+
doc.restore();
|
|
94
106
|
}
|
|
95
107
|
doc.end();
|
|
96
108
|
await new Promise((r) => doc.pipe(fs.createWriteStream(pdfFileName)).on('finish', r));
|
package/lib/text/fonts.d.ts
CHANGED
package/lib/text/fonts.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { srcToBuffer } from '../utils.js';
|
|
2
2
|
import getUrls from 'get-urls';
|
|
3
3
|
import fetch from 'node-fetch';
|
|
4
|
+
const fontUrlRegistry = {};
|
|
5
|
+
export function registerFontUrl(fontFamily, url) {
|
|
6
|
+
fontUrlRegistry[fontFamily] = url;
|
|
7
|
+
}
|
|
4
8
|
/**
|
|
5
9
|
* Get font weight string based on bold/italic state
|
|
6
10
|
*/
|
|
@@ -53,9 +57,20 @@ export async function loadFontForSegment(doc, segment, element, fonts) {
|
|
|
53
57
|
}
|
|
54
58
|
const fontKey = getFontKey(fontFamily, bold, italic, element.fontWeight);
|
|
55
59
|
if (!fonts[fontKey]) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
let src;
|
|
61
|
+
if (fontUrlRegistry[fontFamily]) {
|
|
62
|
+
src = fontUrlRegistry[fontFamily];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const weight = getFontWeight(bold, italic, element.fontWeight);
|
|
66
|
+
src = await getGoogleFontPath(fontFamily, weight, italic);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
doc.registerFont(fontKey, await srcToBuffer(src));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new Error(`Failed to load font "${fontFamily}" from ${src}: ${error.message}`);
|
|
73
|
+
}
|
|
59
74
|
fonts[fontKey] = true;
|
|
60
75
|
}
|
|
61
76
|
doc.font(fontKey);
|
package/lib/text/render.js
CHANGED
|
@@ -186,13 +186,14 @@ export async function renderTextFill(doc, element, textLines, yOffset, lineHeigh
|
|
|
186
186
|
const renderSegments = await buildRenderSegmentsForLine(doc, element, line.text, textOptions, fonts);
|
|
187
187
|
// Apply segment-specific colors for rich text
|
|
188
188
|
const applySegmentColor = (segment) => {
|
|
189
|
-
const segmentColor = segment.color
|
|
190
|
-
? parseColor(segment.color).hex
|
|
191
|
-
: parseColor(element.fill).hex;
|
|
192
189
|
const segmentParsedColor = segment.color
|
|
193
190
|
? parseColor(segment.color)
|
|
194
191
|
: parseColor(element.fill);
|
|
195
|
-
|
|
192
|
+
// Fallback to element fill if segment color parsing fails
|
|
193
|
+
const segmentColor = segmentParsedColor?.hex || parseColor(element.fill).hex || '#000000';
|
|
194
|
+
// Segment alpha can be NaN (e.g. rgba(..., var(--x, 1))) -> fallback to 1
|
|
195
|
+
const a = segmentParsedColor?.rgba?.[3];
|
|
196
|
+
const segmentOpacity = Math.min(typeof a === 'number' && a >= 0 && a <= 1 ? a : 1, element.opacity, 1);
|
|
196
197
|
doc.fillColor(segmentColor, segmentOpacity);
|
|
197
198
|
};
|
|
198
199
|
await renderSegmentsForLine(doc, element, line, renderSegments, context, textOptions, {
|