@segnavia/render 0.5.0 → 0.7.0
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/dist/book-pdf.js +15 -8
- package/dist/book-render.js +3 -2
- package/dist/browser-jpeg.js +42 -0
- package/dist/color/black-point.js +11 -8
- package/dist/color/engine.js +9 -2
- package/dist/color/interpolation.js +11 -1
- package/dist/color/profile.js +17 -7
- package/dist/color/tables.js +47 -13
- package/dist/color/types.js +4 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +42 -14
- package/dist/options.d.ts +34 -0
- package/dist/overflow.js +123 -0
- package/dist/page-paper.js +12 -7
- package/dist/pages-pdf.js +7 -4
- package/dist/press/adapter.js +97 -42
- package/dist/press/color-policy.js +12 -3
- package/dist/press/content.js +28 -20
- package/dist/press/image.js +327 -0
- package/dist/press/policy.js +27 -10
- package/dist/press/report.js +24 -13
- package/dist/press/sample-shading.js +9 -19
- package/dist/press/types.d.ts +7 -2
- package/dist/press/visitor.js +32 -30
- package/package.json +4 -4
package/dist/overflow.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/overflow.ts
|
|
2
|
+
/**
|
|
3
|
+
* Chromium's measured maximum print shrink; the 3x overflow demo check fails
|
|
4
|
+
* if it changes. Chromium lays a scaled print out at the narrower width P / s,
|
|
5
|
+
* then shrinks it by at most this factor. The solved scale searches layout
|
|
6
|
+
* widths for the point where rightward content reaches P, and uses this exact
|
|
7
|
+
* cap when it is already reached at the narrowest layout. Rightward generated
|
|
8
|
+
* content in an RTL root is a known limit: neither scroll extent nor element
|
|
9
|
+
* boxes expose it.
|
|
10
|
+
*/
|
|
11
|
+
const CHROMIUM_MAX_SHRINK = 1.5;
|
|
12
|
+
/** Decides the next layout to measure, or the print scale, from measured widths. */
|
|
13
|
+
function solveScale(pageWidth, measurements) {
|
|
14
|
+
const measured = new Map(measurements.map((measurement) => [measurement.layoutWidth, measurement.contentWidth]));
|
|
15
|
+
const atPage = measured.get(pageWidth);
|
|
16
|
+
if (atPage === void 0) return { measure: pageWidth };
|
|
17
|
+
if (atPage <= pageWidth) return { scale: 1 };
|
|
18
|
+
const narrowest = Math.ceil(pageWidth / CHROMIUM_MAX_SHRINK);
|
|
19
|
+
const atNarrowest = measured.get(narrowest);
|
|
20
|
+
if (atNarrowest === void 0) return { measure: narrowest };
|
|
21
|
+
if (atNarrowest >= pageWidth) return { scale: CHROMIUM_MAX_SHRINK };
|
|
22
|
+
let low = narrowest;
|
|
23
|
+
let high = pageWidth;
|
|
24
|
+
let lastSatisfied = pageWidth;
|
|
25
|
+
while (high - low > 1) {
|
|
26
|
+
const middle = Math.floor((low + high) / 2);
|
|
27
|
+
const width = measured.get(middle);
|
|
28
|
+
if (width === void 0) return { measure: middle };
|
|
29
|
+
if (width >= pageWidth) {
|
|
30
|
+
high = middle;
|
|
31
|
+
lastSatisfied = middle;
|
|
32
|
+
} else low = middle;
|
|
33
|
+
}
|
|
34
|
+
return { scale: pageWidth / (measured.get(high) !== void 0 && measured.get(high) >= pageWidth ? high : lastSatisfied) };
|
|
35
|
+
}
|
|
36
|
+
async function measuredOverflow(tab, pageWidth) {
|
|
37
|
+
return tab.evaluate((width) => {
|
|
38
|
+
const root = document.documentElement;
|
|
39
|
+
const extent = Math.max(root.scrollWidth, document.body?.scrollWidth ?? 0);
|
|
40
|
+
const endOverflow = Math.max(0, extent - root.clientWidth);
|
|
41
|
+
const rtl = getComputedStyle(root).direction === "rtl";
|
|
42
|
+
const contributions = [];
|
|
43
|
+
let clippedStart = 0;
|
|
44
|
+
let rightOverflow = rtl ? 0 : endOverflow;
|
|
45
|
+
const locator = (element) => {
|
|
46
|
+
if (element.id) return `#${CSS.escape(element.id)}`;
|
|
47
|
+
const parts = [];
|
|
48
|
+
let current = element;
|
|
49
|
+
while (current && current !== root && parts.length < 3) {
|
|
50
|
+
const classes = [...current.classList].slice(0, 2).map((name) => `.${CSS.escape(name)}`).join("");
|
|
51
|
+
parts.unshift(`${current.localName}${classes}`);
|
|
52
|
+
current = current.parentElement;
|
|
53
|
+
}
|
|
54
|
+
return parts.join(" > ");
|
|
55
|
+
};
|
|
56
|
+
for (const element of document.querySelectorAll("*")) {
|
|
57
|
+
const box = element.getBoundingClientRect();
|
|
58
|
+
const elementStart = Math.max(0, rtl ? box.right - width : -box.left);
|
|
59
|
+
const elementEnd = Math.max(0, rtl ? -box.left : box.right - width);
|
|
60
|
+
clippedStart = Math.max(clippedStart, elementStart);
|
|
61
|
+
rightOverflow = Math.max(rightOverflow, box.right - width);
|
|
62
|
+
if ((elementStart > 0 || elementEnd > 0) && contributions.length < 20) contributions.push({
|
|
63
|
+
locator: locator(element),
|
|
64
|
+
clippedStart: elementStart,
|
|
65
|
+
clippedEnd: elementEnd
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
overflow: {
|
|
70
|
+
pageWidth: width,
|
|
71
|
+
extent,
|
|
72
|
+
clippedStart,
|
|
73
|
+
clippedEnd: endOverflow,
|
|
74
|
+
elements: contributions
|
|
75
|
+
},
|
|
76
|
+
rightOverflow
|
|
77
|
+
};
|
|
78
|
+
}, pageWidth);
|
|
79
|
+
}
|
|
80
|
+
function overflowEntry(measured, scale, part, inputIndex) {
|
|
81
|
+
return {
|
|
82
|
+
part,
|
|
83
|
+
...inputIndex === void 0 ? {} : { inputIndex },
|
|
84
|
+
...measured.overflow,
|
|
85
|
+
scale
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function measureOverflow(tab, pageWidth, part, inputIndex) {
|
|
89
|
+
await tab.emulateMedia({ media: "print" });
|
|
90
|
+
return overflowEntry(await measuredOverflow(tab, pageWidth), 1, part, inputIndex);
|
|
91
|
+
}
|
|
92
|
+
async function printWithOverflow(tab, pageWidth, part, pdf, inputIndex) {
|
|
93
|
+
await tab.emulateMedia({ media: "print" });
|
|
94
|
+
const viewport = tab.viewportSize();
|
|
95
|
+
if (!viewport) throw new Error("The print tab has no viewport.");
|
|
96
|
+
const measurements = [];
|
|
97
|
+
let original;
|
|
98
|
+
let decision = solveScale(pageWidth, measurements);
|
|
99
|
+
while ("measure" in decision) {
|
|
100
|
+
await tab.setViewportSize({
|
|
101
|
+
width: decision.measure,
|
|
102
|
+
height: viewport.height
|
|
103
|
+
});
|
|
104
|
+
const measured = await measuredOverflow(tab, decision.measure);
|
|
105
|
+
if (decision.measure === pageWidth) original = measured;
|
|
106
|
+
measurements.push({
|
|
107
|
+
layoutWidth: decision.measure,
|
|
108
|
+
contentWidth: decision.measure + measured.rightOverflow
|
|
109
|
+
});
|
|
110
|
+
decision = solveScale(pageWidth, measurements);
|
|
111
|
+
}
|
|
112
|
+
await tab.setViewportSize(viewport);
|
|
113
|
+
if (!original) throw new Error("The page-width overflow measurement was not made.");
|
|
114
|
+
return {
|
|
115
|
+
bytes: await tab.pdf({
|
|
116
|
+
...pdf,
|
|
117
|
+
scale: decision.scale
|
|
118
|
+
}),
|
|
119
|
+
overflow: overflowEntry(original, decision.scale, part, inputIndex)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
export { CHROMIUM_MAX_SHRINK, measureOverflow, printWithOverflow, solveScale };
|
package/dist/page-paper.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RenderRefusedError } from "./errors.js";
|
|
2
2
|
import { pdfBox, pdfPoint, pt } from "./pdf-geometry.js";
|
|
3
|
+
import { printWithOverflow } from "./overflow.js";
|
|
3
4
|
import { PX_PER_MM, printBoxes, printPadding, printProperties, sheetMm } from "@segnavia/format";
|
|
4
5
|
import { PDFDocument, PDFName, rgb } from "pdf-lib";
|
|
5
6
|
//#region src/page-paper.ts
|
|
@@ -56,11 +57,12 @@ async function addPressBoxes(pdf, print) {
|
|
|
56
57
|
});
|
|
57
58
|
return document.save();
|
|
58
59
|
}
|
|
59
|
-
async function printPage(tab, print, preferCssPageSize) {
|
|
60
|
+
async function printPage(tab, print, preferCssPageSize, inputIndex) {
|
|
60
61
|
if (!usesPressLayout(print)) {
|
|
61
62
|
const mm = sheetMm(print);
|
|
62
63
|
const margin = print.margin;
|
|
63
|
-
|
|
64
|
+
const pageWidth = Math.round(mm.width * PX_PER_MM);
|
|
65
|
+
return printWithOverflow(tab, pageWidth, "page", {
|
|
64
66
|
width: `${mm.width}mm`,
|
|
65
67
|
height: `${mm.height}mm`,
|
|
66
68
|
printBackground: true,
|
|
@@ -71,10 +73,10 @@ async function printPage(tab, print, preferCssPageSize) {
|
|
|
71
73
|
bottom: `${margin.bottom}mm`,
|
|
72
74
|
left: `${margin.left}mm`
|
|
73
75
|
} : void 0
|
|
74
|
-
});
|
|
76
|
+
}, inputIndex);
|
|
75
77
|
}
|
|
76
78
|
const boxes = printBoxes(print);
|
|
77
|
-
const
|
|
79
|
+
const printed = await printWithOverflow(tab, Math.round(boxes.bleedBox.width * PX_PER_MM), "page", {
|
|
78
80
|
width: `${boxes.bleedBox.width}mm`,
|
|
79
81
|
height: `${boxes.bleedBox.height}mm`,
|
|
80
82
|
printBackground: true,
|
|
@@ -85,10 +87,13 @@ async function printPage(tab, print, preferCssPageSize) {
|
|
|
85
87
|
bottom: "0mm",
|
|
86
88
|
left: "0mm"
|
|
87
89
|
}
|
|
88
|
-
});
|
|
89
|
-
const papers = (await PDFDocument.load(
|
|
90
|
+
}, inputIndex);
|
|
91
|
+
const papers = (await PDFDocument.load(printed.bytes)).getPageCount();
|
|
90
92
|
if (papers > 1) throw new RenderRefusedError(`A page with bleed or marks is one printed piece, but the content needs ${papers} papers. Shorten the content, or remove the bleed and marks.`);
|
|
91
|
-
return
|
|
93
|
+
return {
|
|
94
|
+
bytes: await addPressBoxes(printed.bytes, print),
|
|
95
|
+
overflow: printed.overflow
|
|
96
|
+
};
|
|
92
97
|
}
|
|
93
98
|
//#endregion
|
|
94
99
|
export { assertPrintOptions, printCss, printPage, printViewport };
|
package/dist/pages-pdf.js
CHANGED
|
@@ -51,13 +51,16 @@ async function renderPagesPdf(inputs, opts, services) {
|
|
|
51
51
|
if (inputs.length === 0) throw new RenderRefusedError("There is nothing to render. Pass at least one page to clear this refusal.");
|
|
52
52
|
const pages = inputs.map((input) => Page.parse(input));
|
|
53
53
|
if (!opts.imposition) {
|
|
54
|
-
const rendered = await Promise.all(pages.map((page) => services.pagePdf(page, {
|
|
54
|
+
const rendered = await Promise.all(pages.map((page, inputIndex) => services.pagePdf(page, {
|
|
55
55
|
...opts,
|
|
56
56
|
effects: false,
|
|
57
57
|
limits: void 0,
|
|
58
|
-
press: void 0
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
press: void 0,
|
|
59
|
+
overflow: void 0
|
|
60
|
+
}, inputIndex)));
|
|
61
|
+
const output = await services.finish(await joinPdfs(rendered.map((part) => part.bytes)), opts);
|
|
62
|
+
opts.overflow?.report?.({ entries: rendered.flatMap((part) => part.overflow ? [part.overflow] : []) });
|
|
63
|
+
return output;
|
|
61
64
|
}
|
|
62
65
|
const imposition = Imposition.parse(opts.imposition);
|
|
63
66
|
if (opts.preferCssPageSize) throw new RenderRefusedError("Pages in an imposition take their size from its paper. Remove preferCssPageSize, or remove the imposition, to clear this refusal.");
|
package/dist/press/adapter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RenderRefusedError } from "../errors.js";
|
|
2
|
-
import { cachedPressColor, newlySnapsToNeutral,
|
|
2
|
+
import { cachedPressColor, destinationColor, newlySnapsToNeutral, pdfMaskLuminosity, pressDestination } from "./color-policy.js";
|
|
3
3
|
import { groupAction, shadingAction } from "./policy.js";
|
|
4
4
|
import { rewriteContent } from "./content.js";
|
|
5
5
|
import { arrayNumbers, decodeContent, dictionaryOf, nameOf, numberOf, replaceArray, resolvedColorSpace } from "./pdf-objects.js";
|
|
@@ -7,6 +7,7 @@ import { walkPressGraph } from "./visitor.js";
|
|
|
7
7
|
import "../effects/types.js";
|
|
8
8
|
import { replaceType1Function, sampleDomainForUses, shadingDeviceMatrix, shadingDomainOf, shadingSampleGridForUses } from "./sample-shading.js";
|
|
9
9
|
import { readProfile } from "../color/profile.js";
|
|
10
|
+
import { convertSuppliedImage, convertedImageCount, createImageState } from "./image.js";
|
|
10
11
|
import { createPaintedBoundsTracker, recordPatternPaintedBounds } from "./pattern-bounds.js";
|
|
11
12
|
import { inspectPressPdf } from "./report.js";
|
|
12
13
|
import { PDFDict, PDFDocument, PDFHexString, PDFName, PDFRef, PDFString } from "pdf-lib";
|
|
@@ -17,12 +18,14 @@ const refuse = (found, supported) => {
|
|
|
17
18
|
throw new RenderRefusedError(`The PDF has unsupported ${found}; supported: ${supported}.`);
|
|
18
19
|
};
|
|
19
20
|
const resolved = (state, object) => object instanceof PDFRef ? state.document.context.lookup(object) : object;
|
|
20
|
-
|
|
21
|
+
const isGrayMask = (state, mask) => state.destination === "gray" && mask !== "none";
|
|
22
|
+
const converterFor = (state, mask) => isGrayMask(state, mask) ? pdfMaskLuminosity : state.convert;
|
|
23
|
+
function convertFunction(state, fn, policy, mask) {
|
|
21
24
|
if (policy.action === "refuse") refuse(policy.found, policy.supported);
|
|
22
25
|
if (!fn) return;
|
|
23
26
|
if (numberOf(fn, "FunctionType") === 2) {
|
|
24
|
-
for (const name of ["C0", "C1"]) replaceArray(fn, name, state
|
|
25
|
-
if (fn.has(key("Range"))) replaceArray(fn, "Range", [
|
|
27
|
+
for (const name of ["C0", "C1"]) replaceArray(fn, name, converterFor(state, mask)(arrayNumbers(fn, name, 3)));
|
|
28
|
+
if (fn.has(key("Range"))) replaceArray(fn, "Range", state.destination === "gray" ? [0, 1] : [
|
|
26
29
|
0,
|
|
27
30
|
1,
|
|
28
31
|
0,
|
|
@@ -35,7 +38,7 @@ function convertFunction(state, fn, policy) {
|
|
|
35
38
|
state.totals.gradientStops += 2;
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
|
-
if (fn.has(key("Range"))) replaceArray(fn, "Range", [
|
|
41
|
+
if (fn.has(key("Range"))) replaceArray(fn, "Range", state.destination === "gray" ? [0, 1] : [
|
|
39
42
|
0,
|
|
40
43
|
1,
|
|
41
44
|
0,
|
|
@@ -72,78 +75,81 @@ function shadingUses(state, shading, pattern) {
|
|
|
72
75
|
});
|
|
73
76
|
return uses.filter((use) => use.painted !== void 0);
|
|
74
77
|
}
|
|
75
|
-
function convertType1Shading(state, shading, pattern) {
|
|
78
|
+
function convertType1Shading(state, shading, pattern, mask) {
|
|
76
79
|
const uses = shadingUses(state, shading, pattern);
|
|
77
80
|
const domain = sampleDomainForUses(shadingDomainOf(shading), uses);
|
|
78
81
|
const matrices = uses.length > 0 ? uses.map((use) => use.matrix) : [shadingDeviceMatrix(shading, pattern)];
|
|
79
82
|
const size = shadingSampleGridForUses(domain, matrices);
|
|
80
|
-
replaceType1Function(state.document, shading, pattern, state.sampleConvert, state.budget, void 0, domain, size);
|
|
83
|
+
replaceType1Function(state.document, shading, pattern, isGrayMask(state, mask) ? pdfMaskLuminosity : state.sampleConvert, state.budget, void 0, domain, size, state.destination === "gray" ? 1 : 4);
|
|
81
84
|
}
|
|
82
|
-
function convertShading(state, shading, pattern) {
|
|
85
|
+
function convertShading(state, shading, pattern, mask) {
|
|
83
86
|
if (state.seenShadings.has(shading)) return false;
|
|
84
87
|
state.seenShadings.add(shading);
|
|
85
88
|
const type = numberOf(shading, "ShadingType");
|
|
86
89
|
const functionType = functionTypeOf(state, shading);
|
|
87
90
|
const space = resolvedColorSpace(state.document, resolved(state, shading.get(key("ColorSpace"))));
|
|
88
|
-
const
|
|
91
|
+
const destination = state.destination;
|
|
92
|
+
const policy = shadingAction(space.kind, type, functionType, destination);
|
|
89
93
|
if (policy.action === "refuse") {
|
|
90
94
|
if (type === 1) refuse(`ShadingType 1 FunctionType ${functionType ?? "missing"}`, policy.supported);
|
|
91
95
|
refuse(`ShadingType ${type ?? "missing"} color space ${space.label}`, policy.supported);
|
|
92
96
|
}
|
|
93
97
|
if (policy.action === "keep") return false;
|
|
94
|
-
if (shading.has(key("Background"))) replaceArray(shading, "Background", state
|
|
95
|
-
shading.set(key("ColorSpace"), key("DeviceCMYK"));
|
|
98
|
+
if (shading.has(key("Background"))) replaceArray(shading, "Background", converterFor(state, mask)(arrayNumbers(shading, "Background", 3)));
|
|
99
|
+
shading.set(key("ColorSpace"), key(destination === "gray" ? "DeviceGray" : "DeviceCMYK"));
|
|
96
100
|
if (type === 1) {
|
|
97
|
-
convertType1Shading(state, shading, pattern);
|
|
101
|
+
convertType1Shading(state, shading, pattern, mask);
|
|
98
102
|
return false;
|
|
99
103
|
}
|
|
100
104
|
return true;
|
|
101
105
|
}
|
|
102
|
-
function convertGroup(state, owner,
|
|
106
|
+
function convertGroup(state, owner, mask) {
|
|
103
107
|
const group = owner.lookupMaybe(key("Group"), PDFDict);
|
|
104
108
|
if (!group || nameOf(group.get(key("S"))) !== "Transparency" || state.seenGroups.has(group)) return;
|
|
105
109
|
state.seenGroups.add(group);
|
|
106
110
|
const space = resolvedColorSpace(state.document, resolved(state, group.get(key("CS"))));
|
|
107
|
-
const
|
|
111
|
+
const destination = state.destination;
|
|
112
|
+
const policy = groupAction(space.kind, space.label, mask !== "none", destination);
|
|
108
113
|
if (policy.action === "refuse") refuse(`transparency group color space ${space.label}`, policy.supported);
|
|
109
114
|
if (policy.action === "convert") {
|
|
110
|
-
group.set(key("CS"), key("DeviceCMYK"));
|
|
115
|
+
group.set(key("CS"), key(destination === "gray" ? "DeviceGray" : "DeviceCMYK"));
|
|
111
116
|
state.totals.groups++;
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
|
-
function convertRuleCImage(state, image, width, height, dictionary) {
|
|
119
|
+
function convertRuleCImage(state, image, width, height, dictionary, convert) {
|
|
115
120
|
const source = decodeContent(image, state.budget);
|
|
116
121
|
if (source.length !== width * height * 3) refuse("effect image sample length", "three 8-bit RGB samples per pixel");
|
|
117
|
-
const
|
|
122
|
+
const channels = state.destination === "gray" ? 1 : 4;
|
|
123
|
+
const converted = new Uint8Array(width * height * channels);
|
|
118
124
|
for (let pixel = 0; pixel < width * height; pixel++) {
|
|
119
|
-
const color =
|
|
125
|
+
const color = convert([
|
|
120
126
|
(source[pixel * 3] ?? 0) / 255,
|
|
121
127
|
(source[pixel * 3 + 1] ?? 0) / 255,
|
|
122
128
|
(source[pixel * 3 + 2] ?? 0) / 255
|
|
123
129
|
]);
|
|
124
|
-
for (let channel = 0; channel <
|
|
130
|
+
for (let channel = 0; channel < channels; channel++) converted[pixel * channels + channel] = Math.round(Math.max(0, Math.min(1, color[channel] ?? 0)) * 255);
|
|
125
131
|
}
|
|
126
|
-
dictionary.ColorSpace = key("DeviceCMYK");
|
|
132
|
+
dictionary.ColorSpace = key(state.destination === "gray" ? "DeviceGray" : "DeviceCMYK");
|
|
127
133
|
const replacement = state.document.context.stream(deflateSync(converted), dictionary);
|
|
128
134
|
const reference = state.document.context.getObjectRef(image);
|
|
129
135
|
if (reference) state.document.context.assign(reference, replacement);
|
|
130
136
|
}
|
|
131
|
-
function convertEffectImage(state, image) {
|
|
137
|
+
function convertEffectImage(state, image, mask) {
|
|
132
138
|
if (!image.dict.has(key("SegnaviaGeneratedEffect")) || resolvedColorSpace(state.document, resolved(state, image.dict.get(key("ColorSpace")))).kind !== "rgb") return;
|
|
133
139
|
const width = numberOf(image.dict, "Width") ?? refuse("effect image Width missing", "a positive Width number");
|
|
134
140
|
const height = numberOf(image.dict, "Height") ?? refuse("effect image Height missing", "a positive Height number");
|
|
135
141
|
if (width <= 0 || height <= 0) refuse("effect image dimensions", "positive Width and Height numbers");
|
|
136
142
|
const dictionary = streamDictionary(image);
|
|
143
|
+
const convert = converterFor(state, mask);
|
|
137
144
|
if (!image.dict.has(key("SegnaviaEffectColor"))) {
|
|
138
|
-
convertRuleCImage(state, image, width, height, dictionary);
|
|
145
|
+
convertRuleCImage(state, image, width, height, dictionary, convert);
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
141
|
-
const
|
|
142
|
-
const color = state.convert(source);
|
|
148
|
+
const color = convert(arrayNumbers(image.dict, "SegnaviaEffectColor", 3));
|
|
143
149
|
const palette = color.map((value) => Math.round(Math.max(0, Math.min(1, value)) * 255));
|
|
144
150
|
dictionary.ColorSpace = state.document.context.obj([
|
|
145
151
|
"Indexed",
|
|
146
|
-
"DeviceCMYK",
|
|
152
|
+
state.destination === "gray" ? "DeviceGray" : "DeviceCMYK",
|
|
147
153
|
0,
|
|
148
154
|
PDFHexString.of(Buffer.from(palette).toString("hex"))
|
|
149
155
|
]);
|
|
@@ -185,9 +191,10 @@ function recordContentBounds(state, stream, resources, budget) {
|
|
|
185
191
|
if (state.sequence) state.sequence.feed(bytes, resolve, resolveGs, resolveShading);
|
|
186
192
|
else recordPatternPaintedBounds(bytes, resolve, state.painted, resolveGs, resolveShading, state.shadingPainted);
|
|
187
193
|
}
|
|
188
|
-
function convertContent(state, object, stream, setDirect) {
|
|
194
|
+
function convertContent(state, object, stream, setDirect, mask) {
|
|
189
195
|
const bytes = decodeContent(stream, state.budget);
|
|
190
|
-
const
|
|
196
|
+
const destination = state.destination;
|
|
197
|
+
const rewritten = rewriteContent(bytes, converterFor(state, mask), destination);
|
|
191
198
|
state.totals.fills += rewritten.counts.fills;
|
|
192
199
|
state.totals.strokes += rewritten.counts.strokes;
|
|
193
200
|
const replacement = state.document.context.stream(deflateSync(rewritten.bytes), streamDictionary(stream));
|
|
@@ -211,15 +218,16 @@ function newState(document, profile, tolerance) {
|
|
|
211
218
|
shadingSamples: 0
|
|
212
219
|
},
|
|
213
220
|
convert: (rgb) => {
|
|
214
|
-
if (newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
|
|
221
|
+
if (profile.colorSpace === "CMYK" && newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
|
|
215
222
|
return cached(rgb);
|
|
216
223
|
},
|
|
224
|
+
destination: pressDestination(profile),
|
|
217
225
|
document,
|
|
218
226
|
painted: /* @__PURE__ */ new WeakMap(),
|
|
219
227
|
profile,
|
|
220
228
|
sampleConvert: (rgb) => {
|
|
221
|
-
if (newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
|
|
222
|
-
return
|
|
229
|
+
if (profile.colorSpace === "CMYK" && newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
|
|
230
|
+
return destinationColor(profile, rgb, tolerance);
|
|
223
231
|
},
|
|
224
232
|
seenGroups: /* @__PURE__ */ new WeakSet(),
|
|
225
233
|
seenShadings: /* @__PURE__ */ new WeakSet(),
|
|
@@ -235,7 +243,9 @@ function rewritePressDocument(document, profile, tolerance = 3) {
|
|
|
235
243
|
beginContentsSequence: () => {
|
|
236
244
|
state.sequence = createPaintedBoundsTracker(state.painted, state.shadingPainted);
|
|
237
245
|
},
|
|
238
|
-
content: (_object, stream, _replace, resources) =>
|
|
246
|
+
content: (_object, stream, _replace, resources) => {
|
|
247
|
+
recordContentBounds(state, stream, resources, boundsBudget);
|
|
248
|
+
},
|
|
239
249
|
endContentsSequence: () => {
|
|
240
250
|
state.sequence = void 0;
|
|
241
251
|
},
|
|
@@ -246,16 +256,16 @@ function rewritePressDocument(document, profile, tolerance = 3) {
|
|
|
246
256
|
shading: (shading, pattern) => indexShadingPattern(state, shading, pattern)
|
|
247
257
|
});
|
|
248
258
|
walkPressGraph(document, {
|
|
249
|
-
content: (object, stream, replace) => convertContent(state, object, stream, replace),
|
|
250
|
-
function: (fn, decision) => convertFunction(state, fn, decision),
|
|
251
|
-
group: (owner, _stream,
|
|
252
|
-
image: (image) => convertEffectImage(state, image),
|
|
253
|
-
shading: (shading, pattern) => convertShading(state, shading, pattern)
|
|
259
|
+
content: (object, stream, replace, _resources, mask) => convertContent(state, object, stream, replace, mask),
|
|
260
|
+
function: (fn, decision, mask) => convertFunction(state, fn, decision, mask),
|
|
261
|
+
group: (owner, _stream, mask) => convertGroup(state, owner, mask),
|
|
262
|
+
image: (image, mask) => convertEffectImage(state, image, mask),
|
|
263
|
+
shading: (shading, pattern, mask) => convertShading(state, shading, pattern, mask)
|
|
254
264
|
});
|
|
255
265
|
return state.totals;
|
|
256
266
|
}
|
|
257
|
-
function addOutputIntent(document, profile) {
|
|
258
|
-
const profileRef = document.context.register(document.context.stream(profile, { N:
|
|
267
|
+
function addOutputIntent(document, profile, channels) {
|
|
268
|
+
const profileRef = document.context.register(document.context.stream(profile, { N: channels }));
|
|
259
269
|
const intentRef = document.context.register(document.context.obj({
|
|
260
270
|
Type: "OutputIntent",
|
|
261
271
|
S: "GTS_PDFX",
|
|
@@ -264,16 +274,61 @@ function addOutputIntent(document, profile) {
|
|
|
264
274
|
}));
|
|
265
275
|
document.catalog.set(key("OutputIntents"), document.context.obj([intentRef]));
|
|
266
276
|
}
|
|
267
|
-
|
|
277
|
+
function replaceSourceImage(document, image, uses, replacement) {
|
|
278
|
+
const reference = document.context.getObjectRef(image);
|
|
279
|
+
if (reference) document.context.assign(reference, replacement);
|
|
280
|
+
else for (const use of uses) use.replace(replacement);
|
|
281
|
+
}
|
|
282
|
+
async function convertImageUses(state, image, uses) {
|
|
283
|
+
const normal = uses.filter((use) => use.mask === "none");
|
|
284
|
+
const masks = uses.filter((use) => use.mask !== "none");
|
|
285
|
+
if (normal.length > 0) {
|
|
286
|
+
const replacement = await convertSuppliedImage(state, image, "none");
|
|
287
|
+
if (!replacement) return;
|
|
288
|
+
replaceSourceImage(state.document, image, normal, replacement);
|
|
289
|
+
}
|
|
290
|
+
if (masks.length === 0) return;
|
|
291
|
+
const replacement = await convertSuppliedImage(state, image, masks[0]?.mask ?? "luminosity");
|
|
292
|
+
if (!replacement) return;
|
|
293
|
+
if (normal.length === 0) replaceSourceImage(state.document, image, masks, replacement);
|
|
294
|
+
else {
|
|
295
|
+
const reference = state.document.context.getObjectRef(replacement) ?? state.document.context.register(replacement);
|
|
296
|
+
for (const use of masks) use.replace(reference);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function convertGrayImages(document, profile, decodeJpeg) {
|
|
300
|
+
const state = createImageState(document, profile, decodeJpeg);
|
|
301
|
+
const images = [];
|
|
302
|
+
walkPressGraph(document, {
|
|
303
|
+
content: () => void 0,
|
|
304
|
+
function: () => void 0,
|
|
305
|
+
group: () => void 0,
|
|
306
|
+
image: (image, mask, replace) => images.push({
|
|
307
|
+
image,
|
|
308
|
+
mask,
|
|
309
|
+
replace
|
|
310
|
+
}),
|
|
311
|
+
shading: () => false
|
|
312
|
+
});
|
|
313
|
+
const uses = /* @__PURE__ */ new Map();
|
|
314
|
+
for (const found of images) uses.set(found.image, [...uses.get(found.image) ?? [], found]);
|
|
315
|
+
for (const [image, found] of uses) await convertImageUses(state, image, found);
|
|
316
|
+
return convertedImageCount(state);
|
|
317
|
+
}
|
|
318
|
+
async function rewritePressPdf(bytes, profileBytes, tolerance = 3, decodeJpeg) {
|
|
268
319
|
try {
|
|
269
320
|
const profile = readProfile(profileBytes);
|
|
270
|
-
if (profile.colorSpace !== "CMYK") refuse(`press profile color space ${profile.colorSpace}`, "CMYK");
|
|
321
|
+
if (profile.colorSpace !== "CMYK" && profile.colorSpace !== "GRAY") refuse(`press profile color space ${profile.colorSpace}`, "CMYK or GRAY");
|
|
271
322
|
const document = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
272
323
|
const counts = rewritePressDocument(document, profile, tolerance);
|
|
273
|
-
|
|
324
|
+
let convertedImages = 0;
|
|
325
|
+
const destination = pressDestination(profile);
|
|
326
|
+
if (destination === "gray") convertedImages = await convertGrayImages(document, profile, decodeJpeg);
|
|
327
|
+
addOutputIntent(document, profileBytes, destination === "gray" ? 1 : 4);
|
|
274
328
|
const output = await document.save({ useObjectStreams: false });
|
|
275
|
-
const report = await inspectPressPdf(output);
|
|
329
|
+
const report = await inspectPressPdf(output, destination);
|
|
276
330
|
report.neutralColors = counts.neutralColors;
|
|
331
|
+
report.convertedImages = convertedImages;
|
|
277
332
|
return {
|
|
278
333
|
bytes: output,
|
|
279
334
|
report
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { RenderRefusedError } from "../errors.js";
|
|
2
|
-
import { toPress } from "../color/engine.js";
|
|
2
|
+
import { toGray, toPress } from "../color/engine.js";
|
|
3
3
|
//#region src/press/color-policy.ts
|
|
4
4
|
const NEUTRAL_TOLERANCE = .001;
|
|
5
|
+
function pressDestination(profile) {
|
|
6
|
+
return profile.colorSpace === "GRAY" ? "gray" : "cmyk";
|
|
7
|
+
}
|
|
8
|
+
function pdfMaskLuminosity([red, green, blue]) {
|
|
9
|
+
return [.3 * red + .59 * green + .11 * blue];
|
|
10
|
+
}
|
|
5
11
|
function assertNeutralTolerance(value) {
|
|
6
12
|
if (!Number.isFinite(value) || value < 0 || value > 255) throw new RenderRefusedError(`The press neutral tolerance is ${String(value)}; supported: a finite number from 0 to 255.`);
|
|
7
13
|
}
|
|
@@ -21,6 +27,9 @@ function pressColor(profile, rgb, tolerance = 3) {
|
|
|
21
27
|
];
|
|
22
28
|
return toPress(profile, rgb);
|
|
23
29
|
}
|
|
30
|
+
function destinationColor(profile, rgb, tolerance = 3) {
|
|
31
|
+
return profile.colorSpace === "GRAY" ? toGray(profile, rgb) : pressColor(profile, rgb, tolerance);
|
|
32
|
+
}
|
|
24
33
|
function formatPdfNumber(value) {
|
|
25
34
|
const rounded = Math.round(value * 1e6) / 1e6;
|
|
26
35
|
if (Object.is(rounded, -0)) return "0";
|
|
@@ -32,10 +41,10 @@ function cachedPressColor(profile, tolerance = 3) {
|
|
|
32
41
|
const key = `${tolerance}:${rgb.join(",")}`;
|
|
33
42
|
const cached = cache.get(key);
|
|
34
43
|
if (cached) return cached;
|
|
35
|
-
const converted =
|
|
44
|
+
const converted = destinationColor(profile, rgb, tolerance);
|
|
36
45
|
cache.set(key, converted);
|
|
37
46
|
return converted;
|
|
38
47
|
};
|
|
39
48
|
}
|
|
40
49
|
//#endregion
|
|
41
|
-
export { assertNeutralTolerance, cachedPressColor, formatPdfNumber, newlySnapsToNeutral, pressColor, snapsToNeutral };
|
|
50
|
+
export { assertNeutralTolerance, cachedPressColor, destinationColor, formatPdfNumber, newlySnapsToNeutral, pdfMaskLuminosity, pressColor, pressDestination, snapsToNeutral };
|
package/dist/press/content.js
CHANGED
|
@@ -120,7 +120,7 @@ function inlineHeader(source, at) {
|
|
|
120
120
|
}
|
|
121
121
|
return refusal("truncated BI operator", "an ID and EI pair");
|
|
122
122
|
}
|
|
123
|
-
function inlineImageEnd(source, at) {
|
|
123
|
+
function inlineImageEnd(source, at, destination) {
|
|
124
124
|
const { colorSpace, id, imageMask } = inlineHeader(source, at);
|
|
125
125
|
const match = /[\0\t\n\f\r ]EI(?=[\0\t\n\f\r ()<>[\]{}/%]|$)/g;
|
|
126
126
|
match.lastIndex = id;
|
|
@@ -129,6 +129,7 @@ function inlineImageEnd(source, at) {
|
|
|
129
129
|
const name = colorSpace?.startsWith("/") ? colorSpace.slice(1) : void 0;
|
|
130
130
|
const policy = inlineImageAction(name, imageMask);
|
|
131
131
|
if (policy.action === "refuse") refusal(`BI operator with color space ${policy.label}`, policy.supported);
|
|
132
|
+
if (destination === "gray" && policy.kind === "cmyk") refusal(`BI operator with color space ${policy.label}`, "RGB or gray input for a greyscale press");
|
|
132
133
|
return end.index + end[0].length;
|
|
133
134
|
}
|
|
134
135
|
function skippedConstruct(source, cursor) {
|
|
@@ -144,7 +145,7 @@ function numeric(operands, count, operator) {
|
|
|
144
145
|
return operands.map((token) => Number(token.raw));
|
|
145
146
|
}
|
|
146
147
|
function replacement(token, operands, values) {
|
|
147
|
-
const operator = token.raw === "
|
|
148
|
+
const operator = values.length === 1 ? token.raw === token.raw.toLowerCase() ? "g" : "G" : token.raw === token.raw.toLowerCase() ? "k" : "K";
|
|
148
149
|
return {
|
|
149
150
|
start: operands[0]?.start ?? token.start,
|
|
150
151
|
end: token.end,
|
|
@@ -169,36 +170,43 @@ function replaceGray(token, operands, convert, counts) {
|
|
|
169
170
|
gray
|
|
170
171
|
]));
|
|
171
172
|
}
|
|
172
|
-
function validateSpace(token, operands) {
|
|
173
|
+
function validateSpace(token, operands, destination) {
|
|
173
174
|
if (operands.length !== 1 || operands[0]?.kind !== "name") refusal(`${token.raw} operator operands`, "one color space name");
|
|
174
175
|
const name = operands[0].raw.slice(1);
|
|
175
|
-
const policy = contentOperatorAction(token.raw, false, name);
|
|
176
|
+
const policy = contentOperatorAction(token.raw, false, name, false, destination);
|
|
176
177
|
if (policy.action === "refuse") refusal(`${token.raw} operator selecting ${colorSpaceLabel({ name })}`, policy.supported);
|
|
177
178
|
}
|
|
178
|
-
function enforceOperatorPolicy(token, operands, policy) {
|
|
179
|
+
function enforceOperatorPolicy(token, operands, policy, destination) {
|
|
179
180
|
if (policy.action !== "refuse") return;
|
|
180
181
|
const operator = token.raw;
|
|
181
182
|
if (operator === "cs" || operator === "CS") {
|
|
182
|
-
validateSpace(token, operands);
|
|
183
|
+
validateSpace(token, operands, destination);
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
186
|
if (operator === "sc" || operator === "SC" || operator === "scn" || operator === "SCN") refusal(`${operator} operator with numeric operands`, policy.supported);
|
|
186
|
-
refusal(`${operator === "g" || operator === "G" ? "gray" : "RGB"} color operator ${operator} after conversion`, policy.supported);
|
|
187
|
+
refusal(`${operator === "g" || operator === "G" ? "gray" : operator === "k" || operator === "K" ? "CMYK" : "RGB"} color operator ${operator} after conversion`, policy.supported);
|
|
187
188
|
}
|
|
188
|
-
function colorOperator(token, operands, convert, counts, inspect) {
|
|
189
|
+
function colorOperator(token, operands, convert, counts, inspect, destination) {
|
|
189
190
|
const operator = token.raw;
|
|
190
191
|
if (!isContentColorOperator(operator)) return void 0;
|
|
191
192
|
const patternName = operands.length === 1 && operands[0]?.kind === "name";
|
|
192
|
-
enforceOperatorPolicy(token, operands, contentOperatorAction(operator, inspect, void 0, patternName));
|
|
193
|
+
enforceOperatorPolicy(token, operands, contentOperatorAction(operator, inspect, void 0, patternName, destination), destination);
|
|
193
194
|
if (operator === "rg" || operator === "RG") return replaceRgb(token, operands, convert, counts);
|
|
194
|
-
if (operator === "g" || operator === "G")
|
|
195
|
-
|
|
195
|
+
if (operator === "g" || operator === "G") {
|
|
196
|
+
if (inspect && destination === "gray") {
|
|
197
|
+
numeric(operands, 1, operator);
|
|
198
|
+
counts[operator === "g" ? "fills" : "strokes"]++;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
return replaceGray(token, operands, convert, counts);
|
|
202
|
+
}
|
|
203
|
+
if (operator === "cs" || operator === "CS") validateSpace(token, operands, destination);
|
|
196
204
|
else if (operator === "k" || operator === "K") {
|
|
197
205
|
numeric(operands, 4, operator);
|
|
198
206
|
counts[operator === "k" ? "fills" : "strokes"]++;
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
|
-
function scan(source, convert, inspect, visit, operatorVisit) {
|
|
209
|
+
function scan(source, convert, inspect, destination, visit, operatorVisit) {
|
|
202
210
|
const counts = {
|
|
203
211
|
fills: 0,
|
|
204
212
|
strokes: 0
|
|
@@ -218,11 +226,11 @@ function scan(source, convert, inspect, visit, operatorVisit) {
|
|
|
218
226
|
}
|
|
219
227
|
cursor = token.end;
|
|
220
228
|
if (token.raw === "BI") {
|
|
221
|
-
cursor = inlineImageEnd(source, cursor);
|
|
229
|
+
cursor = inlineImageEnd(source, cursor, destination);
|
|
222
230
|
operands = [];
|
|
223
231
|
} else if (token.kind === "operator") {
|
|
224
232
|
operatorVisit?.(token, operands);
|
|
225
|
-
const found = colorOperator(token, operands, convert, counts, inspect);
|
|
233
|
+
const found = colorOperator(token, operands, convert, counts, inspect, destination);
|
|
226
234
|
if (found) visit?.(found);
|
|
227
235
|
operands = [];
|
|
228
236
|
} else {
|
|
@@ -241,7 +249,7 @@ function visitContentOperations(bytes, visit) {
|
|
|
241
249
|
0,
|
|
242
250
|
0,
|
|
243
251
|
0
|
|
244
|
-
], false, void 0, (operator, operands) => {
|
|
252
|
+
], false, "cmyk", void 0, (operator, operands) => {
|
|
245
253
|
visit({
|
|
246
254
|
end: operator.end,
|
|
247
255
|
operands: operands.flatMap((operand) => operand.kind === "name" || operand.kind === "number" ? [{
|
|
@@ -255,14 +263,14 @@ function visitContentOperations(bytes, visit) {
|
|
|
255
263
|
});
|
|
256
264
|
});
|
|
257
265
|
}
|
|
258
|
-
function inspectContent(bytes) {
|
|
259
|
-
return scan(decoder.decode(bytes), () => refusal("RGB color operator after conversion", "CMYK operators
|
|
266
|
+
function inspectContent(bytes, destination = "cmyk") {
|
|
267
|
+
return scan(decoder.decode(bytes), () => refusal("RGB color operator after conversion", `${destination === "gray" ? "gray" : "CMYK"} operators`), true, destination);
|
|
260
268
|
}
|
|
261
|
-
function rewriteContent(bytes, convert) {
|
|
269
|
+
function rewriteContent(bytes, convert, destination = "cmyk") {
|
|
262
270
|
const source = decoder.decode(bytes);
|
|
263
271
|
let outputLength = bytes.length;
|
|
264
272
|
let changed = false;
|
|
265
|
-
const counts = scan(source, convert, false, (found) => {
|
|
273
|
+
const counts = scan(source, convert, false, destination, (found) => {
|
|
266
274
|
changed = true;
|
|
267
275
|
outputLength += encoder.encode(found.text).length - (found.end - found.start);
|
|
268
276
|
});
|
|
@@ -273,7 +281,7 @@ function rewriteContent(bytes, convert) {
|
|
|
273
281
|
const output = new Uint8Array(outputLength);
|
|
274
282
|
let inputAt = 0;
|
|
275
283
|
let outputAt = 0;
|
|
276
|
-
scan(source, convert, false, (found) => {
|
|
284
|
+
scan(source, convert, false, destination, (found) => {
|
|
277
285
|
const untouched = bytes.subarray(inputAt, found.start);
|
|
278
286
|
output.set(untouched, outputAt);
|
|
279
287
|
outputAt += untouched.length;
|