@segnavia/render 0.4.0 → 0.6.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.
@@ -1,7 +1,7 @@
1
1
  import { REVERSE_LUT, checked, clamp, refuse } from "./types.js";
2
2
  import { invert3, matrix3 } from "./spaces.js";
3
3
  import { align4, fixed, need, signature, u16, u32 } from "./binary.js";
4
- import { interpolate, matrixInput, multilinear, tetrahedral4 } from "./interpolation.js";
4
+ import { interpolate, linearGrid, matrixInput, multilinear, tetrahedral4 } from "./interpolation.js";
5
5
  //#region src/color/tables.ts
6
6
  function packed(bytes, view, offset, length, width, bounds) {
7
7
  need(offset, length * width, bounds);
@@ -73,6 +73,16 @@ function parseCurve(bytes, view, offset, bounds) {
73
73
  size: 12 + count * 4
74
74
  };
75
75
  }
76
+ function invertCurve(curve, target) {
77
+ let low = 0, high = 1;
78
+ const descending = curve(0) > curve(1);
79
+ for (let index = 0; index < 24; index++) {
80
+ const middle = (low + high) / 2;
81
+ if (descending ? curve(middle) > target : curve(middle) < target) low = middle;
82
+ else high = middle;
83
+ }
84
+ return (low + high) / 2;
85
+ }
76
86
  function curvesAt(bytes, view, offset, count, bounds) {
77
87
  const curves = [];
78
88
  let cursor = offset;
@@ -132,7 +142,7 @@ function parseMft(bytes, view, record, type, inputs, outputs) {
132
142
  ];
133
143
  current = matrix3(fixedMatrix, matrixInput(current));
134
144
  }
135
- current = inputs === 3 ? multilinear(grid, points, outputs, current) : tetrahedral4(grid, points, outputs, current);
145
+ current = inputs === 1 ? linearGrid(grid, points, outputs, current) : inputs === 3 ? multilinear(grid, points, outputs, current) : tetrahedral4(grid, points, outputs, current);
136
146
  return current.map((value, channel) => interpolate(checked(outputTables, channel, `${type} output tables`), value));
137
147
  }
138
148
  };
@@ -179,7 +189,7 @@ function parseMab(bytes, view, record, type, inputs, outputs) {
179
189
  2
180
190
  ].map((row) => checked(matrix, row * 3, `${type} matrix`) * x + checked(matrix, row * 3 + 1, `${type} matrix`) * y + checked(matrix, row * 3 + 2, `${type} matrix`) * z + checked(matrix, 9 + row, `${type} matrix`));
181
191
  };
182
- const clut = (values) => !grid ? [...values] : inputs === 3 ? multilinear(grid, points, outputs, values) : tetrahedral4(grid, points, outputs, values);
192
+ const clut = (values) => !grid ? [...values] : inputs === 1 ? linearGrid(grid, points, outputs, values) : inputs === 3 ? multilinear(grid, points, outputs, values) : tetrahedral4(grid, points, outputs, values);
183
193
  return {
184
194
  inputChannels: inputs,
185
195
  outputChannels: outputs,
@@ -227,15 +237,6 @@ function matrixTables(bytes, view, records) {
227
237
  checked(checked(columns, 2, "colorants"), 2, "blue")
228
238
  ];
229
239
  const inverse = invert3(matrix);
230
- const invertCurve = (curve, target) => {
231
- let low = 0, high = 1;
232
- for (let index = 0; index < 24; index++) {
233
- const middle = (low + high) / 2;
234
- if (curve(middle) < target) low = middle;
235
- else high = middle;
236
- }
237
- return (low + high) / 2;
238
- };
239
240
  return {
240
241
  a2b: {
241
242
  inputChannels: 3,
@@ -251,5 +252,38 @@ function matrixTables(bytes, view, records) {
251
252
  }
252
253
  };
253
254
  }
255
+ function grayTables(bytes, view, record, pcs) {
256
+ const curve = parseCurve(bytes, view, record.offset, record).curve;
257
+ const scale = 32768 / 65535;
258
+ return {
259
+ a2b: {
260
+ inputChannels: 1,
261
+ outputChannels: 3,
262
+ encoding: "mAB ",
263
+ run(input) {
264
+ const luminance = curve(checked(input, 0, "gray input"));
265
+ if (pcs === "XYZ ") return [
266
+ luminance * .9642 * scale,
267
+ luminance * scale,
268
+ luminance * .8249 * scale
269
+ ];
270
+ return [
271
+ luminance,
272
+ 128 / 255,
273
+ 128 / 255
274
+ ];
275
+ }
276
+ },
277
+ b2a: {
278
+ inputChannels: 3,
279
+ outputChannels: 1,
280
+ encoding: REVERSE_LUT,
281
+ run(input) {
282
+ const luminance = pcs === "XYZ " ? checked(input, 1, "gray PCS input") / scale : checked(input, 0, "gray PCS input");
283
+ return [invertCurve(curve, luminance)];
284
+ }
285
+ }
286
+ };
287
+ }
254
288
  //#endregion
255
- export { matrixTables, parseCurve, parseMab, parseMft };
289
+ export { grayTables, invertCurve, matrixTables, parseCurve, parseMab, parseMft };
@@ -8,6 +8,9 @@ function checked(values, index, structure) {
8
8
  if (value === void 0) throw new Error(`${structure} index ${index} is outside length ${values.length}`);
9
9
  return value;
10
10
  }
11
+ function color1(values, structure) {
12
+ return [checked(values, 0, structure)];
13
+ }
11
14
  function color4(values, structure) {
12
15
  return [
13
16
  checked(values, 0, structure),
@@ -18,4 +21,4 @@ function color4(values, structure) {
18
21
  }
19
22
  const clamp = (value) => Math.max(0, Math.min(1, value));
20
23
  //#endregion
21
- export { REVERSE_LUT, checked, clamp, color4, refuse };
24
+ export { REVERSE_LUT, checked, clamp, color1, color4, refuse };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { EffectsReport } from "./effects/types.js";
2
- import { PressImage, PressReport } from "./press/types.js";
2
+ import { PressDestination, 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 PressDestination, type PressImage, type PressReport, type RenderLimits, type RenderOptions, RenderRefusedError, RendererUnavailableError };
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { RenderRefusedError, RendererUnavailableError } from "./errors.js";
2
+ import { renderBrowserBookPdf } from "./book-render.js";
3
+ import { browserJpegDecoder } from "./browser-jpeg.js";
2
4
  import { containFixedPageOverflow } from "./overflow-containment.js";
3
5
  import { renderDeckPdf } from "./deck-pdf.js";
6
+ import { assertNeutralTolerance } from "./press/color-policy.js";
4
7
  import { rewriteEffectsPdf } from "./effects/adapter.js";
5
8
  import { assertPrintOptions, printCss, printPage, printViewport } from "./page-paper.js";
6
9
  import { renderPagesPdf } from "./pages-pdf.js";
@@ -53,7 +56,7 @@ async function enforce(pdf, limits) {
53
56
  }
54
57
  return pdf;
55
58
  }
56
- async function finishPdf(pdf, options) {
59
+ async function finishPdf(pdf, options, decodeJpeg) {
57
60
  if (options.limits?.maxBytes !== void 0 && pdf.byteLength > options.limits.maxBytes) throw new RenderRefusedError(`The PDF is ${pdf.byteLength} bytes; the limit is ${options.limits.maxBytes}.`);
58
61
  let output = pdf;
59
62
  if (options.effects !== false) {
@@ -62,7 +65,9 @@ async function finishPdf(pdf, options) {
62
65
  options.effects?.report?.(rewritten.report);
63
66
  }
64
67
  if (options.press) {
65
- const converted = await rewritePressPdf(output, options.press.profile);
68
+ const neutralTolerance = options.press.neutralTolerance ?? 3;
69
+ assertNeutralTolerance(neutralTolerance);
70
+ const converted = await rewritePressPdf(output, options.press.profile, neutralTolerance, decodeJpeg);
66
71
  output = converted.bytes;
67
72
  options.press.report?.(converted.report);
68
73
  }
@@ -201,6 +206,18 @@ async function openRenderer(options) {
201
206
  reportHeight: false
202
207
  });
203
208
  }
209
+ async function finishWithBrowser(pdf, options) {
210
+ if (!options.press || new TextDecoder().decode(options.press.profile.subarray(16, 20)) !== "GRAY") return finishPdf(pdf, options);
211
+ return withTab({
212
+ viewport: {
213
+ width: 1,
214
+ height: 1
215
+ },
216
+ scripts: true,
217
+ html: "<!doctype html>",
218
+ waitForReady: false
219
+ }, (tab) => finishPdf(pdf, options, browserJpegDecoder(tab)));
220
+ }
204
221
  function pageViewport(page, width, printing = false) {
205
222
  if (page.print) return printViewport(page.print, printing);
206
223
  if (page.size === "fluid") return {
@@ -267,8 +284,15 @@ async function openRenderer(options) {
267
284
  }));
268
285
  }
269
286
  return {
287
+ bookPdf(input, opts = {}) {
288
+ return renderBrowserBookPdf(input, opts, {
289
+ render: (setup, pdf) => withTab(setup, (tab) => tab.pdf(pdf)),
290
+ fontFaces: (options) => fontFaces(options.fonts),
291
+ finish: finishWithBrowser
292
+ });
293
+ },
270
294
  async pagePdf(input, opts = {}) {
271
- return finishPdf(await renderPagePdf(Page.parse(input), opts), opts);
295
+ return finishWithBrowser(await renderPagePdf(Page.parse(input), opts), opts);
272
296
  },
273
297
  async pageImage(input, opts = {}) {
274
298
  const page = Page.parse(input);
@@ -292,7 +316,7 @@ async function openRenderer(options) {
292
316
  pagePdf: renderPagePdf,
293
317
  piecePdf: renderPiecePdf,
294
318
  fontFaces: (options) => fontFaces(options.fonts),
295
- finish: finishPdf
319
+ finish: finishWithBrowser
296
320
  });
297
321
  },
298
322
  deckPdf(deck, opts = {}) {
@@ -302,7 +326,7 @@ async function openRenderer(options) {
302
326
  waitForReady: false
303
327
  }, (tab) => tab.pdf(pdf)),
304
328
  fontFaces: (options) => fontFaces(options.fonts),
305
- finish: finishPdf
329
+ finish: finishWithBrowser
306
330
  });
307
331
  },
308
332
  async slideImage(deck, index, opts = {}) {
@@ -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
@@ -1,5 +1,5 @@
1
1
  import { RenderRefusedError } from "../errors.js";
2
- import { cachedPressColor, pressColor } from "./color-policy.js";
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
- function convertFunction(state, fn, policy) {
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.convert(arrayNumbers(fn, name, 3)));
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, (rgb) => pressColor(state.profile, rgb), 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 policy = shadingAction(space.kind, type, functionType);
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.convert(arrayNumbers(shading, "Background", 3)));
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, maskSource) {
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 policy = groupAction(space.kind, space.label, maskSource);
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 converted = new Uint8Array(width * height * 4);
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 = state.convert([
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 < 4; channel++) converted[pixel * 4 + channel] = Math.round(Math.max(0, Math.min(1, color[channel] ?? 0)) * 255);
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 source = arrayNumbers(image.dict, "SegnaviaEffectColor", 3);
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 rewritten = rewriteContent(bytes, state.convert);
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));
@@ -195,37 +202,50 @@ function convertContent(state, object, stream, setDirect) {
195
202
  if (reference) state.document.context.assign(reference, replacement);
196
203
  else setDirect(replacement);
197
204
  }
198
- function newState(document, profile) {
205
+ function newState(document, profile, tolerance) {
206
+ const cached = cachedPressColor(profile, tolerance);
207
+ const totals = {
208
+ fills: 0,
209
+ strokes: 0,
210
+ gradientStops: 0,
211
+ groups: 0,
212
+ neutralColors: 0
213
+ };
199
214
  return {
200
215
  budget: {
201
216
  calculatorOperations: 0,
202
217
  decodedBytes: 0,
203
218
  shadingSamples: 0
204
219
  },
205
- convert: cachedPressColor(profile),
220
+ convert: (rgb) => {
221
+ if (profile.colorSpace === "CMYK" && newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
222
+ return cached(rgb);
223
+ },
224
+ destination: pressDestination(profile),
206
225
  document,
207
226
  painted: /* @__PURE__ */ new WeakMap(),
208
227
  profile,
228
+ sampleConvert: (rgb) => {
229
+ if (profile.colorSpace === "CMYK" && newlySnapsToNeutral(rgb, tolerance)) totals.neutralColors++;
230
+ return destinationColor(profile, rgb, tolerance);
231
+ },
209
232
  seenGroups: /* @__PURE__ */ new WeakSet(),
210
233
  seenShadings: /* @__PURE__ */ new WeakSet(),
211
234
  shadingPainted: /* @__PURE__ */ new WeakMap(),
212
235
  shadingPatterns: /* @__PURE__ */ new WeakMap(),
213
- totals: {
214
- fills: 0,
215
- strokes: 0,
216
- gradientStops: 0,
217
- groups: 0
218
- }
236
+ totals
219
237
  };
220
238
  }
221
- function rewritePressDocument(document, profile) {
222
- const state = newState(document, profile);
239
+ function rewritePressDocument(document, profile, tolerance = 3) {
240
+ const state = newState(document, profile, tolerance);
223
241
  const boundsBudget = { decodedBytes: 0 };
224
242
  walkPressGraph(document, {
225
243
  beginContentsSequence: () => {
226
244
  state.sequence = createPaintedBoundsTracker(state.painted, state.shadingPainted);
227
245
  },
228
- content: (_object, stream, _replace, resources) => recordContentBounds(state, stream, resources, boundsBudget),
246
+ content: (_object, stream, _replace, resources) => {
247
+ recordContentBounds(state, stream, resources, boundsBudget);
248
+ },
229
249
  endContentsSequence: () => {
230
250
  state.sequence = void 0;
231
251
  },
@@ -236,16 +256,16 @@ function rewritePressDocument(document, profile) {
236
256
  shading: (shading, pattern) => indexShadingPattern(state, shading, pattern)
237
257
  });
238
258
  walkPressGraph(document, {
239
- content: (object, stream, replace) => convertContent(state, object, stream, replace),
240
- function: (fn, decision) => convertFunction(state, fn, decision),
241
- group: (owner, _stream, maskSource) => convertGroup(state, owner, maskSource),
242
- image: (image) => convertEffectImage(state, image),
243
- 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)
244
264
  });
245
265
  return state.totals;
246
266
  }
247
- function addOutputIntent(document, profile) {
248
- const profileRef = document.context.register(document.context.stream(profile, { N: 4 }));
267
+ function addOutputIntent(document, profile, channels) {
268
+ const profileRef = document.context.register(document.context.stream(profile, { N: channels }));
249
269
  const intentRef = document.context.register(document.context.obj({
250
270
  Type: "OutputIntent",
251
271
  S: "GTS_PDFX",
@@ -254,17 +274,64 @@ function addOutputIntent(document, profile) {
254
274
  }));
255
275
  document.catalog.set(key("OutputIntents"), document.context.obj([intentRef]));
256
276
  }
257
- async function rewritePressPdf(bytes, profileBytes) {
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) {
258
319
  try {
259
320
  const profile = readProfile(profileBytes);
260
- 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");
261
322
  const document = await PDFDocument.load(bytes, { updateMetadata: false });
262
- rewritePressDocument(document, profile);
263
- addOutputIntent(document, profileBytes);
323
+ const counts = rewritePressDocument(document, profile, tolerance);
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);
264
328
  const output = await document.save({ useObjectStreams: false });
329
+ const report = await inspectPressPdf(output, destination);
330
+ report.neutralColors = counts.neutralColors;
331
+ report.convertedImages = convertedImages;
265
332
  return {
266
333
  bytes: output,
267
- report: await inspectPressPdf(output)
334
+ report
268
335
  };
269
336
  } catch (error) {
270
337
  if (error instanceof RenderRefusedError) throw error;