@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/README.md CHANGED
@@ -49,9 +49,61 @@ await jsonToPDF(json, './book-cover.pdf', {
49
49
  });
50
50
  ```
51
51
 
52
+ ## Spot Color / Foil Support
53
+
54
+ For professional printing with special inks like metallic foils, Pantone colors, or other spot colors:
55
+
56
+ ```js
57
+ await jsonToPDF(json, './output.pdf', {
58
+ pdfx1a: true,
59
+ spotColors: {
60
+ // Map any color to a spot color
61
+ 'rgba(255,215,0,1)': {
62
+ name: 'Gold Foil',
63
+ type: 'pantone',
64
+ pantoneCode: 'Pantone 871 C', // Optional reference
65
+ cmyk: [0, 0.15, 0.5, 0], // CMYK fallback (0-1 range)
66
+ },
67
+ '#C0C0C0': {
68
+ name: 'Silver Foil',
69
+ type: 'custom',
70
+ cmyk: [0, 0, 0, 0.25],
71
+ },
72
+ },
73
+ });
74
+ ```
75
+
76
+ **How it works:**
77
+
78
+ - Any element with a matching fill or stroke color is automatically converted to use the spot color
79
+ - Colors are matched flexibly - `'#FFD700'`, `'#ffd700'`, and `'rgba(255,215,0,1)'` all match
80
+ - Spot colors are preserved as separation color spaces in PDF/X-1a output
81
+ - CMYK fallback values are used when viewers don't support spot colors
82
+ - Works with all element types: text, shapes, lines, and SVG elements
83
+
84
+ **Color Format Support:**
85
+
86
+ - Hex: `'#FFD700'` or `'#ffd700'`
87
+ - RGB: `'rgb(255,215,0)'`
88
+ - RGBA: `'rgba(255,215,0,1)'`
89
+
90
+ **Tips:**
91
+
92
+ - Use professional color references (like Pantone codes) to communicate exact requirements to printers
93
+ - Provide accurate CMYK fallback values for preview and proof prints
94
+ - Spot colors work best with PDF/X-1a export enabled
95
+ - You can verify spot colors in Adobe Acrobat by checking Output Preview > Separations
96
+
52
97
  ## Requirements
53
98
 
54
99
  - **GhostScript** must be installed for PDF/X-1a conversion
55
100
  - macOS: `brew install ghostscript`
56
101
  - Ubuntu: `apt-get install ghostscript`
57
102
  - Windows: Download from [ghostscript.com](https://www.ghostscript.com/)
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ npm run build # Build the library
108
+ npm test # Run tests
109
+ ```
@@ -0,0 +1,10 @@
1
+ export interface FigureElement {
2
+ fill?: string;
3
+ stroke?: string;
4
+ strokeWidth: number;
5
+ opacity: number;
6
+ width: number;
7
+ height: number;
8
+ [key: string]: any;
9
+ }
10
+ export declare function renderFigure(doc: any, element: FigureElement): void;
package/lib/figure.js CHANGED
@@ -1,49 +1,54 @@
1
- const { parseColor } = require('./utils');
2
- const { figureToSvg } = require('polotno/utils/figure-to-svg');
3
-
4
- function renderFigure(doc, element) {
5
- const svgStr = figureToSvg({
6
- ...element,
7
- fill: (() => {
8
- if (!element.fill) return 'transparent';
9
- const color = parseColor(element.fill);
10
- if (!color || !color.rgba) return element.fill;
11
- const oldOpacity = color.rgba[3] || 1;
12
- const rgba = color.rgba
13
- .slice(0, 3)
14
- .concat(oldOpacity * element.opacity)
15
- .join(',');
16
- return `rgba(${rgba})`;
17
- })(),
18
- stroke: (() => {
19
- if (!element.stroke || element.strokeWidth === 0) return 'none';
20
- const color = parseColor(element.stroke);
21
- if (!color || !color.rgba) return element.stroke;
22
- const oldOpacity = color.rgba[3] || 1;
23
- const rgba = color.rgba
24
- .slice(0, 3)
25
- .concat(oldOpacity * element.opacity)
26
- .join(',');
27
- return `rgba(${rgba})`;
28
- })(),
29
- });
30
-
31
- doc.addSVG(svgStr, 0, 0, {
32
- preserveAspectRatio: 'xMinYMin meet',
33
- width: element.width,
34
- height: element.height,
35
- opacity: element.opacity,
36
- colorCallback: (colors) => {
37
- console.log('colors', colors);
38
- if (!colors) {
39
- return colors;
40
- }
41
- const [color, opacity] = colors;
42
- return [color, opacity * element.opacity];
43
- },
44
- });
1
+ import { parseColor } from './utils.js';
2
+ import { figureToSvg } from 'polotno/utils/figure-to-svg';
3
+ export function renderFigure(doc, element) {
4
+ const svgStr = figureToSvg({
5
+ ...element,
6
+ fill: (() => {
7
+ if (!element.fill)
8
+ return 'transparent';
9
+ const color = parseColor(element.fill);
10
+ if (!color || !color.rgba)
11
+ return element.fill;
12
+ let oldOpacity = color.rgba[3] || 1;
13
+ // Normalize opacity to 0-1 range if it's greater than 1
14
+ if (oldOpacity > 1) {
15
+ oldOpacity = oldOpacity / 100;
16
+ }
17
+ const rgba = color.rgba
18
+ .slice(0, 3)
19
+ .concat(oldOpacity * element.opacity)
20
+ .join(',');
21
+ return `rgba(${rgba})`;
22
+ })(),
23
+ stroke: (() => {
24
+ if (!element.stroke || element.strokeWidth === 0)
25
+ return 'none';
26
+ const color = parseColor(element.stroke);
27
+ if (!color || !color.rgba)
28
+ return element.stroke;
29
+ let oldOpacity = color.rgba[3] || 1;
30
+ // Normalize opacity to 0-1 range if it's greater than 1
31
+ if (oldOpacity > 1) {
32
+ oldOpacity = oldOpacity / 100;
33
+ }
34
+ const rgba = color.rgba
35
+ .slice(0, 3)
36
+ .concat(oldOpacity * element.opacity)
37
+ .join(',');
38
+ return `rgba(${rgba})`;
39
+ })(),
40
+ });
41
+ doc.addSVG(svgStr, 0, 0, {
42
+ preserveAspectRatio: 'xMinYMin meet',
43
+ width: element.width,
44
+ height: element.height,
45
+ opacity: element.opacity,
46
+ colorCallback: (colors) => {
47
+ if (!colors) {
48
+ return colors;
49
+ }
50
+ const [color, opacity] = colors;
51
+ return [color, opacity * element.opacity];
52
+ },
53
+ });
45
54
  }
46
-
47
- module.exports = {
48
- renderFigure,
49
- };
@@ -0,0 +1,21 @@
1
+ export interface PDFXMetadata {
2
+ title?: string;
3
+ author?: string;
4
+ application?: string;
5
+ producer?: string;
6
+ }
7
+ export interface ConversionOptions {
8
+ metadata?: PDFXMetadata;
9
+ }
10
+ /**
11
+ * Convert PDF to PDF/X-1a using GhostScript
12
+ * @param inputPath - Path to input PDF
13
+ * @param outputPath - Path to output PDF/X-1a file
14
+ * @param options - Conversion options
15
+ */
16
+ export declare function convertToPDFX1a(inputPath: string, outputPath: string, options?: ConversionOptions): Promise<void>;
17
+ /**
18
+ * Validate if PDF is PDF/X-1a compliant
19
+ * @param pdfPath - Path to PDF file
20
+ */
21
+ export declare function validatePDFX1a(pdfPath: string): Promise<boolean>;
@@ -1,100 +1,85 @@
1
- const { spawn } = require('child_process');
2
- const fs = require('fs');
3
- const path = require('path');
4
-
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
5
4
  /**
6
5
  * Convert PDF to PDF/X-1a using GhostScript
7
- * @param {string} inputPath - Path to input PDF
8
- * @param {string} outputPath - Path to output PDF/X-1a file
9
- * @param {Object} options - Conversion options
10
- * @returns {Promise<void>}
6
+ * @param inputPath - Path to input PDF
7
+ * @param outputPath - Path to output PDF/X-1a file
8
+ * @param options - Conversion options
11
9
  */
12
- async function convertToPDFX1a(inputPath, outputPath, options = {}) {
13
- return new Promise((resolve, reject) => {
14
- // PDF/X-1a conversion parameters with stroke preservation
15
- const args = [
16
- '-dNOPAUSE',
17
- '-dBATCH',
18
- '-dSAFER',
19
- '-sDEVICE=pdfwrite',
20
- '-dCompatibilityLevel=1.3',
21
- '-dPDFX=true',
22
- '-dColorConversionStrategy=/CMYK',
23
- '-dProcessColorModel=/DeviceCMYK',
24
- '-dUseCIEColor=true',
25
- '-sColorConversionStrategy=CMYK',
26
- '-sProcessColorModel=DeviceCMYK',
27
- '-dOverrideICC=true',
28
- // Preserve text rendering modes and strokes
29
- '-dPreserveAnnots=true',
30
- '-dPreserveCopyPage=true',
31
- '-dPreserveDeviceN=true',
32
- '-dDoThumbnails=false',
33
- '-dDetectDuplicateImages=false',
34
- '-dCompressFonts=false',
35
- '-dSubsetFonts=false',
36
- '-dEmbedAllFonts=true',
37
- // Better handling of text rendering modes
38
- '-dPDFSETTINGS=/prepress',
39
- `-sOutputFile=${outputPath}`,
40
- inputPath,
41
- ];
42
-
43
- // Add custom metadata if provided
44
- if (options.metadata) {
45
- // Create temporary PostScript file with metadata
46
- const psMetadata = createMetadataPS(options.metadata);
47
- const tempPSFile = path.join(path.dirname(outputPath), 'metadata.ps');
48
- fs.writeFileSync(tempPSFile, psMetadata);
49
- args.splice(-1, 0, tempPSFile); // Insert before input file
50
- }
51
-
52
- console.log('GhostScript command:', 'gs', args.join(' '));
53
-
54
- const gs = spawn('gs', args);
55
- let stderr = '';
56
-
57
- gs.stderr.on('data', (data) => {
58
- stderr += data.toString();
59
- });
60
-
61
- gs.on('close', (code) => {
62
- // Clean up temp metadata file
63
- if (options.metadata) {
64
- const tempPSFile = path.join(path.dirname(outputPath), 'metadata.ps');
65
- if (fs.existsSync(tempPSFile)) {
66
- fs.unlinkSync(tempPSFile);
10
+ export async function convertToPDFX1a(inputPath, outputPath, options = {}) {
11
+ return new Promise((resolve, reject) => {
12
+ // PDF/X-1a conversion parameters with stroke preservation
13
+ const args = [
14
+ '-dNOPAUSE',
15
+ '-dBATCH',
16
+ '-dSAFER',
17
+ '-sDEVICE=pdfwrite',
18
+ '-dCompatibilityLevel=1.3',
19
+ '-dPDFX=true',
20
+ '-dColorConversionStrategy=/CMYK',
21
+ '-dProcessColorModel=/DeviceCMYK',
22
+ '-dUseCIEColor=true',
23
+ '-sColorConversionStrategy=CMYK',
24
+ '-sProcessColorModel=DeviceCMYK',
25
+ '-dOverrideICC=true',
26
+ // Preserve text rendering modes and strokes
27
+ '-dPreserveAnnots=true',
28
+ '-dPreserveCopyPage=true',
29
+ '-dPreserveDeviceN=true', // IMPORTANT: Preserves spot/separation colors
30
+ '-dDoThumbnails=false',
31
+ '-dDetectDuplicateImages=true', // Enable duplicate image detection
32
+ '-dCompressFonts=true', // Enable font compression
33
+ '-dSubsetFonts=true', // Subset fonts to reduce size
34
+ '-dEmbedAllFonts=true',
35
+ // Better handling of text rendering modes
36
+ '-dPDFSETTINGS=/prepress',
37
+ `-sOutputFile=${outputPath}`,
38
+ inputPath,
39
+ ];
40
+ // Add custom metadata if provided
41
+ if (options.metadata) {
42
+ // Create temporary PostScript file with metadata
43
+ const psMetadata = createMetadataPS(options.metadata);
44
+ const tempPSFile = path.join(path.dirname(outputPath), 'metadata.ps');
45
+ fs.writeFileSync(tempPSFile, psMetadata);
46
+ args.splice(-1, 0, tempPSFile); // Insert before input file
67
47
  }
68
- }
69
-
70
- if (code === 0) {
71
- console.log('PDF/X-1a conversion successful');
72
- resolve();
73
- } else {
74
- reject(new Error(`GhostScript failed with code ${code}: ${stderr}`));
75
- }
76
- });
77
-
78
- gs.on('error', (error) => {
79
- reject(new Error(`Failed to start GhostScript: ${error.message}`));
48
+ console.log('GhostScript command:', 'gs', args.join(' '));
49
+ const gs = spawn('gs', args);
50
+ let stderr = '';
51
+ gs.stderr.on('data', (data) => {
52
+ stderr += data.toString();
53
+ });
54
+ gs.on('close', (code) => {
55
+ // Clean up temp metadata file
56
+ if (options.metadata) {
57
+ const tempPSFile = path.join(path.dirname(outputPath), 'metadata.ps');
58
+ if (fs.existsSync(tempPSFile)) {
59
+ fs.unlinkSync(tempPSFile);
60
+ }
61
+ }
62
+ if (code === 0) {
63
+ console.log('PDF/X-1a conversion successful');
64
+ resolve();
65
+ }
66
+ else {
67
+ reject(new Error(`GhostScript failed with code ${code}: ${stderr}`));
68
+ }
69
+ });
70
+ gs.on('error', (error) => {
71
+ reject(new Error(`Failed to start GhostScript: ${error.message}`));
72
+ });
80
73
  });
81
- });
82
74
  }
83
-
84
75
  /**
85
76
  * Create PostScript metadata for PDF/X-1a
86
- * @param {Object} metadata - Metadata object
87
- * @returns {string} PostScript code
77
+ * @param metadata - Metadata object
78
+ * @returns PostScript code
88
79
  */
89
80
  function createMetadataPS(metadata) {
90
- const {
91
- title = 'Cover Creator Export',
92
- author = 'KDP Cover Creator',
93
- application = 'KDP CoverCreator',
94
- producer = 'Polotno PDF Export',
95
- } = metadata;
96
-
97
- return `
81
+ const { title = 'Cover Creator Export', author = 'KDP Cover Creator', application = 'KDP CoverCreator', producer = 'Polotno PDF Export', } = metadata;
82
+ return `
98
83
  %!PS
99
84
  % PDF/X-1a Metadata
100
85
  [/Title (${title})
@@ -114,49 +99,37 @@ function createMetadataPS(metadata) {
114
99
  /PDFX pdfmark
115
100
  `;
116
101
  }
117
-
118
102
  /**
119
103
  * Validate if PDF is PDF/X-1a compliant
120
- * @param {string} pdfPath - Path to PDF file
121
- * @returns {Promise<boolean>}
104
+ * @param pdfPath - Path to PDF file
122
105
  */
123
- async function validatePDFX1a(pdfPath) {
124
- return new Promise((resolve, reject) => {
125
- const args = [
126
- '-dNOPAUSE',
127
- '-dBATCH',
128
- '-sDEVICE=nullpage',
129
- '-dPDFX=true',
130
- pdfPath,
131
- ];
132
-
133
- const gs = spawn('gs', args);
134
- let stderr = '';
135
-
136
- gs.stderr.on('data', (data) => {
137
- stderr += data.toString();
106
+ export async function validatePDFX1a(pdfPath) {
107
+ return new Promise((resolve, reject) => {
108
+ const args = [
109
+ '-dNOPAUSE',
110
+ '-dBATCH',
111
+ '-sDEVICE=nullpage',
112
+ '-dPDFX=true',
113
+ pdfPath,
114
+ ];
115
+ const gs = spawn('gs', args);
116
+ let stderr = '';
117
+ gs.stderr.on('data', (data) => {
118
+ stderr += data.toString();
119
+ });
120
+ gs.on('close', (code) => {
121
+ if (code === 0 &&
122
+ !stderr.includes('Error') &&
123
+ !stderr.includes('Warning')) {
124
+ resolve(true);
125
+ }
126
+ else {
127
+ console.log('PDF/X-1a validation issues:', stderr);
128
+ resolve(false);
129
+ }
130
+ });
131
+ gs.on('error', (error) => {
132
+ reject(new Error(`Failed to validate PDF: ${error.message}`));
133
+ });
138
134
  });
139
-
140
- gs.on('close', (code) => {
141
- if (
142
- code === 0 &&
143
- !stderr.includes('Error') &&
144
- !stderr.includes('Warning')
145
- ) {
146
- resolve(true);
147
- } else {
148
- console.log('PDF/X-1a validation issues:', stderr);
149
- resolve(false);
150
- }
151
- });
152
-
153
- gs.on('error', (error) => {
154
- reject(new Error(`Failed to validate PDF: ${error.message}`));
155
- });
156
- });
157
135
  }
158
-
159
- module.exports = {
160
- convertToPDFX1a,
161
- validatePDFX1a,
162
- };
package/lib/group.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface GroupElement {
2
+ children: any[];
3
+ [key: string]: any;
4
+ }
5
+ export declare function renderGroup(doc: any, element: GroupElement, renderElement: Function, fonts: Record<string, boolean>, attrs: any, cache: any): Promise<void>;
package/lib/group.js CHANGED
@@ -1,9 +1,5 @@
1
- async function renderGroup(doc, element, renderElement, fonts, attrs) {
2
- for (const child of element.children) {
3
- await renderElement({ doc, element: child, fonts, attrs });
4
- }
1
+ export async function renderGroup(doc, element, renderElement, fonts, attrs, cache) {
2
+ for (const child of element.children) {
3
+ await renderElement({ doc, element: child, fonts, attrs, cache });
4
+ }
5
5
  }
6
-
7
- module.exports = {
8
- renderGroup,
9
- };
package/lib/image.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { ImageCache } from './utils.js';
2
+ export interface ImageElement {
3
+ src: string;
4
+ width: number;
5
+ height: number;
6
+ cropX: number;
7
+ cropY: number;
8
+ cropWidth: number;
9
+ cropHeight: number;
10
+ clipSrc?: string;
11
+ type?: string;
12
+ opacity?: number;
13
+ flipX?: boolean;
14
+ flipY?: boolean;
15
+ }
16
+ export declare function getProcessedImageKey(element: ImageElement): string;
17
+ export declare function cropImage(src: string, element: ImageElement, cache?: ImageCache | null): Promise<string>;
18
+ export declare function clipImage(src: string, element: ImageElement, cache?: ImageCache | null): Promise<string>;
19
+ export declare function renderImage(doc: any, element: ImageElement, cache?: ImageCache | null): Promise<void>;