@polotno/pdf-export 0.1.37 → 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.
Files changed (52) hide show
  1. package/README.md +61 -8
  2. package/lib/index.d.ts +66 -8
  3. package/lib/index.js +25 -145
  4. package/package.json +17 -18
  5. package/lib/browser-entry.d.ts +0 -7
  6. package/lib/browser-entry.js +0 -11
  7. package/lib/compare-render.d.ts +0 -1
  8. package/lib/compare-render.js +0 -185
  9. package/lib/core/index.d.ts +0 -26
  10. package/lib/core/index.js +0 -87
  11. package/lib/figure.d.ts +0 -10
  12. package/lib/figure.js +0 -54
  13. package/lib/filters.d.ts +0 -2
  14. package/lib/filters.js +0 -163
  15. package/lib/ghostscript.d.ts +0 -21
  16. package/lib/ghostscript.js +0 -132
  17. package/lib/group.d.ts +0 -5
  18. package/lib/group.js +0 -5
  19. package/lib/image.d.ts +0 -38
  20. package/lib/image.js +0 -279
  21. package/lib/line.d.ts +0 -10
  22. package/lib/line.js +0 -66
  23. package/lib/platform/adapter.d.ts +0 -37
  24. package/lib/platform/adapter.js +0 -13
  25. package/lib/platform/browser-polyfill.d.ts +0 -1
  26. package/lib/platform/browser-polyfill.js +0 -5
  27. package/lib/platform/browser.d.ts +0 -7
  28. package/lib/platform/browser.js +0 -145
  29. package/lib/platform/node.d.ts +0 -7
  30. package/lib/platform/node.js +0 -142
  31. package/lib/spot-colors.d.ts +0 -38
  32. package/lib/spot-colors.js +0 -141
  33. package/lib/svg-render.d.ts +0 -9
  34. package/lib/svg-render.js +0 -63
  35. package/lib/svg.d.ts +0 -12
  36. package/lib/svg.js +0 -224
  37. package/lib/text/fonts.d.ts +0 -16
  38. package/lib/text/fonts.js +0 -82
  39. package/lib/text/index.d.ts +0 -8
  40. package/lib/text/index.js +0 -42
  41. package/lib/text/layout.d.ts +0 -22
  42. package/lib/text/layout.js +0 -522
  43. package/lib/text/parser.d.ts +0 -46
  44. package/lib/text/parser.js +0 -415
  45. package/lib/text/render.d.ts +0 -8
  46. package/lib/text/render.js +0 -237
  47. package/lib/text/types.d.ts +0 -91
  48. package/lib/text/types.js +0 -1
  49. package/lib/text.d.ts +0 -49
  50. package/lib/text.js +0 -1277
  51. package/lib/utils.d.ts +0 -16
  52. package/lib/utils.js +0 -124
@@ -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
- });
@@ -1,26 +0,0 @@
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<Blob | Uint8Array | undefined>;
package/lib/core/index.js DELETED
@@ -1,87 +0,0 @@
1
- import { srcToBuffer, parseColor } from '../utils.js';
2
- import { renderImage } from '../image.js';
3
- import { loadFontIfNeeded, renderText } from '../text.js';
4
- import { renderFigure } from '../figure.js';
5
- import { renderGroup } from '../group.js';
6
- import { lineToPDF } from '../line.js';
7
- import { renderSVG } from '../svg-render.js';
8
- import { enableSpotColorSupport } from '../spot-colors.js';
9
- import { getPlatformAdapter } from '../platform/adapter.js';
10
- async function renderElement({ doc, element, fonts, attrs, cache, }) {
11
- if ((element.visible !== undefined && !element.visible) ||
12
- (element.showInExport !== undefined && !element.showInExport)) {
13
- return;
14
- }
15
- doc.save();
16
- if (element.type !== 'group') {
17
- doc.translate(element.x, element.y);
18
- doc.rotate(element.rotation);
19
- }
20
- if (element.opacity !== undefined) {
21
- doc.opacity(element.opacity);
22
- }
23
- if (element.type === 'group') {
24
- await renderGroup(doc, element, renderElement, fonts, attrs, cache);
25
- }
26
- else if (element.type === 'text') {
27
- await loadFontIfNeeded(doc, element, fonts);
28
- await renderText(doc, element, fonts, attrs);
29
- }
30
- else if (element.type === 'line') {
31
- lineToPDF(doc, element);
32
- }
33
- else if (element.type === 'image') {
34
- await renderImage(doc, element, cache);
35
- }
36
- else if (element.type === 'svg') {
37
- await renderSVG(doc, element, cache);
38
- }
39
- else if (element.type === 'figure') {
40
- renderFigure(doc, element);
41
- }
42
- doc.restore();
43
- }
44
- export async function jsonToPDF(json, pdfFileName, attrs = {}) {
45
- const fonts = {};
46
- const cache = {
47
- images: new Map(),
48
- buffers: new Map(),
49
- processedImages: new Map(),
50
- };
51
- const adapter = getPlatformAdapter();
52
- const { doc, finalize } = adapter.createPDFContext({
53
- size: [json.width, json.height],
54
- autoFirstPage: false,
55
- });
56
- if (attrs.spotColors) {
57
- enableSpotColorSupport(doc, attrs.spotColors);
58
- }
59
- for (const font of json.fonts) {
60
- await adapter.registerFont(doc, font.fontFamily, await srcToBuffer(font.url, cache));
61
- fonts[font.fontFamily] = true;
62
- }
63
- for (const page of json.pages) {
64
- doc.addPage();
65
- if (page.background) {
66
- const isURL = page.background.indexOf('http') >= 0 ||
67
- page.background.indexOf('.png') >= 0 ||
68
- page.background.indexOf('.jpg') >= 0;
69
- if (isURL) {
70
- const mime = cache.buffers.get(`mime:${page.background}`);
71
- await adapter.embedImage(doc, await srcToBuffer(page.background, cache), 0, 0, {
72
- cover: [json.width, json.height],
73
- align: 'center',
74
- }, mime);
75
- }
76
- else {
77
- doc.rect(0, 0, json.width, json.height);
78
- doc.fill(parseColor(page.background).hex);
79
- }
80
- }
81
- for (const element of page.children) {
82
- await renderElement({ doc, element, fonts, attrs, cache });
83
- }
84
- }
85
- doc.end();
86
- return finalize({ fileName: pdfFileName, attrs });
87
- }
package/lib/figure.d.ts DELETED
@@ -1,10 +0,0 @@
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 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
@@ -1,2 +0,0 @@
1
- import { Filter } from "konva/lib/Node";
2
- export declare const elementFilterToKonva: Record<string, (intensity: number) => Filter>;
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
- };
@@ -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>;
@@ -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
- }