@segnavia/render 0.3.0 → 0.5.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-destinations.js +101 -0
- package/dist/book-margins.js +20 -0
- package/dist/book-pdf.js +233 -0
- package/dist/book-render.js +48 -0
- package/dist/deck-pdf.js +4 -2
- package/dist/index.d.ts +4 -37
- package/dist/index.js +17 -3
- package/dist/options.d.ts +39 -0
- package/dist/overflow-containment.js +7 -0
- package/dist/press/adapter.js +27 -15
- package/dist/press/color-policy.js +17 -7
- package/dist/press/report.js +2 -1
- package/dist/press/types.d.ts +2 -0
- package/package.json +4 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { RenderRefusedError } from "./errors.js";
|
|
2
|
+
import { PDFArray, PDFDict, PDFHexString, PDFName, PDFNumber, PDFRef, PDFString } from "pdf-lib";
|
|
3
|
+
//#region src/book-destinations.ts
|
|
4
|
+
const key = (value) => PDFName.of(value);
|
|
5
|
+
const resolve = (document, value) => value instanceof PDFRef ? document.context.lookup(value) : value;
|
|
6
|
+
function decode(value) {
|
|
7
|
+
if (value instanceof PDFString || value instanceof PDFHexString) return value.decodeText();
|
|
8
|
+
if (value instanceof PDFName) return value.toString().slice(1);
|
|
9
|
+
}
|
|
10
|
+
function destination(document, value) {
|
|
11
|
+
let found = resolve(document, value);
|
|
12
|
+
if (found instanceof PDFDict) found = resolve(document, found.get(key("D")));
|
|
13
|
+
if (!(found instanceof PDFArray)) return void 0;
|
|
14
|
+
const reference = found.get(0);
|
|
15
|
+
if (!(reference instanceof PDFRef)) return void 0;
|
|
16
|
+
const page = document.getPages().findIndex((candidate) => candidate.ref.toString() === reference.toString());
|
|
17
|
+
if (page < 0) return void 0;
|
|
18
|
+
const x = resolve(document, found.get(2));
|
|
19
|
+
const y = resolve(document, found.get(3));
|
|
20
|
+
return {
|
|
21
|
+
page: page + 1,
|
|
22
|
+
x: x instanceof PDFNumber ? x.asNumber() : 0,
|
|
23
|
+
y: y instanceof PDFNumber ? y.asNumber() : document.getPage(page).getHeight()
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function nameTree(document, value, output) {
|
|
27
|
+
const dictionary = resolve(document, value);
|
|
28
|
+
if (!(dictionary instanceof PDFDict)) return;
|
|
29
|
+
const names = resolve(document, dictionary.get(key("Names")));
|
|
30
|
+
if (names instanceof PDFArray) for (let index = 0; index + 1 < names.size(); index += 2) {
|
|
31
|
+
const name = decode(resolve(document, names.get(index)));
|
|
32
|
+
const target = destination(document, names.get(index + 1));
|
|
33
|
+
if (name && target) output.set(name, target);
|
|
34
|
+
}
|
|
35
|
+
const kids = resolve(document, dictionary.get(key("Kids")));
|
|
36
|
+
if (kids instanceof PDFArray) for (let index = 0; index < kids.size(); index += 1) nameTree(document, kids.get(index), output);
|
|
37
|
+
}
|
|
38
|
+
function namedDestinations(document) {
|
|
39
|
+
const output = /* @__PURE__ */ new Map();
|
|
40
|
+
const names = resolve(document, document.catalog.get(key("Names")));
|
|
41
|
+
if (names instanceof PDFDict) nameTree(document, names.get(key("Dests")), output);
|
|
42
|
+
const direct = resolve(document, document.catalog.get(key("Dests")));
|
|
43
|
+
if (direct instanceof PDFDict) for (const [name, value] of direct.entries()) {
|
|
44
|
+
const target = destination(document, value);
|
|
45
|
+
if (target) output.set(name.toString().slice(1), target);
|
|
46
|
+
}
|
|
47
|
+
return output;
|
|
48
|
+
}
|
|
49
|
+
function hiddenAnnotation(document, annotation) {
|
|
50
|
+
const rectangle = resolve(document, annotation.get(key("Rect")));
|
|
51
|
+
if (!(rectangle instanceof PDFArray) || rectangle.size() !== 4) return false;
|
|
52
|
+
const values = Array.from({ length: 4 }, (_, index) => resolve(document, rectangle.get(index)));
|
|
53
|
+
if (!values.every((value) => value instanceof PDFNumber)) return false;
|
|
54
|
+
const [left = 0, bottom = 0, right = 0, top = 0] = values.map((value) => value.asNumber());
|
|
55
|
+
return Math.abs(right - left) <= 1 && Math.abs(top - bottom) <= 1;
|
|
56
|
+
}
|
|
57
|
+
function explicit(document, target, offset) {
|
|
58
|
+
return document.context.obj([
|
|
59
|
+
document.getPage(offset + target.page - 1).ref,
|
|
60
|
+
"XYZ",
|
|
61
|
+
target.x,
|
|
62
|
+
target.y,
|
|
63
|
+
null
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
function customTarget(uri) {
|
|
67
|
+
if (!uri?.startsWith("segnavia:target/")) return void 0;
|
|
68
|
+
try {
|
|
69
|
+
return decodeURIComponent(uri.slice(16));
|
|
70
|
+
} catch {
|
|
71
|
+
return uri.slice(16);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function rewriteBookLinks(document, frontPages, front, body) {
|
|
75
|
+
for (const [pageIndex, page] of document.getPages().entries()) {
|
|
76
|
+
const annotations = resolve(document, page.node.get(key("Annots")));
|
|
77
|
+
if (!(annotations instanceof PDFArray)) continue;
|
|
78
|
+
const kept = Array.from({ length: annotations.size() }, (_, index) => annotations.get(index)).filter((object) => rewriteAnnotation(document, object, pageIndex, frontPages, front, body));
|
|
79
|
+
if (kept.length > 0) page.node.set(key("Annots"), document.context.obj(kept));
|
|
80
|
+
else page.node.delete(key("Annots"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function rewriteAnnotation(document, object, pageIndex, frontPages, front, body) {
|
|
84
|
+
const annotation = resolve(document, object);
|
|
85
|
+
if (!(annotation instanceof PDFDict)) return true;
|
|
86
|
+
if (hiddenAnnotation(document, annotation)) return false;
|
|
87
|
+
const named = decode(resolve(document, annotation.get(key("Dest"))));
|
|
88
|
+
const action = resolve(document, annotation.get(key("A")));
|
|
89
|
+
const uri = action instanceof PDFDict ? decode(resolve(document, action.get(key("URI")))) : void 0;
|
|
90
|
+
const cross = customTarget(uri);
|
|
91
|
+
const target = cross ? front.get(cross) ?? body.get(cross) : named ? pageIndex < frontPages ? front.get(named) : body.get(named) : void 0;
|
|
92
|
+
if (cross && !target) throw new RenderRefusedError(`The segnavia link to ${cross} has no printed destination.`);
|
|
93
|
+
if (target) {
|
|
94
|
+
const offset = front.has(cross ?? "") || !cross && pageIndex < frontPages ? 0 : frontPages;
|
|
95
|
+
annotation.set(key("Dest"), explicit(document, target, offset));
|
|
96
|
+
annotation.delete(key("A"));
|
|
97
|
+
} else if (uri?.startsWith("segnavia:")) throw new RenderRefusedError(`The PDF still contains the segnavia link ${uri}.`);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { namedDestinations, rewriteBookLinks };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolveDirection, sheetMm } from "@segnavia/format";
|
|
2
|
+
//#region src/book-margins.ts
|
|
3
|
+
function escapeHtml(value) {
|
|
4
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
5
|
+
}
|
|
6
|
+
function marginCss(book) {
|
|
7
|
+
if (!book.page) return "";
|
|
8
|
+
const size = sheetMm(book.page);
|
|
9
|
+
const edge = book.page.margin;
|
|
10
|
+
return `@page{size:${size.width}mm ${size.height}mm;margin:0}:where(html,body){margin:0}:where(.segnavia-margin-page){position:relative;inline-size:${size.width}mm;block-size:${size.height}mm;break-after:page;direction:ltr}:where(.segnavia-running-head,.segnavia-folio){position:absolute;display:flex;align-items:center}:where(.segnavia-running-head){inset-block-start:0;block-size:${edge.top}mm}:where(.segnavia-folio){inset-block-end:0;block-size:${edge.bottom}mm}:where([data-side="left"] .segnavia-running-head,[data-side="left"] .segnavia-folio){inset-inline-start:${edge.outer}mm;inset-inline-end:${edge.inner}mm;justify-content:flex-start}:where([data-side="right"] .segnavia-running-head,[data-side="right"] .segnavia-folio){inset-inline-start:${edge.inner}mm;inset-inline-end:${edge.outer}mm;justify-content:flex-end}`;
|
|
11
|
+
}
|
|
12
|
+
function marginDocument(book, pages) {
|
|
13
|
+
return pages.map((page) => {
|
|
14
|
+
const head = page.runningHead === void 0 ? "" : `<div class="segnavia-running-head"><bdi dir="${resolveDirection(book.dir, book.lang, page.runningHead)}">${escapeHtml(page.runningHead)}</bdi></div>`;
|
|
15
|
+
const folio = page.folio === void 0 ? "" : `<div class="segnavia-folio"><bdi dir="${resolveDirection(book.dir, book.lang, page.folio)}">${escapeHtml(page.folio)}</bdi></div>`;
|
|
16
|
+
return `<div class="segnavia-margin-page" data-side="${page.side}">${head}${folio}</div>`;
|
|
17
|
+
}).join("");
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { marginCss, marginDocument };
|
package/dist/book-pdf.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { marginDocument } from "./book-margins.js";
|
|
2
|
+
import { RenderRefusedError } from "./errors.js";
|
|
3
|
+
import { namedDestinations, rewriteBookLinks } from "./book-destinations.js";
|
|
4
|
+
import { Book, formatNumber, resolveDirection, sheetMm } from "@segnavia/format";
|
|
5
|
+
import { PDFDocument } from "pdf-lib";
|
|
6
|
+
import { bookDocument, bookPrintPart } from "@segnavia/html";
|
|
7
|
+
//#region src/book-pdf.ts
|
|
8
|
+
async function inspect(bytes) {
|
|
9
|
+
const document = await PDFDocument.load(bytes);
|
|
10
|
+
return {
|
|
11
|
+
bytes,
|
|
12
|
+
pages: document.getPageCount(),
|
|
13
|
+
destinations: namedDestinations(document)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function hashTargets(document) {
|
|
17
|
+
return [...new Set([...document.html.matchAll(/href="#([^"]+)"/g)].map((match) => match[1]))];
|
|
18
|
+
}
|
|
19
|
+
function requireTargets(part, expected) {
|
|
20
|
+
const missing = expected.filter((id) => !part.destinations.has(id));
|
|
21
|
+
if (missing.length > 0) throw new RenderRefusedError(`Chromium wrote ${expected.length - missing.length} of ${expected.length} named destinations for hidden targets; missing ${missing.join(", ")}.`);
|
|
22
|
+
}
|
|
23
|
+
const sectionsFor = (sections, part) => sections.filter((section) => bookPrintPart(section.kind) === part);
|
|
24
|
+
function planBlanks(sections, destinations) {
|
|
25
|
+
const ids = [];
|
|
26
|
+
const predicted = /* @__PURE__ */ new Map();
|
|
27
|
+
let blanks = 0;
|
|
28
|
+
for (const section of sectionsFor(sections, "body")) {
|
|
29
|
+
const original = destinations.get(section.id)?.page;
|
|
30
|
+
if (original === void 0) throw new RenderRefusedError(`The first body print has no destination for ${section.id}.`);
|
|
31
|
+
let page = original + blanks;
|
|
32
|
+
if (page % 2 === 0) {
|
|
33
|
+
ids.push(section.id);
|
|
34
|
+
blanks += 1;
|
|
35
|
+
page += 1;
|
|
36
|
+
}
|
|
37
|
+
predicted.set(section.id, page);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ids,
|
|
41
|
+
predicted
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function verifyStarts(label, sections, expected, actual) {
|
|
45
|
+
for (const section of sections) {
|
|
46
|
+
const predicted = expected.get(section.id);
|
|
47
|
+
const found = actual.get(section.id)?.page;
|
|
48
|
+
if (predicted !== found) throw new RenderRefusedError(`${section.id} was predicted to start on ${label} page ${predicted}, but started on page ${found}.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function appendBlank(part) {
|
|
52
|
+
if (!part.bytes) return part;
|
|
53
|
+
const document = await PDFDocument.load(part.bytes);
|
|
54
|
+
const last = document.getPage(document.getPageCount() - 1);
|
|
55
|
+
document.addPage([last.getWidth(), last.getHeight()]);
|
|
56
|
+
return {
|
|
57
|
+
...part,
|
|
58
|
+
bytes: await document.save(),
|
|
59
|
+
pages: part.pages + 1
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function pageSide(book, physicalPage) {
|
|
63
|
+
const odd = resolveDirection(book.dir, book.lang, book.title) === "rtl" ? "left" : "right";
|
|
64
|
+
return physicalPage % 2 === 1 ? odd : odd === "left" ? "right" : "left";
|
|
65
|
+
}
|
|
66
|
+
function marginsFor(book, sections, destinations, pages, offset, blanks, system) {
|
|
67
|
+
const starts = sections.flatMap((section) => {
|
|
68
|
+
const page = destinations.get(section.id)?.page;
|
|
69
|
+
return page === void 0 ? [] : [{
|
|
70
|
+
page,
|
|
71
|
+
section
|
|
72
|
+
}];
|
|
73
|
+
}).sort((left, right) => left.page - right.page);
|
|
74
|
+
return Array.from({ length: pages }, (_, index) => {
|
|
75
|
+
const page = index + 1;
|
|
76
|
+
if (blanks.has(page)) return {
|
|
77
|
+
kind: "blank",
|
|
78
|
+
side: pageSide(book, offset + page)
|
|
79
|
+
};
|
|
80
|
+
const owner = starts.filter((start) => start.page <= page).at(-1);
|
|
81
|
+
return {
|
|
82
|
+
kind: owner?.section.kind ?? "blank",
|
|
83
|
+
side: pageSide(book, offset + page),
|
|
84
|
+
folio: formatNumber(page, system),
|
|
85
|
+
runningHead: owner?.page === page ? void 0 : owner?.section.runningHead
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function destinationOwner(id, destination, sections, destinations) {
|
|
90
|
+
const owner = sections.flatMap((section) => {
|
|
91
|
+
const start = destinations.get(section.id);
|
|
92
|
+
return start && start.page <= destination.page ? [{
|
|
93
|
+
section,
|
|
94
|
+
start
|
|
95
|
+
}] : [];
|
|
96
|
+
}).sort((left, right) => left.start.page - right.start.page).at(-1)?.section;
|
|
97
|
+
return owner ? `${owner.title ?? owner.id} (${owner.id})` : id;
|
|
98
|
+
}
|
|
99
|
+
function frontPrintStatus(prints, ids, sections) {
|
|
100
|
+
if (prints.length < 2) return { status: "more" };
|
|
101
|
+
const expected = prints.at(-2);
|
|
102
|
+
const actual = prints.at(-1);
|
|
103
|
+
for (const id of ids) {
|
|
104
|
+
const before = expected.get(id);
|
|
105
|
+
const after = actual.get(id);
|
|
106
|
+
if (!before || !after || before.page === after.page) continue;
|
|
107
|
+
if (prints.length < 3) return { status: "more" };
|
|
108
|
+
return {
|
|
109
|
+
status: "refused",
|
|
110
|
+
message: `Front destination ${id} in ${destinationOwner(id, before, sections, expected)} moved from page ${before.page} to page ${after.page} after three prints.`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { status: "stable" };
|
|
114
|
+
}
|
|
115
|
+
function finalNumbers(book, headings, front, body) {
|
|
116
|
+
return Object.fromEntries(headings.map((heading) => {
|
|
117
|
+
const frontPage = front.destinations.get(heading.id)?.page;
|
|
118
|
+
if (frontPage !== void 0) return [heading.id, formatNumber(frontPage, book.numbering.frontMatter)];
|
|
119
|
+
const bodyPage = body.destinations.get(heading.id)?.page;
|
|
120
|
+
if (bodyPage === void 0) throw new RenderRefusedError(`The heading ${heading.id} has no printed destination.`);
|
|
121
|
+
return [heading.id, formatNumber(bodyPage, book.numbering.body)];
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
async function printBody(book, options, adapter) {
|
|
125
|
+
const firstDocument = bookDocument(book, {
|
|
126
|
+
part: "body",
|
|
127
|
+
targets: true
|
|
128
|
+
});
|
|
129
|
+
if (firstDocument.issues.length > 0) throw new RenderRefusedError(`The book cannot be printed: ${firstDocument.issues.join("; ")}.`);
|
|
130
|
+
let part = await inspect(await adapter.print(firstDocument.html, book, options));
|
|
131
|
+
requireTargets(part, hashTargets(firstDocument));
|
|
132
|
+
const plan = book.rectoOpenings ? planBlanks(firstDocument.sections, part.destinations) : {
|
|
133
|
+
ids: [],
|
|
134
|
+
predicted: /* @__PURE__ */ new Map()
|
|
135
|
+
};
|
|
136
|
+
if (book.rectoOpenings) {
|
|
137
|
+
const finalDocument = bookDocument(book, {
|
|
138
|
+
part: "body",
|
|
139
|
+
targets: true,
|
|
140
|
+
blankBefore: plan.ids
|
|
141
|
+
});
|
|
142
|
+
part = await inspect(await adapter.print(finalDocument.html, book, options));
|
|
143
|
+
requireTargets(part, hashTargets(finalDocument));
|
|
144
|
+
verifyStarts("body", sectionsFor(firstDocument.sections, "body"), plan.predicted, part.destinations);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
document: firstDocument,
|
|
148
|
+
part,
|
|
149
|
+
blankBefore: plan.ids
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function printFront(book, source, body, options, adapter) {
|
|
153
|
+
const sections = sectionsFor(source.sections, "front");
|
|
154
|
+
if (sections.length === 0) return {
|
|
155
|
+
part: {
|
|
156
|
+
pages: 0,
|
|
157
|
+
destinations: /* @__PURE__ */ new Map()
|
|
158
|
+
},
|
|
159
|
+
trailingBlank: false
|
|
160
|
+
};
|
|
161
|
+
const ids = [];
|
|
162
|
+
const prints = [];
|
|
163
|
+
let numbers;
|
|
164
|
+
let final;
|
|
165
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
166
|
+
const document = bookDocument(book, {
|
|
167
|
+
part: "front",
|
|
168
|
+
targets: true,
|
|
169
|
+
crossLinks: true,
|
|
170
|
+
pageNumbers: numbers
|
|
171
|
+
});
|
|
172
|
+
const printed = await inspect(await adapter.print(document.html, book, options));
|
|
173
|
+
if (ids.length === 0) ids.push(...hashTargets(document));
|
|
174
|
+
requireTargets(printed, ids);
|
|
175
|
+
prints.push(printed);
|
|
176
|
+
const status = frontPrintStatus(prints.map((part) => part.destinations), ids, sections);
|
|
177
|
+
if (status.status === "stable") {
|
|
178
|
+
final = printed;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (status.status === "refused") throw new RenderRefusedError(status.message);
|
|
182
|
+
numbers = finalNumbers(book, source.headings, printed, body);
|
|
183
|
+
}
|
|
184
|
+
if (!final) throw new RenderRefusedError("The front print did not converge after three prints.");
|
|
185
|
+
const trailingBlank = book.rectoOpenings && final.pages % 2 === 1;
|
|
186
|
+
return {
|
|
187
|
+
part: trailingBlank ? await appendBlank(final) : final,
|
|
188
|
+
trailingBlank
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function join(front, body, marginBytes) {
|
|
192
|
+
const output = await PDFDocument.create();
|
|
193
|
+
for (const part of [front, body]) {
|
|
194
|
+
if (!part.bytes) continue;
|
|
195
|
+
const source = await PDFDocument.load(part.bytes);
|
|
196
|
+
for (const page of await output.copyPages(source, source.getPageIndices())) output.addPage(page);
|
|
197
|
+
}
|
|
198
|
+
const layer = await PDFDocument.load(marginBytes);
|
|
199
|
+
if (layer.getPageCount() !== output.getPageCount()) throw new RenderRefusedError(`The margin layer has ${layer.getPageCount()} pages, but the book has ${output.getPageCount()}.`);
|
|
200
|
+
for (const [index, page] of layer.getPages().entries()) {
|
|
201
|
+
const [embedded] = await output.embedPdf(layer, [index]);
|
|
202
|
+
if (embedded) output.getPage(index).drawPage(embedded, {
|
|
203
|
+
x: 0,
|
|
204
|
+
y: 0,
|
|
205
|
+
width: page.getWidth(),
|
|
206
|
+
height: page.getHeight()
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
rewriteBookLinks(output, front.pages, front.destinations, body.destinations);
|
|
210
|
+
return output.save();
|
|
211
|
+
}
|
|
212
|
+
async function renderBookPdf(input, options, adapter) {
|
|
213
|
+
const book = Book.parse(input);
|
|
214
|
+
if (book.mode === "paginated") throw new RenderRefusedError("bookPdf prints continuous books; printing a paginated book is not built yet.");
|
|
215
|
+
if (!book.page) throw new RenderRefusedError("Printing a continuous book needs page; set page before calling bookPdf.");
|
|
216
|
+
const printedBody = await printBody(book, options, adapter);
|
|
217
|
+
const printedFront = await printFront(book, printedBody.document, printedBody.part, options, adapter);
|
|
218
|
+
const front = printedFront.part;
|
|
219
|
+
const bodyBlankPages = new Set(printedBody.blankBefore.map((id) => (printedBody.part.destinations.get(id)?.page ?? 1) - 1));
|
|
220
|
+
const frontBlankPages = new Set(printedFront.trailingBlank ? [front.pages] : []);
|
|
221
|
+
const marginPages = [...marginsFor(book, sectionsFor(printedBody.document.sections, "front"), front.destinations, front.pages, 0, frontBlankPages, book.numbering.frontMatter), ...marginsFor(book, sectionsFor(printedBody.document.sections, "body"), printedBody.part.destinations, printedBody.part.pages, front.pages, bodyBlankPages, book.numbering.body)];
|
|
222
|
+
const marginBytes = await adapter.print(marginDocument(book, marginPages), book, options, true);
|
|
223
|
+
return adapter.finish(await join(front, printedBody.part, marginBytes), options);
|
|
224
|
+
}
|
|
225
|
+
function bookPageCss(book, marginLayer = false) {
|
|
226
|
+
if (!book.page) return "";
|
|
227
|
+
const size = sheetMm(book.page);
|
|
228
|
+
if (marginLayer) return `@page{size:${size.width}mm ${size.height}mm;margin:0}`;
|
|
229
|
+
const edge = book.page.margin;
|
|
230
|
+
return `@page{size:${size.width}mm ${size.height}mm;margin-top:${edge.top}mm;margin-bottom:${edge.bottom}mm}@page :right{margin-left:${edge.inner}mm;margin-right:${edge.outer}mm}@page :left{margin-left:${edge.outer}mm;margin-right:${edge.inner}mm}`;
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
export { bookPageCss, frontPrintStatus, renderBookPdf };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { marginCss } from "./book-margins.js";
|
|
2
|
+
import { RenderRefusedError } from "./errors.js";
|
|
3
|
+
import { bookPageCss, renderBookPdf } from "./book-pdf.js";
|
|
4
|
+
import { PX_PER_MM, resolveDirection, sheetMm } from "@segnavia/format";
|
|
5
|
+
import { frameDocument, newNonce } from "@segnavia/frame/bridge";
|
|
6
|
+
//#region src/book-render.ts
|
|
7
|
+
function renderBrowserBookPdf(input, options, dependencies) {
|
|
8
|
+
return renderBookPdf(input, options, {
|
|
9
|
+
print: (html, book, renderOptions, marginLayer = false) => {
|
|
10
|
+
if (!book.page) throw new RenderRefusedError("Printing a continuous book needs page; set page before calling bookPdf.");
|
|
11
|
+
const size = sheetMm(book.page);
|
|
12
|
+
const css = `${bookPageCss(book, marginLayer)}${marginLayer ? marginCss(book) : ""}${dependencies.fontFaces(renderOptions)}${renderOptions.css ?? ""}`;
|
|
13
|
+
return dependencies.render({
|
|
14
|
+
viewport: {
|
|
15
|
+
width: Math.round(size.width * PX_PER_MM),
|
|
16
|
+
height: Math.round(size.height * PX_PER_MM)
|
|
17
|
+
},
|
|
18
|
+
scripts: !marginLayer && book.scripts === "sandboxed",
|
|
19
|
+
html: frameDocument(html, {
|
|
20
|
+
scripts: marginLayer ? "refuse" : book.scripts,
|
|
21
|
+
nonce: newNonce(),
|
|
22
|
+
dir: marginLayer ? "ltr" : resolveDirection(book.dir, book.lang, book.title),
|
|
23
|
+
lang: book.lang,
|
|
24
|
+
css,
|
|
25
|
+
relayKeys: false,
|
|
26
|
+
reportHeight: false
|
|
27
|
+
}),
|
|
28
|
+
waitForReady: !marginLayer && book.scripts === "sandboxed" && renderOptions.waitForPageReady !== false,
|
|
29
|
+
boundMs: renderOptions.boundMs,
|
|
30
|
+
beforeOutput: renderOptions.beforeOutput
|
|
31
|
+
}, {
|
|
32
|
+
width: `${size.width}mm`,
|
|
33
|
+
height: `${size.height}mm`,
|
|
34
|
+
printBackground: true,
|
|
35
|
+
preferCSSPageSize: true,
|
|
36
|
+
margin: {
|
|
37
|
+
top: "0mm",
|
|
38
|
+
right: "0mm",
|
|
39
|
+
bottom: "0mm",
|
|
40
|
+
left: "0mm"
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
finish: dependencies.finish
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { renderBrowserBookPdf };
|
package/dist/deck-pdf.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RenderRefusedError } from "./errors.js";
|
|
2
|
+
import { containFixedPageOverflow } from "./overflow-containment.js";
|
|
2
3
|
import { pdfPixelBox } from "./pdf-geometry.js";
|
|
3
4
|
import { composeImposedPdf, drawClippedPage, embedOnePage } from "./pdf-compose.js";
|
|
4
5
|
import { Imposition, fitPage, impose, pagePlacementLayout } from "@segnavia/format";
|
|
@@ -20,6 +21,7 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
20
21
|
html: slide.content.html
|
|
21
22
|
}] : []);
|
|
22
23
|
const css = `${services.fontFaces(opts)}${opts.css ?? ""}`;
|
|
24
|
+
const slideCss = containFixedPageOverflow(css);
|
|
23
25
|
const notesGeometry = opts.layout === "notes" ? notesLayoutGeometry(deck.size, opts.notesPaper) : void 0;
|
|
24
26
|
const [printable, rendered] = await Promise.all([services.renderPdf({
|
|
25
27
|
viewport: notesGeometry ? {
|
|
@@ -46,7 +48,7 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
46
48
|
scripts: deck.scripts === "sandboxed",
|
|
47
49
|
html: htmlSlideDocument(deck, html, {
|
|
48
50
|
nonce: newNonce(),
|
|
49
|
-
css
|
|
51
|
+
css: slideCss
|
|
50
52
|
}),
|
|
51
53
|
boundMs: opts.boundMs
|
|
52
54
|
}, {
|
|
@@ -84,7 +86,7 @@ async function regularDeckPdf(deck, opts, services) {
|
|
|
84
86
|
return services.finish(await output.save(), opts);
|
|
85
87
|
}
|
|
86
88
|
async function imposedDeckPdf(deck, imposition, opts, services) {
|
|
87
|
-
const css = `${services.fontFaces(opts)}${opts.css ?? ""}
|
|
89
|
+
const css = containFixedPageOverflow(`${services.fontFaces(opts)}${opts.css ?? ""}`);
|
|
88
90
|
const imposed = impose(imposition, deck.slides.length);
|
|
89
91
|
const rendered = await Promise.all(deck.slides.map(async (slide, index) => {
|
|
90
92
|
const single = {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { EffectsReport } from "./effects/types.js";
|
|
2
2
|
import { PressImage, PressReport } from "./press/types.js";
|
|
3
|
+
import { Font, ImageOptions, RenderLimits, RenderOptions } from "./options.js";
|
|
3
4
|
import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
|
|
4
|
-
import { Deck, ImpositionInput, PageInput } from "@segnavia/format";
|
|
5
|
+
import { BookInput, Deck, ImpositionInput, PageInput } from "@segnavia/format";
|
|
5
6
|
import { PrintOptions } from "@segnavia/frame/print-document";
|
|
6
7
|
import { Page as Page$1 } from "playwright-core";
|
|
7
8
|
//#region src/index.d.ts
|
|
@@ -11,41 +12,6 @@ export declare const CONTAINER_ARGS: string[];
|
|
|
11
12
|
export declare class RenderTimeoutError extends Error {
|
|
12
13
|
name: string;
|
|
13
14
|
}
|
|
14
|
-
export interface Font {
|
|
15
|
-
family: string;
|
|
16
|
-
data: Uint8Array;
|
|
17
|
-
format: 'woff2' | 'woff' | 'truetype' | 'opentype';
|
|
18
|
-
weight?: number | string;
|
|
19
|
-
style?: 'normal' | 'italic';
|
|
20
|
-
}
|
|
21
|
-
export interface RenderLimits {
|
|
22
|
-
maxSheets?: number;
|
|
23
|
-
maxBytes?: number;
|
|
24
|
-
}
|
|
25
|
-
export interface RenderOptions {
|
|
26
|
-
css?: string;
|
|
27
|
-
fonts?: Font[];
|
|
28
|
-
limits?: RenderLimits;
|
|
29
|
-
boundMs?: number;
|
|
30
|
-
waitForPageReady?: boolean;
|
|
31
|
-
preferCssPageSize?: boolean;
|
|
32
|
-
beforeOutput?: (tab: Page$1) => Promise<void>;
|
|
33
|
-
effects?: false | {
|
|
34
|
-
report?: (report: EffectsReport) => void;
|
|
35
|
-
};
|
|
36
|
-
press?: {
|
|
37
|
-
/** The ICC profile of the press, as bytes. The library ships none. */
|
|
38
|
-
profile: Uint8Array;
|
|
39
|
-
/** Called with authored RGB images left unchanged and counts read back from the output. */
|
|
40
|
-
report?: (report: PressReport) => void;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
export interface ImageOptions extends RenderOptions {
|
|
44
|
-
format?: 'png' | 'jpeg' | 'webp';
|
|
45
|
-
quality?: number[];
|
|
46
|
-
scale?: number;
|
|
47
|
-
width?: number;
|
|
48
|
-
}
|
|
49
15
|
export interface RendererOptions {
|
|
50
16
|
executablePath: string;
|
|
51
17
|
args?: string[];
|
|
@@ -53,6 +19,7 @@ export interface RendererOptions {
|
|
|
53
19
|
jsHeapMb?: number;
|
|
54
20
|
}
|
|
55
21
|
export interface Renderer {
|
|
22
|
+
bookPdf(book: BookInput, options?: RenderOptions): Promise<Uint8Array>;
|
|
56
23
|
pagePdf(page: PageInput, options?: RenderOptions): Promise<Uint8Array>;
|
|
57
24
|
pagesPdf(pages: PageInput[], options?: RenderOptions & {
|
|
58
25
|
imposition?: ImpositionInput;
|
|
@@ -66,4 +33,4 @@ export declare function fontFaces(fonts?: Font[]): string;
|
|
|
66
33
|
export declare function captureImage(tab: Page$1, options: Pick<ImageOptions, 'format' | 'quality' | 'limits'>, fullPage: boolean): Promise<Uint8Array>;
|
|
67
34
|
export declare function openRenderer(options: RendererOptions): Promise<Renderer>;
|
|
68
35
|
//#endregion
|
|
69
|
-
export { type EffectsReport, type PressImage, type PressReport, RenderRefusedError, RendererUnavailableError };
|
|
36
|
+
export { type EffectsReport, type Font, type ImageOptions, type PressImage, type PressReport, type RenderLimits, type RenderOptions, RenderRefusedError, RendererUnavailableError };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
|
|
2
|
+
import { renderBrowserBookPdf } from "./book-render.js";
|
|
3
|
+
import { containFixedPageOverflow } from "./overflow-containment.js";
|
|
2
4
|
import { renderDeckPdf } from "./deck-pdf.js";
|
|
5
|
+
import { assertNeutralTolerance } from "./press/color-policy.js";
|
|
3
6
|
import { rewriteEffectsPdf } from "./effects/adapter.js";
|
|
4
7
|
import { assertPrintOptions, printCss, printPage, printViewport } from "./page-paper.js";
|
|
5
8
|
import { renderPagesPdf } from "./pages-pdf.js";
|
|
@@ -61,7 +64,9 @@ async function finishPdf(pdf, options) {
|
|
|
61
64
|
options.effects?.report?.(rewritten.report);
|
|
62
65
|
}
|
|
63
66
|
if (options.press) {
|
|
64
|
-
const
|
|
67
|
+
const neutralTolerance = options.press.neutralTolerance ?? 3;
|
|
68
|
+
assertNeutralTolerance(neutralTolerance);
|
|
69
|
+
const converted = await rewritePressPdf(output, options.press.profile, neutralTolerance);
|
|
65
70
|
output = converted.bytes;
|
|
66
71
|
options.press.report?.(converted.report);
|
|
67
72
|
}
|
|
@@ -187,7 +192,9 @@ async function openRenderer(options) {
|
|
|
187
192
|
}
|
|
188
193
|
}
|
|
189
194
|
function pageDocument(page, options, printing = false) {
|
|
190
|
-
const
|
|
195
|
+
const hostCss = `${page.print ? printing ? printCss(page.print) : printProperties(page.print) : ""}${fontFaces(options.fonts)}${options.css ?? ""}`;
|
|
196
|
+
const fixedSize = typeof page.size === "object" && page.size.height !== "auto";
|
|
197
|
+
const css = printing && !page.print && fixedSize ? containFixedPageOverflow(hostCss) : hostCss;
|
|
191
198
|
return frameDocument(page.html, {
|
|
192
199
|
scripts: page.scripts,
|
|
193
200
|
nonce: newNonce(),
|
|
@@ -238,7 +245,7 @@ async function openRenderer(options) {
|
|
|
238
245
|
nonce: newNonce(),
|
|
239
246
|
dir: page.dir,
|
|
240
247
|
lang: page.lang,
|
|
241
|
-
css,
|
|
248
|
+
css: containFixedPageOverflow(css),
|
|
242
249
|
relayKeys: false,
|
|
243
250
|
reportHeight: false
|
|
244
251
|
});
|
|
@@ -264,6 +271,13 @@ async function openRenderer(options) {
|
|
|
264
271
|
}));
|
|
265
272
|
}
|
|
266
273
|
return {
|
|
274
|
+
bookPdf(input, opts = {}) {
|
|
275
|
+
return renderBrowserBookPdf(input, opts, {
|
|
276
|
+
render: (setup, pdf) => withTab(setup, (tab) => tab.pdf(pdf)),
|
|
277
|
+
fontFaces: (options) => fontFaces(options.fonts),
|
|
278
|
+
finish: finishPdf
|
|
279
|
+
});
|
|
280
|
+
},
|
|
267
281
|
async pagePdf(input, opts = {}) {
|
|
268
282
|
return finishPdf(await renderPagePdf(Page.parse(input), opts), opts);
|
|
269
283
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EffectsReport } from "./effects/types.js";
|
|
2
|
+
import { PressReport } from "./press/types.js";
|
|
3
|
+
import { Page } from "playwright-core";
|
|
4
|
+
//#region src/options.d.ts
|
|
5
|
+
export interface Font {
|
|
6
|
+
family: string;
|
|
7
|
+
data: Uint8Array;
|
|
8
|
+
format: 'woff2' | 'woff' | 'truetype' | 'opentype';
|
|
9
|
+
weight?: number | string;
|
|
10
|
+
style?: 'normal' | 'italic';
|
|
11
|
+
}
|
|
12
|
+
export interface RenderLimits {
|
|
13
|
+
maxSheets?: number;
|
|
14
|
+
maxBytes?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface RenderOptions {
|
|
17
|
+
css?: string;
|
|
18
|
+
fonts?: Font[];
|
|
19
|
+
limits?: RenderLimits;
|
|
20
|
+
boundMs?: number;
|
|
21
|
+
waitForPageReady?: boolean;
|
|
22
|
+
preferCssPageSize?: boolean;
|
|
23
|
+
beforeOutput?: (tab: Page) => Promise<void>;
|
|
24
|
+
effects?: false | {
|
|
25
|
+
report?: (report: EffectsReport) => void;
|
|
26
|
+
};
|
|
27
|
+
press?: {
|
|
28
|
+
profile: Uint8Array;
|
|
29
|
+
neutralTolerance?: number;
|
|
30
|
+
report?: (report: PressReport) => void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface ImageOptions extends RenderOptions {
|
|
34
|
+
format?: 'png' | 'jpeg' | 'webp';
|
|
35
|
+
quality?: number[];
|
|
36
|
+
scale?: number;
|
|
37
|
+
width?: number;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
package/dist/press/adapter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RenderRefusedError } from "../errors.js";
|
|
2
|
-
import { cachedPressColor, pressColor } from "./color-policy.js";
|
|
2
|
+
import { cachedPressColor, newlySnapsToNeutral, pressColor } 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";
|
|
@@ -77,7 +77,7 @@ function convertType1Shading(state, shading, pattern) {
|
|
|
77
77
|
const domain = sampleDomainForUses(shadingDomainOf(shading), uses);
|
|
78
78
|
const matrices = uses.length > 0 ? uses.map((use) => use.matrix) : [shadingDeviceMatrix(shading, pattern)];
|
|
79
79
|
const size = shadingSampleGridForUses(domain, matrices);
|
|
80
|
-
replaceType1Function(state.document, shading, pattern,
|
|
80
|
+
replaceType1Function(state.document, shading, pattern, state.sampleConvert, state.budget, void 0, domain, size);
|
|
81
81
|
}
|
|
82
82
|
function convertShading(state, shading, pattern) {
|
|
83
83
|
if (state.seenShadings.has(shading)) return false;
|
|
@@ -195,31 +195,41 @@ function convertContent(state, object, stream, setDirect) {
|
|
|
195
195
|
if (reference) state.document.context.assign(reference, replacement);
|
|
196
196
|
else setDirect(replacement);
|
|
197
197
|
}
|
|
198
|
-
function newState(document, profile) {
|
|
198
|
+
function newState(document, profile, tolerance) {
|
|
199
|
+
const cached = cachedPressColor(profile, tolerance);
|
|
200
|
+
const totals = {
|
|
201
|
+
fills: 0,
|
|
202
|
+
strokes: 0,
|
|
203
|
+
gradientStops: 0,
|
|
204
|
+
groups: 0,
|
|
205
|
+
neutralColors: 0
|
|
206
|
+
};
|
|
199
207
|
return {
|
|
200
208
|
budget: {
|
|
201
209
|
calculatorOperations: 0,
|
|
202
210
|
decodedBytes: 0,
|
|
203
211
|
shadingSamples: 0
|
|
204
212
|
},
|
|
205
|
-
convert:
|
|
213
|
+
convert: (rgb) => {
|
|
214
|
+
if (newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
|
|
215
|
+
return cached(rgb);
|
|
216
|
+
},
|
|
206
217
|
document,
|
|
207
218
|
painted: /* @__PURE__ */ new WeakMap(),
|
|
208
219
|
profile,
|
|
220
|
+
sampleConvert: (rgb) => {
|
|
221
|
+
if (newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
|
|
222
|
+
return pressColor(profile, rgb, tolerance);
|
|
223
|
+
},
|
|
209
224
|
seenGroups: /* @__PURE__ */ new WeakSet(),
|
|
210
225
|
seenShadings: /* @__PURE__ */ new WeakSet(),
|
|
211
226
|
shadingPainted: /* @__PURE__ */ new WeakMap(),
|
|
212
227
|
shadingPatterns: /* @__PURE__ */ new WeakMap(),
|
|
213
|
-
totals
|
|
214
|
-
fills: 0,
|
|
215
|
-
strokes: 0,
|
|
216
|
-
gradientStops: 0,
|
|
217
|
-
groups: 0
|
|
218
|
-
}
|
|
228
|
+
totals
|
|
219
229
|
};
|
|
220
230
|
}
|
|
221
|
-
function rewritePressDocument(document, profile) {
|
|
222
|
-
const state = newState(document, profile);
|
|
231
|
+
function rewritePressDocument(document, profile, tolerance = 3) {
|
|
232
|
+
const state = newState(document, profile, tolerance);
|
|
223
233
|
const boundsBudget = { decodedBytes: 0 };
|
|
224
234
|
walkPressGraph(document, {
|
|
225
235
|
beginContentsSequence: () => {
|
|
@@ -254,17 +264,19 @@ function addOutputIntent(document, profile) {
|
|
|
254
264
|
}));
|
|
255
265
|
document.catalog.set(key("OutputIntents"), document.context.obj([intentRef]));
|
|
256
266
|
}
|
|
257
|
-
async function rewritePressPdf(bytes, profileBytes) {
|
|
267
|
+
async function rewritePressPdf(bytes, profileBytes, tolerance = 3) {
|
|
258
268
|
try {
|
|
259
269
|
const profile = readProfile(profileBytes);
|
|
260
270
|
if (profile.colorSpace !== "CMYK") refuse(`press profile color space ${profile.colorSpace}`, "CMYK");
|
|
261
271
|
const document = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
262
|
-
rewritePressDocument(document, profile);
|
|
272
|
+
const counts = rewritePressDocument(document, profile, tolerance);
|
|
263
273
|
addOutputIntent(document, profileBytes);
|
|
264
274
|
const output = await document.save({ useObjectStreams: false });
|
|
275
|
+
const report = await inspectPressPdf(output);
|
|
276
|
+
report.neutralColors = counts.neutralColors;
|
|
265
277
|
return {
|
|
266
278
|
bytes: output,
|
|
267
|
-
report
|
|
279
|
+
report
|
|
268
280
|
};
|
|
269
281
|
} catch (error) {
|
|
270
282
|
if (error instanceof RenderRefusedError) throw error;
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
import { RenderRefusedError } from "../errors.js";
|
|
1
2
|
import { toPress } from "../color/engine.js";
|
|
2
3
|
//#region src/press/color-policy.ts
|
|
3
4
|
const NEUTRAL_TOLERANCE = .001;
|
|
4
|
-
function
|
|
5
|
+
function assertNeutralTolerance(value) {
|
|
6
|
+
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
|
+
}
|
|
8
|
+
function snapsToNeutral(rgb, tolerance) {
|
|
9
|
+
return Math.max(...rgb) - Math.min(...rgb) <= Math.max(NEUTRAL_TOLERANCE, tolerance / 255) + Number.EPSILON;
|
|
10
|
+
}
|
|
11
|
+
function newlySnapsToNeutral(rgb, tolerance) {
|
|
12
|
+
return snapsToNeutral(rgb, tolerance) && !snapsToNeutral(rgb, 0);
|
|
13
|
+
}
|
|
14
|
+
function pressColor(profile, rgb, tolerance = 3) {
|
|
5
15
|
const [red, green, blue] = rgb;
|
|
6
|
-
if (
|
|
16
|
+
if (snapsToNeutral(rgb, tolerance)) return [
|
|
7
17
|
0,
|
|
8
18
|
0,
|
|
9
19
|
0,
|
|
10
|
-
1 - red
|
|
20
|
+
1 - (red === green && green === blue ? red : (red + green + blue) / 3)
|
|
11
21
|
];
|
|
12
22
|
return toPress(profile, rgb);
|
|
13
23
|
}
|
|
@@ -16,16 +26,16 @@ function formatPdfNumber(value) {
|
|
|
16
26
|
if (Object.is(rounded, -0)) return "0";
|
|
17
27
|
return rounded.toFixed(6).replace(/(?:\.0+|(?<=\.[0-9]*?)0+)$/, "");
|
|
18
28
|
}
|
|
19
|
-
function cachedPressColor(profile) {
|
|
29
|
+
function cachedPressColor(profile, tolerance = 3) {
|
|
20
30
|
const cache = /* @__PURE__ */ new Map();
|
|
21
31
|
return (rgb) => {
|
|
22
|
-
const key = rgb.join(",")
|
|
32
|
+
const key = `${tolerance}:${rgb.join(",")}`;
|
|
23
33
|
const cached = cache.get(key);
|
|
24
34
|
if (cached) return cached;
|
|
25
|
-
const converted = pressColor(profile, rgb);
|
|
35
|
+
const converted = pressColor(profile, rgb, tolerance);
|
|
26
36
|
cache.set(key, converted);
|
|
27
37
|
return converted;
|
|
28
38
|
};
|
|
29
39
|
}
|
|
30
40
|
//#endregion
|
|
31
|
-
export { cachedPressColor, formatPdfNumber, pressColor };
|
|
41
|
+
export { assertNeutralTolerance, cachedPressColor, formatPdfNumber, newlySnapsToNeutral, pressColor, snapsToNeutral };
|
package/dist/press/report.js
CHANGED
package/dist/press/types.d.ts
CHANGED
|
@@ -14,5 +14,7 @@ export interface PressReport {
|
|
|
14
14
|
/** Function-based shadings rewritten as DeviceCMYK sampled functions. */
|
|
15
15
|
sampledShadings: number;
|
|
16
16
|
groups: number;
|
|
17
|
+
/** Color conversions snapped to black ink by the configured tolerance but not by the exact-neutral rule. */
|
|
18
|
+
neutralColors: number;
|
|
17
19
|
}
|
|
18
20
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@segnavia/format": "0.
|
|
30
|
-
"@segnavia/frame": "0.
|
|
29
|
+
"@segnavia/format": "0.5.0",
|
|
30
|
+
"@segnavia/frame": "0.5.0",
|
|
31
|
+
"@segnavia/html": "0.5.0",
|
|
31
32
|
"playwright-core": "1.55.1",
|
|
32
33
|
"pdf-lib": "^1.17.1"
|
|
33
34
|
}
|