@polotno/pdf-export 0.1.38 → 0.1.39
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 +61 -8
- package/lib/index.d.ts +66 -8
- package/lib/index.js +25 -145
- package/package.json +17 -18
- package/lib/compare-render.d.ts +0 -1
- package/lib/compare-render.js +0 -185
- package/lib/figure.d.ts +0 -10
- package/lib/figure.js +0 -54
- package/lib/filters.d.ts +0 -2
- package/lib/filters.js +0 -163
- package/lib/ghostscript.d.ts +0 -21
- package/lib/ghostscript.js +0 -132
- package/lib/group.d.ts +0 -5
- package/lib/group.js +0 -5
- package/lib/image.d.ts +0 -38
- package/lib/image.js +0 -279
- package/lib/line.d.ts +0 -10
- package/lib/line.js +0 -66
- package/lib/pdf-import/coordinate-transform.d.ts +0 -51
- package/lib/pdf-import/coordinate-transform.js +0 -99
- package/lib/pdf-import/element-builder.d.ts +0 -21
- package/lib/pdf-import/element-builder.js +0 -163
- package/lib/pdf-import/font-mapper.d.ts +0 -17
- package/lib/pdf-import/font-mapper.js +0 -142
- package/lib/pdf-import/index.d.ts +0 -35
- package/lib/pdf-import/index.js +0 -105
- package/lib/pdf-import/parser.d.ts +0 -29
- package/lib/pdf-import/parser.js +0 -285
- package/lib/pdf-import/text-analysis.d.ts +0 -17
- package/lib/pdf-import/text-analysis.js +0 -186
- package/lib/pdf-import/types.d.ts +0 -101
- package/lib/pdf-import/types.js +0 -1
- package/lib/scripts/compare-json.d.ts +0 -1
- package/lib/scripts/compare-json.js +0 -141
- package/lib/spot-colors.d.ts +0 -38
- package/lib/spot-colors.js +0 -141
- package/lib/svg-render.d.ts +0 -9
- package/lib/svg-render.js +0 -63
- package/lib/svg.d.ts +0 -12
- package/lib/svg.js +0 -224
- package/lib/text/fonts.d.ts +0 -16
- package/lib/text/fonts.js +0 -113
- package/lib/text/index.d.ts +0 -8
- package/lib/text/index.js +0 -42
- package/lib/text/layout.d.ts +0 -22
- package/lib/text/layout.js +0 -522
- package/lib/text/parser.d.ts +0 -46
- package/lib/text/parser.js +0 -415
- package/lib/text/render.d.ts +0 -8
- package/lib/text/render.js +0 -237
- package/lib/text/types.d.ts +0 -91
- package/lib/text/types.js +0 -1
- package/lib/text.d.ts +0 -39
- package/lib/text.js +0 -576
- package/lib/utils.d.ts +0 -16
- package/lib/utils.js +0 -124
package/lib/compare-render.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Compare PDFs produced by polotno-node vs this library for a subset of samples.
|
|
3
|
-
|
|
4
|
-
Usage examples:
|
|
5
|
-
node lib/compare-render.js --limit 10
|
|
6
|
-
node lib/compare-render.js --glob "2021-03-*"
|
|
7
|
-
node lib/compare-render.js --start 0 --end 50
|
|
8
|
-
|
|
9
|
-
Outputs:
|
|
10
|
-
- builds PDFs under `comparisons/<sample>/polotno-node.pdf` and `comparisons/<sample>/current.pdf`
|
|
11
|
-
- writes a simple byte-size diff report to `comparisons/report.json`
|
|
12
|
-
*/
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import crypto from 'crypto';
|
|
16
|
-
import { promisify } from 'util';
|
|
17
|
-
import { createInstance } from 'polotno-node';
|
|
18
|
-
import { pdfToPng } from 'pdf-to-png-converter';
|
|
19
|
-
import pixelmatch from 'pixelmatch';
|
|
20
|
-
import { PNG } from 'pngjs';
|
|
21
|
-
import { jsonToPDF } from './index.js';
|
|
22
|
-
const readFile = promisify(fs.readFile);
|
|
23
|
-
const writeFile = promisify(fs.writeFile);
|
|
24
|
-
const mkdir = promisify(fs.mkdir);
|
|
25
|
-
const access = promisify(fs.access);
|
|
26
|
-
const readdir = promisify(fs.readdir);
|
|
27
|
-
const SAMPLES_DIR = path.resolve('./samples');
|
|
28
|
-
const OUTPUT_DIR = path.resolve('./comparisons');
|
|
29
|
-
function parseArgs() {
|
|
30
|
-
const args = process.argv.slice(2);
|
|
31
|
-
const result = {};
|
|
32
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
33
|
-
const a = args[i];
|
|
34
|
-
if (a === '--limit')
|
|
35
|
-
result.limit = Number(args[++i]);
|
|
36
|
-
else if (a === '--start')
|
|
37
|
-
result.start = Number(args[++i]);
|
|
38
|
-
else if (a === '--end')
|
|
39
|
-
result.end = Number(args[++i]);
|
|
40
|
-
else if (a === '--glob')
|
|
41
|
-
result.glob = String(args[++i]);
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
async function tryEnsureDir(dir) {
|
|
46
|
-
try {
|
|
47
|
-
await access(dir, fs.constants.F_OK);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
await mkdir(dir, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
function sha256(buf) {
|
|
54
|
-
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
55
|
-
}
|
|
56
|
-
async function loadPolotnoJson(filePath) {
|
|
57
|
-
const buf = await readFile(filePath, 'utf8');
|
|
58
|
-
return JSON.parse(buf);
|
|
59
|
-
}
|
|
60
|
-
let polotnoInstance = null;
|
|
61
|
-
async function getPolotnoInstance() {
|
|
62
|
-
if (polotnoInstance)
|
|
63
|
-
return polotnoInstance;
|
|
64
|
-
const key = process.env.POLOTNO_API_KEY || process.env.POLOTNO_API_TOKEN || '';
|
|
65
|
-
polotnoInstance = await createInstance({ key });
|
|
66
|
-
return polotnoInstance;
|
|
67
|
-
}
|
|
68
|
-
async function renderWithPolotnoNode(json, outPath) {
|
|
69
|
-
const instance = await getPolotnoInstance();
|
|
70
|
-
const base64 = await instance.jsonToPDFBase64(json, {
|
|
71
|
-
pixelRatio: 4,
|
|
72
|
-
});
|
|
73
|
-
await writeFile(outPath, Buffer.from(base64, 'base64'));
|
|
74
|
-
}
|
|
75
|
-
async function renderWithCurrent(json, outPath) {
|
|
76
|
-
await jsonToPDF(json, outPath, {});
|
|
77
|
-
}
|
|
78
|
-
function pickSampleDirs(allDirs, { start = 0, end, limit, glob }) {
|
|
79
|
-
let list = allDirs.filter((d) => !d.startsWith('.'));
|
|
80
|
-
if (glob) {
|
|
81
|
-
const re = new RegExp('^' + glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
|
|
82
|
-
list = list.filter((d) => re.test(d));
|
|
83
|
-
}
|
|
84
|
-
if (typeof start === 'number' && typeof end === 'number' && end > start) {
|
|
85
|
-
list = list.slice(start, end);
|
|
86
|
-
}
|
|
87
|
-
if (typeof limit === 'number' && limit > 0) {
|
|
88
|
-
list = list.slice(0, limit);
|
|
89
|
-
}
|
|
90
|
-
return list;
|
|
91
|
-
}
|
|
92
|
-
async function run() {
|
|
93
|
-
const args = parseArgs();
|
|
94
|
-
await tryEnsureDir(OUTPUT_DIR);
|
|
95
|
-
const all = await readdir(SAMPLES_DIR, { withFileTypes: true });
|
|
96
|
-
const sampleDirs = pickSampleDirs(all.filter((d) => d.isDirectory()).map((d) => d.name), args);
|
|
97
|
-
const report = [];
|
|
98
|
-
for (const dir of sampleDirs) {
|
|
99
|
-
const samplePath = path.join(SAMPLES_DIR, dir);
|
|
100
|
-
const jsonFiles = (await readdir(samplePath)).filter((f) => f.endsWith('.json') && f !== 'meta.json');
|
|
101
|
-
if (jsonFiles.length === 0)
|
|
102
|
-
continue;
|
|
103
|
-
const designPath = path.join(samplePath, jsonFiles[0]);
|
|
104
|
-
const json = await loadPolotnoJson(designPath);
|
|
105
|
-
const outDir = path.join(OUTPUT_DIR, dir);
|
|
106
|
-
await tryEnsureDir(outDir);
|
|
107
|
-
const polotnoOut = path.join(outDir, 'polotno-node.pdf');
|
|
108
|
-
const currentOut = path.join(outDir, 'current.pdf');
|
|
109
|
-
if (!fs.existsSync(polotnoOut)) {
|
|
110
|
-
// Render both
|
|
111
|
-
try {
|
|
112
|
-
await renderWithPolotnoNode(json, polotnoOut);
|
|
113
|
-
}
|
|
114
|
-
catch (e) {
|
|
115
|
-
await writeFile(path.join(outDir, 'error-polotno.txt'), String(e?.stack || e?.message || e));
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
await renderWithCurrent(json, currentOut);
|
|
121
|
-
}
|
|
122
|
-
catch (e) {
|
|
123
|
-
await writeFile(path.join(outDir, 'error-current.txt'), String(e?.stack || e?.message || e));
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
// Rasterize first page of each PDF to PNG (via pdf-to-png-converter)
|
|
127
|
-
try {
|
|
128
|
-
const [imgA] = await pdfToPng(polotnoOut, { disableFontFace: true, viewportScale: 2, pagesToProcess: [1] });
|
|
129
|
-
const [imgB] = await pdfToPng(currentOut, { disableFontFace: true, viewportScale: 2, pagesToProcess: [1] });
|
|
130
|
-
const pngA = imgA.content;
|
|
131
|
-
const pngB = imgB.content;
|
|
132
|
-
const pngAPath = path.join(outDir, 'polotno-node.png');
|
|
133
|
-
const pngBPath = path.join(outDir, 'current.png');
|
|
134
|
-
await writeFile(pngAPath, pngA);
|
|
135
|
-
await writeFile(pngBPath, pngB);
|
|
136
|
-
// Visual diff with pixelmatch
|
|
137
|
-
const imgAParsed = PNG.sync.read(pngA);
|
|
138
|
-
const imgBParsed = PNG.sync.read(pngB);
|
|
139
|
-
const width = Math.min(imgAParsed.width, imgBParsed.width);
|
|
140
|
-
const height = Math.min(imgAParsed.height, imgBParsed.height);
|
|
141
|
-
const cropA = new PNG({ width, height });
|
|
142
|
-
const cropB = new PNG({ width, height });
|
|
143
|
-
PNG.bitblt(imgAParsed, cropA, 0, 0, width, height, 0, 0);
|
|
144
|
-
PNG.bitblt(imgBParsed, cropB, 0, 0, width, height, 0, 0);
|
|
145
|
-
const diff = new PNG({ width, height });
|
|
146
|
-
const numDiff = pixelmatch(cropA.data, cropB.data, diff.data, width, height, {
|
|
147
|
-
threshold: 0.1,
|
|
148
|
-
includeAA: true,
|
|
149
|
-
});
|
|
150
|
-
const diffPath = path.join(outDir, 'diff.png');
|
|
151
|
-
await writeFile(diffPath, PNG.sync.write(diff));
|
|
152
|
-
const a = await readFile(polotnoOut);
|
|
153
|
-
const b = await readFile(currentOut);
|
|
154
|
-
const item = {
|
|
155
|
-
sample: dir,
|
|
156
|
-
polotnoBytes: a.length,
|
|
157
|
-
currentBytes: b.length,
|
|
158
|
-
bytesDiff: Math.abs(a.length - b.length),
|
|
159
|
-
polotnoSha256: sha256(a),
|
|
160
|
-
currentSha256: sha256(b),
|
|
161
|
-
identical: numDiff === 0,
|
|
162
|
-
diffPixels: numDiff,
|
|
163
|
-
imageWidth: width,
|
|
164
|
-
imageHeight: height,
|
|
165
|
-
};
|
|
166
|
-
report.push(item);
|
|
167
|
-
await writeFile(path.join(outDir, 'summary.json'), JSON.stringify(item, null, 2));
|
|
168
|
-
console.log(`${dir}: diffPixels=${numDiff} sizeA=${item.polotnoBytes} sizeB=${item.currentBytes}`);
|
|
169
|
-
}
|
|
170
|
-
catch (e) {
|
|
171
|
-
await writeFile(path.join(outDir, 'error-diff.txt'), String(e?.stack || e?.message || e));
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
await writeFile(path.join(OUTPUT_DIR, 'report.json'), JSON.stringify(report, null, 2));
|
|
175
|
-
console.log(`Done. Compared ${report.length} samples. Report: ${path.join(OUTPUT_DIR, 'report.json')}`);
|
|
176
|
-
// Close polotno-node instance to allow process to exit
|
|
177
|
-
if (polotnoInstance) {
|
|
178
|
-
await polotnoInstance.close();
|
|
179
|
-
polotnoInstance = null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
run().catch((err) => {
|
|
183
|
-
console.error(err);
|
|
184
|
-
process.exitCode = 1;
|
|
185
|
-
});
|
package/lib/figure.d.ts
DELETED
package/lib/figure.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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
|
-
});
|
|
54
|
-
}
|
package/lib/filters.d.ts
DELETED
package/lib/filters.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
export const elementFilterToKonva = {
|
|
2
|
-
warm: (intensity) => (imageData) => {
|
|
3
|
-
const data = imageData.data; // Access pixel data
|
|
4
|
-
// Intensity should be in 0-1 range from the model
|
|
5
|
-
intensity = Math.max(0, Math.min(1, intensity));
|
|
6
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
7
|
-
// Increase red and green values for a warm effect
|
|
8
|
-
data[i] = Math.min(data[i] + 30 * intensity, 255); // Red channel
|
|
9
|
-
data[i + 1] = Math.min(data[i + 1] + 15 * intensity, 255); // Green channel
|
|
10
|
-
// Leave blue unchanged for a warm tone
|
|
11
|
-
}
|
|
12
|
-
return imageData;
|
|
13
|
-
},
|
|
14
|
-
cold: (intensity) => (imageData) => {
|
|
15
|
-
const data = imageData.data; // Access pixel data
|
|
16
|
-
// Intensity should be in 0-1 range from the model
|
|
17
|
-
intensity = Math.max(0, Math.min(1, intensity));
|
|
18
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
19
|
-
// Adjust red, green, and blue channels
|
|
20
|
-
data[i] = Math.min(data[i] - 15 * intensity, 255); // Reduce red
|
|
21
|
-
data[i + 1] = Math.min(data[i + 1] - 10 * intensity, 255); // Slightly reduce green
|
|
22
|
-
data[i + 2] = Math.min(data[i + 2] + 15 * intensity, 255); // Boost blue
|
|
23
|
-
}
|
|
24
|
-
return imageData;
|
|
25
|
-
},
|
|
26
|
-
natural: (intensity) => (imageData) => {
|
|
27
|
-
const data = imageData.data; // Access pixel data
|
|
28
|
-
// Intensity should be in 0-1 range from the model
|
|
29
|
-
intensity = Math.max(0, Math.min(1, intensity));
|
|
30
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
31
|
-
// Adjust each channel (R, G, B)
|
|
32
|
-
data[i] = Math.min(data[i] * (1 + 0.1 * intensity), 255); // Enhance red
|
|
33
|
-
data[i + 1] = Math.min(data[i + 1] * (1 + 0.1 * intensity), 255); // Enhance green
|
|
34
|
-
data[i + 2] = Math.min(data[i + 2] * (1 + 0.1 * intensity), 255); // Enhance blue
|
|
35
|
-
// Increase contrast based on intensity
|
|
36
|
-
const average = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
37
|
-
data[i] = Math.min((data[i] - average) * (1 + 0.2 * intensity) + average, 255);
|
|
38
|
-
data[i + 1] = Math.min((data[i + 1] - average) * (1 + 0.2 * intensity) + average, 255);
|
|
39
|
-
data[i + 2] = Math.min((data[i + 2] - average) * (1 + 0.2 * intensity) + average, 255);
|
|
40
|
-
}
|
|
41
|
-
return imageData;
|
|
42
|
-
},
|
|
43
|
-
temperature: (intensity) => (imageData) => {
|
|
44
|
-
const data = imageData.data;
|
|
45
|
-
// Intensity should be in -1 to 1 range from the model
|
|
46
|
-
intensity = Math.max(-1, Math.min(1, intensity));
|
|
47
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
48
|
-
const r = data[i];
|
|
49
|
-
const g = data[i + 1];
|
|
50
|
-
const b = data[i + 2];
|
|
51
|
-
// Adjust red and blue channels based on temperature
|
|
52
|
-
// Warmer = more red, less blue
|
|
53
|
-
// Colder = more blue, less red
|
|
54
|
-
data[i] = Math.min(Math.max(r + 15 * intensity, 0), 255); // Red channel
|
|
55
|
-
data[i + 2] = Math.min(Math.max(b - 15 * intensity, 0), 255); // Blue channel
|
|
56
|
-
// Optionally: slight tweak to green for balance (optional)
|
|
57
|
-
// data[i + 1] = Math.min(Math.max(g + 10 * intensity, 0), 255);
|
|
58
|
-
}
|
|
59
|
-
return imageData;
|
|
60
|
-
},
|
|
61
|
-
contrast: (intensity) => (imageData) => {
|
|
62
|
-
const data = imageData.data;
|
|
63
|
-
// Intensity should be in -1 to 1 range from the model
|
|
64
|
-
intensity = Math.max(-1, Math.min(1, intensity));
|
|
65
|
-
// Convert intensity to a contrast factor
|
|
66
|
-
// Scale from -1,1 to -100,100 for the formula
|
|
67
|
-
const scaledIntensity = intensity * 100;
|
|
68
|
-
const factor = (259 * (scaledIntensity + 255)) / (255 * (259 - scaledIntensity));
|
|
69
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
70
|
-
// Apply contrast formula to R, G, B channels
|
|
71
|
-
data[i] = Math.min(Math.max(factor * (data[i] - 128) + 128, 0), 255); // Red
|
|
72
|
-
data[i + 1] = Math.min(Math.max(factor * (data[i + 1] - 128) + 128, 0), 255); // Green
|
|
73
|
-
data[i + 2] = Math.min(Math.max(factor * (data[i + 2] - 128) + 128, 0), 255); // Blue
|
|
74
|
-
}
|
|
75
|
-
return imageData;
|
|
76
|
-
},
|
|
77
|
-
shadows: (intensity) => (imageData) => {
|
|
78
|
-
const data = imageData.data;
|
|
79
|
-
// Intensity should be in -1 to 1 range from the model
|
|
80
|
-
intensity = Math.max(-1, Math.min(1, intensity));
|
|
81
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
82
|
-
// Compute brightness as average of R, G, B
|
|
83
|
-
const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
84
|
-
// Only affect pixels in shadow range (brightness < 128)
|
|
85
|
-
if (brightness < 128) {
|
|
86
|
-
const factor = 1 + intensity * (1 - brightness / 128) * 2; // More adjustment for darker pixels
|
|
87
|
-
data[i] = Math.min(Math.max(data[i] * factor, 0), 255); // Red
|
|
88
|
-
data[i + 1] = Math.min(Math.max(data[i + 1] * factor, 0), 255); // Green
|
|
89
|
-
data[i + 2] = Math.min(Math.max(data[i + 2] * factor, 0), 255); // Blue
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return imageData;
|
|
93
|
-
},
|
|
94
|
-
white: (intensity) => (imageData) => {
|
|
95
|
-
const data = imageData.data;
|
|
96
|
-
// Intensity should be in -1 to 1 range from the model
|
|
97
|
-
intensity = Math.max(-1, Math.min(1, intensity));
|
|
98
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
99
|
-
const r = data[i];
|
|
100
|
-
const g = data[i + 1];
|
|
101
|
-
const b = data[i + 2];
|
|
102
|
-
const brightness = (r + g + b) / 3;
|
|
103
|
-
if (brightness > 128) {
|
|
104
|
-
const factor = 1 + intensity * ((brightness - 128) / 127); // stronger effect on brighter pixels
|
|
105
|
-
data[i] = Math.min(Math.max(r * factor, 0), 255);
|
|
106
|
-
data[i + 1] = Math.min(Math.max(g * factor, 0), 255);
|
|
107
|
-
data[i + 2] = Math.min(Math.max(b * factor, 0), 255);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return imageData;
|
|
111
|
-
},
|
|
112
|
-
black: (intensity) => (imageData) => {
|
|
113
|
-
const data = imageData.data;
|
|
114
|
-
// Intensity should be in -1 to 1 range from the model
|
|
115
|
-
intensity = Math.max(-1, Math.min(1, intensity));
|
|
116
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
117
|
-
const r = data[i];
|
|
118
|
-
const g = data[i + 1];
|
|
119
|
-
const b = data[i + 2];
|
|
120
|
-
const brightness = (r + g + b) / 3;
|
|
121
|
-
if (brightness < 128) {
|
|
122
|
-
const factor = 1 + intensity * ((128 - brightness) / 128); // stronger effect on darker pixels
|
|
123
|
-
data[i] = Math.min(Math.max(r * factor, 0), 255);
|
|
124
|
-
data[i + 1] = Math.min(Math.max(g * factor, 0), 255);
|
|
125
|
-
data[i + 2] = Math.min(Math.max(b * factor, 0), 255);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return imageData;
|
|
129
|
-
},
|
|
130
|
-
vibrance: (intensity) => (imageData) => {
|
|
131
|
-
const data = imageData.data;
|
|
132
|
-
// Intensity should be in -1 to 1 range from the model
|
|
133
|
-
intensity = Math.max(-1, Math.min(1, intensity));
|
|
134
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
135
|
-
const r = data[i];
|
|
136
|
-
const g = data[i + 1];
|
|
137
|
-
const b = data[i + 2];
|
|
138
|
-
const max = Math.max(r, g, b);
|
|
139
|
-
const avg = (r + g + b) / 3;
|
|
140
|
-
const saturation = max === 0 ? 0 : (max - avg) / max;
|
|
141
|
-
const factor = intensity < 0 ? (1 - saturation) * intensity * 1.5 : intensity * 0.5;
|
|
142
|
-
data[i] = Math.min(Math.max(r - (max - r) * factor, 0), 255);
|
|
143
|
-
data[i + 1] = Math.min(Math.max(g - (max - g) * factor, 0), 255);
|
|
144
|
-
data[i + 2] = Math.min(Math.max(b - (max - b) * factor, 0), 255);
|
|
145
|
-
}
|
|
146
|
-
return imageData;
|
|
147
|
-
},
|
|
148
|
-
saturation: (intensity) => (imageData) => {
|
|
149
|
-
const data = imageData.data;
|
|
150
|
-
// Intensity should be in -1 to 1 range from the model
|
|
151
|
-
intensity = Math.max(-1, Math.min(1, intensity));
|
|
152
|
-
for (let i = 0; i < data.length; i += 4) {
|
|
153
|
-
const r = data[i];
|
|
154
|
-
const g = data[i + 1];
|
|
155
|
-
const b = data[i + 2];
|
|
156
|
-
const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
157
|
-
data[i] = Math.min(Math.max(gray + (r - gray) * (1 + intensity), 0), 255);
|
|
158
|
-
data[i + 1] = Math.min(Math.max(gray + (g - gray) * (1 + intensity), 0), 255);
|
|
159
|
-
data[i + 2] = Math.min(Math.max(gray + (b - gray) * (1 + intensity), 0), 255);
|
|
160
|
-
}
|
|
161
|
-
return imageData;
|
|
162
|
-
},
|
|
163
|
-
};
|
package/lib/ghostscript.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
/**
|
|
5
|
-
* Convert PDF to PDF/X-1a using GhostScript
|
|
6
|
-
* @param inputPath - Path to input PDF
|
|
7
|
-
* @param outputPath - Path to output PDF/X-1a file
|
|
8
|
-
* @param options - Conversion options
|
|
9
|
-
*/
|
|
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
|
|
47
|
-
}
|
|
48
|
-
const gs = spawn('gs', args);
|
|
49
|
-
let stderr = '';
|
|
50
|
-
gs.stderr.on('data', (data) => {
|
|
51
|
-
stderr += data.toString();
|
|
52
|
-
});
|
|
53
|
-
gs.on('close', (code) => {
|
|
54
|
-
// Clean up temp metadata file
|
|
55
|
-
if (options.metadata) {
|
|
56
|
-
const tempPSFile = path.join(path.dirname(outputPath), 'metadata.ps');
|
|
57
|
-
if (fs.existsSync(tempPSFile)) {
|
|
58
|
-
fs.unlinkSync(tempPSFile);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (code === 0) {
|
|
62
|
-
resolve();
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
reject(new Error(`GhostScript failed with code ${code}: ${stderr}`));
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
gs.on('error', (error) => {
|
|
69
|
-
reject(new Error(`Failed to start GhostScript: ${error.message}`));
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Create PostScript metadata for PDF/X-1a
|
|
75
|
-
* @param metadata - Metadata object
|
|
76
|
-
* @returns PostScript code
|
|
77
|
-
*/
|
|
78
|
-
function createMetadataPS(metadata) {
|
|
79
|
-
const { title = 'Cover Creator Export', author = 'KDP Cover Creator', application = 'KDP CoverCreator', producer = 'Polotno PDF Export', } = metadata;
|
|
80
|
-
return `
|
|
81
|
-
%!PS
|
|
82
|
-
% PDF/X-1a Metadata
|
|
83
|
-
[/Title (${title})
|
|
84
|
-
/Author (${author})
|
|
85
|
-
/Creator (${application})
|
|
86
|
-
/Producer (${producer})
|
|
87
|
-
/CreationDate (D:${new Date().toISOString().replace(/[-:]/g, '').slice(0, 14)})
|
|
88
|
-
/ModDate (D:${new Date().toISOString().replace(/[-:]/g, '').slice(0, 14)})
|
|
89
|
-
/DOCINFO pdfmark
|
|
90
|
-
|
|
91
|
-
% Output Intent for PDF/X-1a
|
|
92
|
-
[/OutputIntent
|
|
93
|
-
/GTS_PDFX
|
|
94
|
-
/Info (PDF/X-1a Output Intent)
|
|
95
|
-
/OutputConditionIdentifier (CGATS TR 001)
|
|
96
|
-
/RegistryName (http://www.color.org)
|
|
97
|
-
/PDFX pdfmark
|
|
98
|
-
`;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Validate if PDF is PDF/X-1a compliant
|
|
102
|
-
* @param pdfPath - Path to PDF file
|
|
103
|
-
*/
|
|
104
|
-
export async function validatePDFX1a(pdfPath) {
|
|
105
|
-
return new Promise((resolve, reject) => {
|
|
106
|
-
const args = [
|
|
107
|
-
'-dNOPAUSE',
|
|
108
|
-
'-dBATCH',
|
|
109
|
-
'-sDEVICE=nullpage',
|
|
110
|
-
'-dPDFX=true',
|
|
111
|
-
pdfPath,
|
|
112
|
-
];
|
|
113
|
-
const gs = spawn('gs', args);
|
|
114
|
-
let stderr = '';
|
|
115
|
-
gs.stderr.on('data', (data) => {
|
|
116
|
-
stderr += data.toString();
|
|
117
|
-
});
|
|
118
|
-
gs.on('close', (code) => {
|
|
119
|
-
if (code === 0 &&
|
|
120
|
-
!stderr.includes('Error') &&
|
|
121
|
-
!stderr.includes('Warning')) {
|
|
122
|
-
resolve(true);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
resolve(false);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
gs.on('error', (error) => {
|
|
129
|
-
reject(new Error(`Failed to validate PDF: ${error.message}`));
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
package/lib/group.d.ts
DELETED
package/lib/group.js
DELETED
package/lib/image.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
borderSize?: number;
|
|
16
|
-
borderColor?: string;
|
|
17
|
-
brightnessEnabled: boolean;
|
|
18
|
-
brightness: number;
|
|
19
|
-
grayscaleEnabled: boolean;
|
|
20
|
-
sepiaEnabled: boolean;
|
|
21
|
-
blurEnabled: boolean;
|
|
22
|
-
blurRadius: number;
|
|
23
|
-
filters: Record<string, ShapeFilter>;
|
|
24
|
-
shadowEnabled?: boolean;
|
|
25
|
-
shadowBlur?: number;
|
|
26
|
-
shadowOffsetX?: number;
|
|
27
|
-
shadowOffsetY?: number;
|
|
28
|
-
shadowColor?: string;
|
|
29
|
-
shadowOpacity?: number;
|
|
30
|
-
}
|
|
31
|
-
type ShapeFilter = {
|
|
32
|
-
intensity: number;
|
|
33
|
-
};
|
|
34
|
-
export declare function getProcessedImageKey(element: ImageElement): string;
|
|
35
|
-
export declare function cropImage(src: string, element: ImageElement, cache?: ImageCache | null): Promise<string>;
|
|
36
|
-
export declare function clipImage(src: string, element: ImageElement, cache?: ImageCache | null): Promise<string>;
|
|
37
|
-
export declare function renderImage(doc: PDFKit.PDFDocument, element: ImageElement, cache?: ImageCache | null): Promise<void>;
|
|
38
|
-
export {};
|