@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 +52 -0
- package/lib/figure.d.ts +10 -0
- package/lib/figure.js +53 -48
- package/lib/ghostscript.d.ts +21 -0
- package/lib/ghostscript.js +101 -128
- package/lib/group.d.ts +5 -0
- package/lib/group.js +4 -8
- package/lib/image.d.ts +19 -0
- package/lib/image.js +134 -89
- package/lib/index.d.ts +26 -0
- package/lib/index.js +130 -0
- package/lib/line.d.ts +10 -0
- package/lib/line.js +41 -57
- package/lib/spot-colors.d.ts +38 -0
- package/lib/spot-colors.js +141 -0
- package/lib/svg-render.d.ts +9 -0
- package/lib/svg-render.js +19 -28
- package/lib/svg.d.ts +11 -0
- package/lib/svg.js +203 -232
- package/lib/text.d.ts +28 -0
- package/lib/text.js +147 -174
- package/lib/utils.d.ts +15 -0
- package/lib/utils.js +88 -72
- package/package.json +22 -14
- package/index.js +0 -130
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
|
+
```
|
package/lib/figure.d.ts
ADDED
package/lib/figure.js
CHANGED
|
@@ -1,49 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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>;
|
package/lib/ghostscript.js
CHANGED
|
@@ -1,100 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
8
|
-
* @param
|
|
9
|
-
* @param
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
87
|
-
* @returns
|
|
77
|
+
* @param metadata - Metadata object
|
|
78
|
+
* @returns PostScript code
|
|
88
79
|
*/
|
|
89
80
|
function createMetadataPS(metadata) {
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
121
|
-
* @returns {Promise<boolean>}
|
|
104
|
+
* @param pdfPath - Path to PDF file
|
|
122
105
|
*/
|
|
123
|
-
async function validatePDFX1a(pdfPath) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
package/lib/group.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
async function renderGroup(doc, element, renderElement, fonts, attrs) {
|
|
2
|
-
|
|
3
|
-
|
|
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>;
|