@polotno/pdf-export 0.1.18 → 0.1.19

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/lib/image.js CHANGED
@@ -1,92 +1,137 @@
1
- const { loadImage, PIXEL_RATIO, srcToBuffer } = require('./utils');
2
- const Canvas = require('canvas');
3
-
4
- async function cropImage(src, element) {
5
- const image = await loadImage(src);
6
- const canvas = new Canvas.Canvas();
7
-
8
- canvas.width = element.width * PIXEL_RATIO;
9
- canvas.height = element.height * PIXEL_RATIO;
10
-
11
- const ctx = canvas.getContext('2d');
12
-
13
- let { cropX, cropY } = element;
14
-
15
- const availableWidth = image.width * element.cropWidth;
16
- const availableHeight = image.height * element.cropHeight;
17
-
18
- const aspectRatio = element.width / element.height;
19
-
20
- let cropAbsoluteWidth;
21
- let cropAbsoluteHeight;
22
-
23
- const imageRatio = availableWidth / availableHeight;
24
- const allowScale = element.type === 'svg';
25
-
26
- if (allowScale) {
27
- cropAbsoluteWidth = availableWidth;
28
- cropAbsoluteHeight = availableHeight;
29
- } else if (aspectRatio >= imageRatio) {
30
- cropAbsoluteWidth = availableWidth;
31
- cropAbsoluteHeight = availableWidth / aspectRatio;
32
- } else {
33
- cropAbsoluteWidth = availableHeight * aspectRatio;
34
- cropAbsoluteHeight = availableHeight;
35
- }
36
-
37
- ctx.drawImage(
38
- image,
39
- cropX * image.width,
40
- cropY * image.height,
41
- cropAbsoluteWidth,
42
- cropAbsoluteHeight,
43
- 0,
44
- 0,
45
- canvas.width,
46
- canvas.height
47
- );
48
-
49
- return canvas.toDataURL('image/png');
1
+ import { loadImage, PIXEL_RATIO, srcToBuffer } from './utils.js';
2
+ import Canvas from 'canvas';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import crypto from 'crypto';
7
+ async function applyFlip(image, element) {
8
+ const { flipX, flipY } = element;
9
+ if (!flipX && !flipY) {
10
+ return image;
11
+ }
12
+ if (!image || !image.width || !image.height) {
13
+ return null;
14
+ }
15
+ const canvas = new Canvas.Canvas(image.width, image.height);
16
+ const ctx = canvas.getContext('2d');
17
+ let x = flipX ? -canvas.width : 0;
18
+ let y = flipY ? -canvas.height : 0;
19
+ ctx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
20
+ ctx.drawImage(image, x, y, canvas.width, canvas.height);
21
+ return canvas;
50
22
  }
51
-
52
- async function clipImage(src, element) {
53
- const image = await loadImage(src);
54
- const clipImage = await loadImage(element.clipSrc);
55
-
56
- const canvas = new Canvas.Canvas(element.width, element.height);
57
- const ctx = canvas.getContext('2d');
58
-
59
- ctx.drawImage(image, 0, 0, element.width, element.height);
60
-
61
- const clipCanvas = new Canvas.Canvas(element.width, element.height);
62
- const clipCtx = clipCanvas.getContext('2d');
63
-
64
- clipCtx.drawImage(clipImage, 0, 0, element.width, element.height);
65
-
66
- ctx.globalCompositeOperation = 'destination-in';
67
- ctx.drawImage(clipCanvas, 0, 0);
68
-
69
- ctx.globalCompositeOperation = 'source-over';
70
-
71
- return canvas.toDataURL('image/png');
72
- }
73
-
74
- async function renderImage(doc, element) {
75
- let src = await cropImage(element.src, element);
76
- if (element.clipSrc) {
77
- src = await clipImage(src, element);
78
- }
79
- if (src) {
80
- doc.image(await srcToBuffer(src), 0, 0, {
81
- width: element.width,
82
- height: element.height,
83
- opacity: element.opacity,
23
+ // Helper to create cache key for processed images
24
+ export function getProcessedImageKey(element) {
25
+ return JSON.stringify({
26
+ src: element.src,
27
+ width: element.width,
28
+ height: element.height,
29
+ cropX: element.cropX,
30
+ cropY: element.cropY,
31
+ cropWidth: element.cropWidth,
32
+ cropHeight: element.cropHeight,
33
+ clipSrc: element.clipSrc,
84
34
  });
85
- }
86
35
  }
87
-
88
- module.exports = {
89
- cropImage,
90
- clipImage,
91
- renderImage,
92
- };
36
+ export async function cropImage(src, element, cache = null) {
37
+ let image = await loadImage(src);
38
+ // Apply flip transformations first
39
+ image = await applyFlip(image, element);
40
+ if (!image) {
41
+ return null;
42
+ }
43
+ const canvas = Canvas.createCanvas(element.width * PIXEL_RATIO, element.height * PIXEL_RATIO);
44
+ const ctx = canvas.getContext('2d');
45
+ let { cropX, cropY } = element;
46
+ const availableWidth = image.width * element.cropWidth;
47
+ const availableHeight = image.height * element.cropHeight;
48
+ const aspectRatio = element.width / element.height;
49
+ let cropAbsoluteWidth;
50
+ let cropAbsoluteHeight;
51
+ const imageRatio = availableWidth / availableHeight;
52
+ const allowScale = element.type === 'svg';
53
+ if (allowScale) {
54
+ cropAbsoluteWidth = availableWidth;
55
+ cropAbsoluteHeight = availableHeight;
56
+ }
57
+ else if (aspectRatio >= imageRatio) {
58
+ cropAbsoluteWidth = availableWidth;
59
+ cropAbsoluteHeight = availableWidth / aspectRatio;
60
+ }
61
+ else {
62
+ cropAbsoluteWidth = availableHeight * aspectRatio;
63
+ cropAbsoluteHeight = availableHeight;
64
+ }
65
+ ctx.drawImage(image, cropX * image.width, cropY * image.height, cropAbsoluteWidth, cropAbsoluteHeight, 0, 0, canvas.width, canvas.height);
66
+ return canvas.toDataURL('image/png');
67
+ }
68
+ export async function clipImage(src, element, cache = null) {
69
+ const image = await loadImage(src, cache);
70
+ const clipImage = await loadImage(element.clipSrc, cache);
71
+ const canvas = Canvas.createCanvas(element.width, element.height);
72
+ const ctx = canvas.getContext('2d');
73
+ ctx.drawImage(image, 0, 0, element.width, element.height);
74
+ const clipCanvas = Canvas.createCanvas(element.width, element.height);
75
+ const clipCtx = clipCanvas.getContext('2d');
76
+ clipCtx.drawImage(clipImage, 0, 0, element.width, element.height);
77
+ ctx.globalCompositeOperation = 'destination-in';
78
+ ctx.drawImage(clipCanvas, 0, 0);
79
+ ctx.globalCompositeOperation = 'source-over';
80
+ return canvas.toDataURL('image/png');
81
+ }
82
+ export async function renderImage(doc, element, cache = null) {
83
+ // Check if we have a cached processed version
84
+ const cacheKey = getProcessedImageKey(element);
85
+ // Check if we have a cached file path for this image
86
+ if (cache && cache.imageFiles && cache.imageFiles.has(cacheKey)) {
87
+ const filePath = cache.imageFiles.get(cacheKey);
88
+ console.log('✓ Using cached image file:', path.basename(filePath));
89
+ doc.image(filePath, 0, 0, {
90
+ width: element.width,
91
+ height: element.height,
92
+ opacity: element.opacity,
93
+ });
94
+ return;
95
+ }
96
+ let src = null;
97
+ if (cache && cache.processedImages.has(cacheKey)) {
98
+ src = cache.processedImages.get(cacheKey);
99
+ }
100
+ else {
101
+ src = await cropImage(element.src, element, cache);
102
+ if (element.clipSrc) {
103
+ src = await clipImage(src, element, cache);
104
+ }
105
+ // Cache the processed result
106
+ if (cache && src) {
107
+ cache.processedImages.set(cacheKey, src);
108
+ }
109
+ }
110
+ if (src) {
111
+ const buffer = await srcToBuffer(src, cache);
112
+ // Save buffer to a temp file and cache the path
113
+ let filePath;
114
+ if (cache) {
115
+ if (!cache.imageFiles) {
116
+ cache.imageFiles = new Map();
117
+ }
118
+ if (!cache.tempDir) {
119
+ cache.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pdfkit-images-'));
120
+ }
121
+ // Create a unique filename based on cache key hash
122
+ const hash = crypto.createHash('md5').update(cacheKey).digest('hex');
123
+ filePath = path.join(cache.tempDir, `${hash}.png`);
124
+ // Write buffer to file
125
+ fs.writeFileSync(filePath, buffer);
126
+ cache.imageFiles.set(cacheKey, filePath);
127
+ }
128
+ else {
129
+ filePath = buffer;
130
+ }
131
+ doc.image(filePath, 0, 0, {
132
+ width: element.width,
133
+ height: element.height,
134
+ opacity: element.opacity,
135
+ });
136
+ }
137
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { SpotColorConfig } from './spot-colors.js';
2
+ export interface PolotnoJSON {
3
+ width: number;
4
+ height: number;
5
+ fonts: Array<{
6
+ fontFamily: string;
7
+ url: string;
8
+ }>;
9
+ pages: Array<{
10
+ background?: string;
11
+ children: any[];
12
+ }>;
13
+ }
14
+ export interface RenderAttrs {
15
+ pdfx1a?: boolean;
16
+ validate?: boolean;
17
+ metadata?: {
18
+ title?: string;
19
+ author?: string;
20
+ application?: string;
21
+ producer?: string;
22
+ };
23
+ spotColors?: SpotColorConfig;
24
+ textVerticalResizeEnabled?: boolean;
25
+ }
26
+ export declare function jsonToPDF(json: PolotnoJSON, pdfFileName: string, attrs?: RenderAttrs): Promise<void>;
package/lib/index.js ADDED
@@ -0,0 +1,130 @@
1
+ import PDFDocument from 'pdfkit';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { srcToBuffer, parseColor } from './utils.js';
5
+ import { renderImage } from './image.js';
6
+ import { loadFontIfNeeded, renderText } from './text.js';
7
+ import { renderFigure } from './figure.js';
8
+ import { renderGroup } from './group.js';
9
+ import { lineToPDF } from './line.js';
10
+ import { renderSVG } from './svg-render.js';
11
+ import { convertToPDFX1a, validatePDFX1a } from './ghostscript.js';
12
+ import { enableSpotColorSupport } from './spot-colors.js';
13
+ import SVGtoPDF from 'svg-to-pdfkit';
14
+ // Extend PDFDocument prototype with addSVG method
15
+ PDFDocument.prototype.addSVG = function (svg, x, y, options) {
16
+ return SVGtoPDF(this, svg, x, y, options), this;
17
+ };
18
+ async function renderElement({ doc, element, fonts, attrs, cache, }) {
19
+ if ((element.visible !== undefined && !element.visible) ||
20
+ (element.showInExport !== undefined && !element.showInExport)) {
21
+ return;
22
+ }
23
+ doc.save();
24
+ if (element.type !== 'group') {
25
+ doc.translate(element.x, element.y);
26
+ doc.rotate(element.rotation);
27
+ }
28
+ if (element.opacity !== undefined) {
29
+ doc.opacity(element.opacity);
30
+ }
31
+ if (element.type === 'group') {
32
+ await renderGroup(doc, element, renderElement, fonts, attrs, cache);
33
+ }
34
+ else if (element.type === 'text') {
35
+ await loadFontIfNeeded(doc, element, fonts);
36
+ renderText(doc, element, attrs);
37
+ }
38
+ else if (element.type === 'line') {
39
+ lineToPDF(doc, element);
40
+ }
41
+ else if (element.type === 'image') {
42
+ await renderImage(doc, element, cache);
43
+ }
44
+ else if (element.type === 'svg') {
45
+ await renderSVG(doc, element, cache);
46
+ }
47
+ else if (element.type === 'figure') {
48
+ renderFigure(doc, element);
49
+ }
50
+ doc.restore();
51
+ }
52
+ export async function jsonToPDF(json, pdfFileName, attrs = {}) {
53
+ const fonts = {};
54
+ // Create cache for images and processed results
55
+ const cache = {
56
+ images: new Map(), // Cache for loaded Canvas images
57
+ buffers: new Map(), // Cache for fetched buffers
58
+ processedImages: new Map(), // Cache for cropped/clipped image data URLs
59
+ imageFiles: new Map(), // Cache for image file paths
60
+ tempDir: null, // Temporary directory for image files
61
+ };
62
+ var doc = new PDFDocument({
63
+ size: [json.width, json.height],
64
+ autoFirstPage: false,
65
+ });
66
+ // Enable spot color support if configured
67
+ if (attrs.spotColors) {
68
+ enableSpotColorSupport(doc, attrs.spotColors);
69
+ }
70
+ for (const font of json.fonts) {
71
+ doc.registerFont(font.fontFamily, await srcToBuffer(font.url, cache));
72
+ fonts[font.fontFamily] = true;
73
+ }
74
+ for (const page of json.pages) {
75
+ doc.addPage();
76
+ if (page.background) {
77
+ const isURL = page.background.indexOf('http') >= 0 ||
78
+ page.background.indexOf('.png') >= 0 ||
79
+ page.background.indexOf('.jpg') >= 0;
80
+ if (isURL) {
81
+ doc.image(await srcToBuffer(page.background, cache), 0, 0);
82
+ }
83
+ else {
84
+ doc.rect(0, 0, json.width, json.height);
85
+ doc.fill(parseColor(page.background).hex);
86
+ }
87
+ }
88
+ for (const element of page.children) {
89
+ await renderElement({ doc, element, fonts, attrs, cache });
90
+ }
91
+ }
92
+ doc.end();
93
+ await new Promise((r) => doc.pipe(fs.createWriteStream(pdfFileName)).on('finish', r));
94
+ // Clean up temporary image files
95
+ if (cache.tempDir && fs.existsSync(cache.tempDir)) {
96
+ const files = fs.readdirSync(cache.tempDir);
97
+ for (const file of files) {
98
+ fs.unlinkSync(path.join(cache.tempDir, file));
99
+ }
100
+ fs.rmdirSync(cache.tempDir);
101
+ }
102
+ // Optional PDF/X-1a conversion
103
+ if (attrs.pdfx1a) {
104
+ const tempFileName = pdfFileName.replace('.pdf', '-temp.pdf');
105
+ // Rename current file to temp
106
+ fs.renameSync(pdfFileName, tempFileName);
107
+ try {
108
+ // Convert temp file to PDF/X-1a
109
+ await convertToPDFX1a(tempFileName, pdfFileName, {
110
+ metadata: attrs.metadata || {},
111
+ });
112
+ // Clean up temp file
113
+ fs.unlinkSync(tempFileName);
114
+ // Optional validation
115
+ if (attrs.validate) {
116
+ const isValid = await validatePDFX1a(pdfFileName);
117
+ if (!isValid) {
118
+ console.warn('Warning: Generated PDF may not be fully PDF/X-1a compliant');
119
+ }
120
+ }
121
+ }
122
+ catch (error) {
123
+ // Restore original file if conversion fails
124
+ if (fs.existsSync(tempFileName)) {
125
+ fs.renameSync(tempFileName, pdfFileName);
126
+ }
127
+ throw new Error(`PDF/X-1a conversion failed: ${error.message}`);
128
+ }
129
+ }
130
+ }
package/lib/line.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface LineElement {
2
+ height: number;
3
+ width: number;
4
+ color: string;
5
+ opacity: number;
6
+ dash?: number[];
7
+ startHead?: string;
8
+ endHead?: string;
9
+ }
10
+ export declare function lineToPDF(doc: any, element: LineElement): void;
package/lib/line.js CHANGED
@@ -1,82 +1,66 @@
1
- const { Util } = require('konva');
2
- const h = (type, props, ...children) => {
3
- return { type, props, children: children || [] };
4
- };
5
-
6
- const getLineHead = ({ element, type, doc }) => {
1
+ import { Util } from 'konva/lib/Util.js';
2
+ function rgbToHex({ r, g, b }) {
3
+ // Ensure each value is within the valid range
4
+ r = Math.max(0, Math.min(255, r));
5
+ g = Math.max(0, Math.min(255, g));
6
+ b = Math.max(0, Math.min(255, b));
7
+ // Convert each value to a 2-digit hexadecimal string
8
+ const hexR = r.toString(16).padStart(2, '0');
9
+ const hexG = g.toString(16).padStart(2, '0');
10
+ const hexB = b.toString(16).padStart(2, '0');
11
+ // Return the concatenated hex string
12
+ return `#${hexR}${hexG}${hexB}`;
13
+ }
14
+ function getLineHead({ element, type, doc }) {
7
15
  doc.lineWidth(element.height);
8
16
  doc.lineCap('round');
9
17
  doc.lineJoin('round');
10
18
  doc.opacity(element.opacity);
11
-
12
19
  const rgba = Util.colorToRGBA(element.color);
13
20
  const fillColor = rgbToHex(rgba);
14
-
15
21
  if (type === 'arrow') {
16
- doc.moveTo(element.height * 3, -element.height * 2)
22
+ doc
23
+ .moveTo(element.height * 3, -element.height * 2)
17
24
  .lineTo(0, 0)
18
25
  .lineTo(element.height * 3, element.height * 2);
19
- doc.stroke()
20
- return
21
-
22
- } else if (type === 'triangle') {
23
- doc.polygon([element.height * 3, -element.height * 2],
24
- [0, 0],
25
- [element.height * 3, element.height * 2]);
26
-
27
- } else if (type === 'bar') {
28
- doc.polygon([0, -element.height * 2],
29
- [0, 0],
30
- [0, element.height * 2]);
31
-
32
- } else if (type === 'circle') {
26
+ doc.stroke();
27
+ return;
28
+ }
29
+ else if (type === 'triangle') {
30
+ doc.polygon([element.height * 3, -element.height * 2], [0, 0], [element.height * 3, element.height * 2]);
31
+ }
32
+ else if (type === 'bar') {
33
+ doc.polygon([0, -element.height * 2], [0, 0], [0, element.height * 2]);
34
+ }
35
+ else if (type === 'circle') {
33
36
  doc.circle(element.height * 2, 0, element.height * 2);
34
-
35
- } else if (type === 'square') {
36
- doc.rect(0, -element.height * 2,
37
- element.height * 4,
38
- element.height * 4);
39
-
40
- } else {
41
- return null;
42
37
  }
43
-
38
+ else if (type === 'square') {
39
+ doc.rect(0, -element.height * 2, element.height * 4, element.height * 4);
40
+ }
41
+ else {
42
+ return;
43
+ }
44
44
  doc.fillAndStroke(fillColor, fillColor);
45
- };
46
-
47
- module.exports.lineToPDF = (doc, element) => {
45
+ }
46
+ export function lineToPDF(doc, element) {
48
47
  doc.translate(0, element.height / 2);
49
48
  doc.lineWidth(element.height);
50
49
  doc.moveTo(0, 0);
51
50
  doc.lineTo(element.width, 0);
52
-
53
51
  if (element.dash && element.dash.length > 0) {
54
- doc.dash(element.dash.map(dash => dash * element.height));
52
+ doc.dash(element.dash.map((dash) => dash * element.height));
55
53
  }
56
-
57
54
  const rgba = Util.colorToRGBA(element.color);
58
55
  doc.strokeColor(rgbToHex(rgba));
59
56
  doc.stroke();
60
-
61
57
  if (element.dash && element.dash.length > 0) {
62
58
  doc.undash();
63
59
  }
64
-
65
- getLineHead({element, doc: doc, type: element.startHead});
66
- getLineHead({element, doc: doc.translate(element.width, 0).rotate(180), type: element.endHead});
67
- }
68
-
69
- function rgbToHex({ r, g, b}) {
70
- // Ensure each value is within the valid range
71
- r = Math.max(0, Math.min(255, r));
72
- g = Math.max(0, Math.min(255, g));
73
- b = Math.max(0, Math.min(255, b));
74
-
75
- // Convert each value to a 2-digit hexadecimal string
76
- const hexR = r.toString(16).padStart(2, '0');
77
- const hexG = g.toString(16).padStart(2, '0');
78
- const hexB = b.toString(16).padStart(2, '0');
79
-
80
- // Return the concatenated hex string
81
- return `#${hexR}${hexG}${hexB}`;
60
+ getLineHead({ element, doc: doc, type: element.startHead });
61
+ getLineHead({
62
+ element,
63
+ doc: doc.translate(element.width, 0).rotate(180),
64
+ type: element.endHead,
65
+ });
82
66
  }
@@ -0,0 +1,38 @@
1
+ export interface SpotColorDefinition {
2
+ name?: string;
3
+ cmyk?: number[];
4
+ }
5
+ export interface SpotColorConfig {
6
+ [color: string]: SpotColorDefinition;
7
+ }
8
+ /**
9
+ * Normalize color to a consistent format for matching using Konva's color parser
10
+ * This ensures consistency with how Polotno/Konva handles colors
11
+ * @param color - Color in any format (string or [r, g, b, a] array)
12
+ * @returns Normalized color string in rgba format
13
+ */
14
+ export declare function normalizeColor(color: string | number[]): string | null;
15
+ /**
16
+ * Get spot color definition for a given color
17
+ * @param color - Color to check
18
+ * @param spotColorConfig - Spot color configuration
19
+ * @returns Spot color definition or null
20
+ */
21
+ export declare function getSpotColorForColor(color: string, spotColorConfig: SpotColorConfig): SpotColorDefinition | null;
22
+ /**
23
+ * Register a spot color using PDFKit's built-in addSpotColor method
24
+ * @param doc - PDFKit document
25
+ * @param spotName - Name of the spot color
26
+ * @param spotColorDef - Spot color definition with CMYK fallback
27
+ * @returns Reference to the registered spot color
28
+ */
29
+ export declare function registerSpotColor(doc: any, spotName: string, spotColorDef: SpotColorDefinition): {
30
+ name: string;
31
+ cmyk: number[];
32
+ };
33
+ /**
34
+ * Enable spot color support on a PDFDocument by intercepting color methods
35
+ * @param doc - PDFKit document
36
+ * @param spotColorConfig - Spot color configuration mapping
37
+ */
38
+ export declare function enableSpotColorSupport(doc: any, spotColorConfig: SpotColorConfig): void;