diff --git a/apps/deno/tests/test12.ts b/apps/deno/tests/test12.ts index 0ad1f319d..cba301d5f 100644 --- a/apps/deno/tests/test12.ts +++ b/apps/deno/tests/test12.ts @@ -274,11 +274,154 @@ const thirdPage = async (pdfDoc: PDFDocument, assets: Assets) => { }); }; +const fourthPage = async (pdfDoc: PDFDocument) => { + const page = pdfDoc.addPage(); + + const pdfSeparation = await pdfDoc.embedSeparation( + 'PANTONE 123 C', + cmyk(0, 0.22, 0.83, 0), + ); + + const color = page.getSeparationColor(pdfSeparation, 0.5); + const borderColor = page.getSeparationColor(pdfSeparation, 1); + + page.drawRectangle({ + x: inchToPt(0), + y: inchToPt(0), + width: inchToPt(8.5), + height: inchToPt(12), + color: cmyk(0, 0, 0, 1), + }); + + page.drawText('This text will be printed using a spot color', { + x: inchToPt(0.5), + y: inchToPt(11), + size: 16, + color, + }); + + // No overprint + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(12), + color, + borderColor, + borderWidth: 2, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(8), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(9), + color, + borderColor, + borderWidth: 2, + }); + + // Overprint + + page.drawText('This text will be printed over the existing background', { + x: inchToPt(0.5), + y: inchToPt(7), + color, + size: 16, + overprint: true, + }); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(8), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(4), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(5), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + // Overprint only for strokes + + page.drawText( + 'Only the filling will be overprinted on the following shapes', + { + x: inchToPt(0.5), + y: inchToPt(3), + color, + size: 16, + overprint: true, + }, + ); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(4), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(0), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(1), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); +}; + export default async (assets: Assets) => { const pdfDoc = await PDFDocument.create(); await firstPage(pdfDoc); await secondPage(pdfDoc); await thirdPage(pdfDoc, assets); + await fourthPage(pdfDoc); const pdfBytes = await pdfDoc.save(); return pdfBytes; diff --git a/apps/node/tests/test12.ts b/apps/node/tests/test12.ts index e04a2c15a..e0d15ada9 100644 --- a/apps/node/tests/test12.ts +++ b/apps/node/tests/test12.ts @@ -269,11 +269,154 @@ const thirdPage = async (pdfDoc: PDFDocument, assets: Assets) => { }); }; +const fourthPage = async (pdfDoc: PDFDocument) => { + const page = pdfDoc.addPage(); + + const pdfSeparation = await pdfDoc.embedSeparation( + 'PANTONE 123 C', + cmyk(0, 0.22, 0.83, 0), + ); + + const color = page.getSeparationColor(pdfSeparation, 0.5); + const borderColor = page.getSeparationColor(pdfSeparation, 1); + + page.drawRectangle({ + x: inchToPt(0), + y: inchToPt(0), + width: inchToPt(8.5), + height: inchToPt(12), + color: cmyk(0, 0, 0, 1), + }); + + page.drawText('This text will be printed using a spot color', { + x: inchToPt(0.5), + y: inchToPt(11), + size: 16, + color, + }); + + // No overprint + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(12), + color, + borderColor, + borderWidth: 2, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(8), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(9), + color, + borderColor, + borderWidth: 2, + }); + + // Overprint + + page.drawText('This text will be printed over the existing background', { + x: inchToPt(0.5), + y: inchToPt(7), + color, + size: 16, + overprint: true, + }); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(8), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(4), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(5), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + // Overprint only for strokes + + page.drawText( + 'Only the filling will be overprinted on the following shapes', + { + x: inchToPt(0.5), + y: inchToPt(3), + color, + size: 16, + overprint: true, + }, + ); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(4), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(0), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(1), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); +}; + export default async (assets: Assets) => { const pdfDoc = await PDFDocument.create(); await firstPage(pdfDoc); await secondPage(pdfDoc); await thirdPage(pdfDoc, assets); + await fourthPage(pdfDoc); const pdfBytes = await pdfDoc.save(); return pdfBytes; diff --git a/apps/rn/src/tests/test12.js b/apps/rn/src/tests/test12.js index 7e10d11bf..ff17de837 100644 --- a/apps/rn/src/tests/test12.js +++ b/apps/rn/src/tests/test12.js @@ -272,6 +272,148 @@ const thirdPage = async (pdfDoc, assets) => { }); }; +const fourthPage = async (pdfDoc) => { + const page = pdfDoc.addPage(); + + const pdfSeparation = await pdfDoc.embedSeparation( + 'PANTONE 123 C', + cmyk(0, 0.22, 0.83, 0), + ); + + const color = page.getSeparationColor(pdfSeparation, 0.5); + const borderColor = page.getSeparationColor(pdfSeparation, 1); + + page.drawRectangle({ + x: inchToPt(0), + y: inchToPt(0), + width: inchToPt(8.5), + height: inchToPt(12), + color: cmyk(0, 0, 0, 1), + }); + + page.drawText('This text will be printed using a spot color', { + x: inchToPt(0.5), + y: inchToPt(11), + size: 16, + color, + }); + + // No overprint + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(12), + color, + borderColor, + borderWidth: 2, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(8), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(9), + color, + borderColor, + borderWidth: 2, + }); + + // Overprint + + page.drawText('This text will be printed over the existing background', { + x: inchToPt(0.5), + y: inchToPt(7), + color, + size: 16, + overprint: true, + }); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(8), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(4), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(5), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + // Overprint only for strokes + + page.drawText( + 'Only the filling will be overprinted on the following shapes', + { + x: inchToPt(0.5), + y: inchToPt(3), + color, + size: 16, + overprint: true, + }, + ); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(4), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(0), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(1), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); +}; + export default async () => { const [selfDrivePngBytes, simplePdf2ExampleBytes] = await Promise.all([ fetchAsset('images/self_drive.png'), @@ -283,6 +425,7 @@ export default async () => { await firstPage(pdfDoc); await secondPage(pdfDoc); await thirdPage(pdfDoc, { selfDrivePngBytes, simplePdf2ExampleBytes }); + await fourthPage(pdfDoc); const base64Pdf = await pdfDoc.saveAsBase64({ dataUri: true }); diff --git a/apps/web/test12.html b/apps/web/test12.html index 9ebf11411..6b11db936 100644 --- a/apps/web/test12.html +++ b/apps/web/test12.html @@ -321,6 +321,147 @@ }); }; + const fourthPage = async (pdfDoc) => { + const page = pdfDoc.addPage(); + + const pdfSeparation = await pdfDoc.embedSeparation( + 'PANTONE 123 C', + cmyk(0, 0.22, 0.83, 0), + ); + + const color = page.getSeparationColor(pdfSeparation, 0.5); + const borderColor = page.getSeparationColor(pdfSeparation, 1); + page.drawRectangle({ + x: inchToPt(0), + y: inchToPt(0), + width: inchToPt(8.5), + height: inchToPt(12), + color: cmyk(0, 0, 0, 1), + }); + + page.drawText('This text will be printed using a spot color', { + x: inchToPt(0.5), + y: inchToPt(11), + size: 16, + color, + }); + + // No overprint + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(12), + color, + borderColor, + borderWidth: 2, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(8), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(9), + color, + borderColor, + borderWidth: 2, + }); + + // Overprint + + page.drawText('This text will be printed over the existing background', { + x: inchToPt(0.5), + y: inchToPt(7), + color, + size: 16, + overprint: true, + }); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(8), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(4), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(5), + color, + borderColor, + borderWidth: 2, + overprint: true, + }); + + // Overprint only for strokes + + page.drawText( + 'Only the filling will be overprinted on the following shapes', + { + x: inchToPt(0.5), + y: inchToPt(3), + color, + size: 16, + overprint: true, + }, + ); + + page.drawSvgPath('M 100 100 L 300 100 L 200 300 z', { + x: inchToPt(-1), + y: inchToPt(4), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawRectangle({ + x: inchToPt(2), + y: inchToPt(0), + width: 60, + height: 160, + rotate: degrees(-30), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + + page.drawCircle({ + x: inchToPt(5), + y: inchToPt(1), + color, + borderColor, + borderWidth: 2, + overprint: false, + nonStrokingOverprint: true, + }); + }; + async function test() { const [selfDrivePngBytes, simplePdf2ExampleBytes] = await Promise.all([ fetchBinaryAsset('images/self_drive.png'), @@ -332,6 +473,7 @@ await firstPage(pdfDoc); await secondPage(pdfDoc); await thirdPage(pdfDoc, { selfDrivePngBytes, simplePdf2ExampleBytes }); + await fourthPage(pdfDoc); const pdfBytes = await pdfDoc.save(); diff --git a/rollup.config.js b/rollup.config.js index 5964f6c0e..8fc32c4b0 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,6 +9,7 @@ const IgnoredWarnings = [ // Mac & Linux 'Circular dependency: es/api/PDFDocument.js -> es/api/PDFFont.js -> es/api/PDFDocument.js', 'Circular dependency: es/api/PDFDocument.js -> es/api/PDFImage.js -> es/api/PDFDocument.js', + 'Circular dependency: es/api/PDFDocument.js -> es/api/PDFSeparation.js -> es/api/PDFDocument.js', 'Circular dependency: es/api/PDFDocument.js -> es/api/PDFPage.js -> es/api/PDFDocument.js', 'Circular dependency: es/api/PDFPage.js -> es/api/PDFDocument.js -> es/api/PDFPage.js', 'Circular dependency: es/api/PDFDocument.js -> es/api/PDFEmbeddedPage.js -> es/api/PDFDocument.js', @@ -25,6 +26,7 @@ const IgnoredWarnings = [ // Windows 'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFFont.js -> es\\api\\PDFDocument.js', 'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFImage.js -> es\\api\\PDFDocument.js', + 'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFSeparation.js -> es\\api\\PDFDocument.js', 'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFPage.js -> es\\api\\PDFDocument.js', 'Circular dependency: es\\api\\PDFPage.js -> es\\api\\PDFDocument.js -> es\\api\\PDFPage.js', 'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFEmbeddedPage.js -> es\\api\\PDFDocument.js', diff --git a/scratchpad/index.ts b/scratchpad/index.ts index 370b95030..de03f18df 100644 --- a/scratchpad/index.ts +++ b/scratchpad/index.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import { openPdf, Reader } from './open'; -import { PDFDocument } from 'src/index'; +import { PDFDocument, cmyk } from 'src/index'; (async () => { const pdfDoc1 = await PDFDocument.create(); @@ -18,6 +18,12 @@ import { PDFDocument } from 'src/index'; ); const page2 = pdfDoc2.getPage(0); page2.drawImage(image2, { ...image2.scale(0.5), x: 100, y: 100 }); + page2.drawText('Hello World!', { + x: 100, + y: 200, + overprint: true, + color: cmyk(0, 0, 0, 1), + }); const pdfBytes = await pdfDoc2.save(); diff --git a/src/api/PDFDocument.ts b/src/api/PDFDocument.ts index e102368e2..d625d3701 100644 --- a/src/api/PDFDocument.ts +++ b/src/api/PDFDocument.ts @@ -10,6 +10,7 @@ import PDFFont from 'src/api/PDFFont'; import PDFImage from 'src/api/PDFImage'; import PDFPage from 'src/api/PDFPage'; import PDFForm from 'src/api/form/PDFForm'; +import PDFSeparation from 'src/api/PDFSeparation'; import { PageSizes } from 'src/api/sizes'; import { StandardFonts } from 'src/api/StandardFonts'; import { @@ -33,6 +34,7 @@ import { PDFWriter, PngEmbedder, StandardFontEmbedder, + SeparationEmbedder, UnexpectedObjectTypeError, } from 'src/core'; import { @@ -61,11 +63,13 @@ import { pluckIndices, range, toUint8Array, + error, } from 'src/utils'; import FileEmbedder, { AFRelationship } from 'src/core/embedders/FileEmbedder'; import PDFEmbeddedFile from 'src/api/PDFEmbeddedFile'; import PDFJavaScript from 'src/api/PDFJavaScript'; import JavaScriptEmbedder from 'src/core/embedders/JavaScriptEmbedder'; +import { Color, ColorTypes, colorToComponents } from './colors'; /** * Represents a PDF document. @@ -185,6 +189,7 @@ export default class PDFDocument { private readonly formCache: Cache; private readonly fonts: PDFFont[]; private readonly images: PDFImage[]; + private readonly separationColorSpaces: PDFSeparation[]; private readonly embeddedPages: PDFEmbeddedPage[]; private readonly embeddedFiles: PDFEmbeddedFile[]; private readonly javaScripts: PDFJavaScript[]; @@ -206,6 +211,7 @@ export default class PDFDocument { this.formCache = Cache.populatedBy(this.getOrCreateForm); this.fonts = []; this.images = []; + this.separationColorSpaces = []; this.embeddedPages = []; this.embeddedFiles = []; this.javaScripts = []; @@ -997,6 +1003,32 @@ export default class PDFDocument { return pdfFont; } + /** + * Embed a separation color space into this document. + * For example: + * ```js + * import { rgb } from 'pdf-lib' + * const separation = pdfDoc.embedSeparation('PANTONE 123 C', rgb(1, 0, 0)) + * ``` + * + * @param name The name of the separation color space. + * @param alternate An alternate color to be used to approximate the intended + * color. + */ + embedSeparation(name: string, alternate: Color): PDFSeparation { + const ref = this.context.nextRef(); + const alternateColorSpace = getColorSpace(alternate); + const alternateColorComponents = colorToComponents(alternate); + const embedder = SeparationEmbedder.for( + name, + alternateColorSpace, + alternateColorComponents, + ); + const separation = PDFSeparation.of(ref, this, embedder); + this.separationColorSpaces.push(separation); + return separation; + } + /** * Embed a JPEG image into this document. The input data can be provided in * multiple formats: @@ -1243,6 +1275,7 @@ export default class PDFDocument { async flush(): Promise { await this.embedAll(this.fonts); await this.embedAll(this.images); + await this.embedAll(this.separationColorSpaces); await this.embedAll(this.embeddedPages); await this.embedAll(this.embeddedFiles); await this.embedAll(this.javaScripts); @@ -1393,3 +1426,10 @@ function assertIsLiteralOrHexString( throw new UnexpectedObjectTypeError([PDFHexString, PDFString], pdfObject); } } + +// prettier-ignore +const getColorSpace = (color: Color) => + color.type === ColorTypes.Grayscale ? 'DeviceGray' + : color.type === ColorTypes.RGB ? 'DeviceRGB' + : color.type === ColorTypes.CMYK ? 'DeviceCMYK' + : error(`Invalid alternate color: ${JSON.stringify(color)}`); diff --git a/src/api/PDFPage.ts b/src/api/PDFPage.ts index 7333440bd..a4b60f7e0 100644 --- a/src/api/PDFPage.ts +++ b/src/api/PDFPage.ts @@ -1,4 +1,4 @@ -import { Color, rgb } from 'src/api/colors'; +import { Color, rgb, separation } from 'src/api/colors'; import { drawImage, drawLine, @@ -55,6 +55,7 @@ import { assertRangeOrUndefined, assertIsOneOfOrUndefined, } from 'src/utils'; +import PDFSeparation from './PDFSeparation'; /** * Represents a single page of a [[PDFDocument]]. @@ -763,6 +764,30 @@ export default class PDFPage { this.lineHeight = lineHeight; } + /** + * Creates a local Separation color for this page. The color can then be + * used to draw text or fill shapes. For example: + * ```js + * const pdfSeparation = await pdfDoc.embedSeparation( + * 'PANTONE 123 C', + * cmyk(0, 0.22, 0.83, 0), + * ); + * const color = page.getSeparationColor(pdfSeparation, 0.5); + * page.drawText('This text will be printed using a spot color', { color }); + * ``` + * + * @param pdfSeparation A PDFSeparation object that was embedded into the + * document. + * @param tint The tint (intensity) value to use for the color. + * + * @returns The name of the color space in the page's resources. + */ + getSeparationColor(pdfSeparation: PDFSeparation, tint: number): Color { + const name = pdfSeparation.name; + const ref = pdfSeparation.ref; + return separation(this.node.newColorSpace(name, ref), tint); + } + /** * Get the default position of this page. For example: * ```js @@ -966,6 +991,7 @@ export default class PDFPage { assertIs(text, 'text', ['string']); assertOrUndefined(options.color, 'options.color', [[Object, 'Color']]); assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1); + assertOrUndefined(options.overprint, 'options.overprint', ['boolean']); assertOrUndefined(options.font, 'options.font', [[PDFFont, 'PDFFont']]); assertOrUndefined(options.size, 'options.size', ['number']); assertOrUndefined(options.rotate, 'options.rotate', [[Object, 'Rotation']]); @@ -996,6 +1022,7 @@ export default class PDFPage { const graphicsStateKey = this.maybeEmbedGraphicsState({ opacity: options.opacity, blendMode: options.blendMode, + overprint: options.overprint, }); const contentStream = this.getContentStream(); @@ -1063,6 +1090,7 @@ export default class PDFPage { const graphicsStateKey = this.maybeEmbedGraphicsState({ opacity: options.opacity, blendMode: options.blendMode, + overprint: options.overprint, }); const contentStream = this.getContentStream(); @@ -1140,6 +1168,7 @@ export default class PDFPage { const graphicsStateKey = this.maybeEmbedGraphicsState({ opacity: options.opacity, blendMode: options.blendMode, + overprint: options.overprint, }); // prettier-ignore @@ -1243,6 +1272,8 @@ export default class PDFPage { opacity: options.opacity, borderOpacity: options.borderOpacity, blendMode: options.blendMode, + overprint: options.overprint, + nonStrokingOverprint: options.nonStrokingOverprint, }); if (!('color' in options) && !('borderColor' in options)) { @@ -1304,6 +1335,7 @@ export default class PDFPage { const graphicsStateKey = this.maybeEmbedGraphicsState({ borderOpacity: options.opacity, blendMode: options.blendMode, + overprint: options.overprint, }); if (!('color' in options)) { @@ -1382,6 +1414,8 @@ export default class PDFPage { opacity: options.opacity, borderOpacity: options.borderOpacity, blendMode: options.blendMode, + overprint: options.overprint, + nonStrokingOverprint: options.nonStrokingOverprint, }); if (!('color' in options) && !('borderColor' in options)) { @@ -1487,6 +1521,8 @@ export default class PDFPage { opacity: options.opacity, borderOpacity: options.borderOpacity, blendMode: options.blendMode, + overprint: options.overprint, + nonStrokingOverprint: options.nonStrokingOverprint, }); if (!('color' in options) && !('borderColor' in options)) { @@ -1580,13 +1616,23 @@ export default class PDFPage { opacity?: number; borderOpacity?: number; blendMode?: BlendMode; + overprint?: boolean; + nonStrokingOverprint?: boolean; }): PDFName | undefined { - const { opacity, borderOpacity, blendMode } = options; + const { + opacity, + borderOpacity, + blendMode, + overprint, + nonStrokingOverprint, + } = options; if ( opacity === undefined && borderOpacity === undefined && - blendMode === undefined + blendMode === undefined && + overprint === undefined && + nonStrokingOverprint === undefined ) { return undefined; } @@ -1596,6 +1642,8 @@ export default class PDFPage { ca: opacity, CA: borderOpacity, BM: blendMode, + OP: overprint, + op: nonStrokingOverprint, }); const key = this.node.newExtGState('GS', graphicsState); diff --git a/src/api/PDFPageOptions.ts b/src/api/PDFPageOptions.ts index 6ddd8357f..9cf8e14d1 100644 --- a/src/api/PDFPageOptions.ts +++ b/src/api/PDFPageOptions.ts @@ -22,6 +22,7 @@ export interface PDFPageDrawTextOptions { color?: Color; opacity?: number; blendMode?: BlendMode; + overprint?: boolean; font?: PDFFont; size?: number; rotate?: Rotation; @@ -44,6 +45,7 @@ export interface PDFPageDrawImageOptions { ySkew?: Rotation; opacity?: number; blendMode?: BlendMode; + overprint?: boolean; } export interface PDFPageDrawPageOptions { @@ -58,6 +60,7 @@ export interface PDFPageDrawPageOptions { ySkew?: Rotation; opacity?: number; blendMode?: BlendMode; + overprint?: boolean; } export interface PDFPageDrawSVGOptions { @@ -74,6 +77,8 @@ export interface PDFPageDrawSVGOptions { borderDashPhase?: number; borderLineCap?: LineCapStyle; blendMode?: BlendMode; + overprint?: boolean; + nonStrokingOverprint?: boolean; } export interface PDFPageDrawLineOptions { @@ -86,6 +91,7 @@ export interface PDFPageDrawLineOptions { dashArray?: number[]; dashPhase?: number; blendMode?: BlendMode; + overprint?: boolean; } export interface PDFPageDrawRectangleOptions { @@ -105,6 +111,8 @@ export interface PDFPageDrawRectangleOptions { borderDashPhase?: number; borderLineCap?: LineCapStyle; blendMode?: BlendMode; + overprint?: boolean; + nonStrokingOverprint?: boolean; } export interface PDFPageDrawSquareOptions { @@ -123,6 +131,8 @@ export interface PDFPageDrawSquareOptions { borderDashPhase?: number; borderLineCap?: LineCapStyle; blendMode?: BlendMode; + overprint?: boolean; + nonStrokingOverprint?: boolean; } export interface PDFPageDrawEllipseOptions { @@ -140,6 +150,8 @@ export interface PDFPageDrawEllipseOptions { borderDashPhase?: number; borderLineCap?: LineCapStyle; blendMode?: BlendMode; + overprint?: boolean; + nonStrokingOverprint?: boolean; } export interface PDFPageDrawCircleOptions { @@ -155,4 +167,6 @@ export interface PDFPageDrawCircleOptions { borderDashPhase?: number; borderLineCap?: LineCapStyle; blendMode?: BlendMode; + overprint?: boolean; + nonStrokingOverprint?: boolean; } diff --git a/src/api/PDFSeparation.ts b/src/api/PDFSeparation.ts new file mode 100644 index 000000000..21f7229fe --- /dev/null +++ b/src/api/PDFSeparation.ts @@ -0,0 +1,73 @@ +import Embeddable from 'src/api/Embeddable'; +import PDFDocument from 'src/api/PDFDocument'; +import { PDFRef } from 'src/core'; +import SeparationEmbedder from 'src/core/embedders/SeparationEmbedder'; +import { assertIs } from 'src/utils'; + +/** + * Represents a file that has been embedded in a [[PDFDocument]]. + */ +export default class PDFSeparation implements Embeddable { + /** + * > **NOTE:** You probably don't want to call this method directly. Instead, + * > consider using the [[PDFDocument.embedSeparation]] method which will + * > create instances of [[PDFSeparation]] for you. + * + * Create an instance of [[PDFSeparation]] from an existing ref and embedder + * + * @param ref The unique reference for this file. + * @param doc The document to which the file will belong. + * @param embedder The embedder that will be used to embed the file. + */ + static of = (ref: PDFRef, doc: PDFDocument, embedder: SeparationEmbedder) => + new PDFSeparation(ref, doc, embedder); + + /** The unique reference assigned to this separation within the document. */ + readonly ref: PDFRef; + + /** The document to which this separation belongs. */ + readonly doc: PDFDocument; + + /** The name of this separation. */ + readonly name: string; + + private alreadyEmbedded = false; + private readonly embedder: SeparationEmbedder; + + private constructor( + ref: PDFRef, + doc: PDFDocument, + embedder: SeparationEmbedder, + ) { + assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]); + assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]); + assertIs(embedder, 'embedder', [ + [SeparationEmbedder, 'SeparationEmbedder'], + ]); + this.ref = ref; + this.doc = doc; + this.name = embedder.separationName; + + this.embedder = embedder; + } + + /** + * > **NOTE:** You probably don't need to call this method directly. The + * > [[PDFDocument.save]] and [[PDFDocument.saveAsBase64]] methods will + * > automatically ensure all separations get embedded. + * + * Embed this separation in its document. + * + * @returns Resolves when the embedding is complete. + */ + async embed(): Promise { + if (!this.embedder) return; + + // The separation should only be embedded once. If there's a pending embed + // operation then wait on it. Otherwise we need to start the embed. + if (this.alreadyEmbedded) return; + this.alreadyEmbedded = true; + + await this.embedder.embedIntoContext(this.doc.context, this.ref); + } +} diff --git a/src/api/colors.ts b/src/api/colors.ts index 6f2988c82..f5d00792c 100644 --- a/src/api/colors.ts +++ b/src/api/colors.ts @@ -2,16 +2,22 @@ import { setFillingCmykColor, setFillingGrayscaleColor, setFillingRgbColor, + setFillingSpecialColor, setStrokingCmykColor, setStrokingGrayscaleColor, setStrokingRgbColor, + setStrokingSpecialColor, + setFillingColorspace, + setStrokingColorspace, } from 'src/api/operators'; import { assertRange, error } from 'src/utils'; +import { PDFName } from 'src/core'; export enum ColorTypes { Grayscale = 'Grayscale', RGB = 'RGB', CMYK = 'CMYK', + Separation = 'Separation', } export interface Grayscale { @@ -34,7 +40,13 @@ export interface CMYK { key: number; } -export type Color = Grayscale | RGB | CMYK; +export interface Separation { + type: ColorTypes.Separation; + name: PDFName; + tint: number; +} + +export type Color = Grayscale | RGB | CMYK | Separation; export const grayscale = (gray: number): Grayscale => { assertRange(gray, 'gray', 0.0, 1.0); @@ -61,20 +73,33 @@ export const cmyk = ( return { type: ColorTypes.CMYK, cyan, magenta, yellow, key }; }; -const { Grayscale, RGB, CMYK } = ColorTypes; +export const separation = (name: PDFName, tint: number): Separation => { + assertRange(tint, 'tint', 0, 1); + return { type: ColorTypes.Separation, name, tint }; +}; + +const { Grayscale, RGB, CMYK, Separation } = ColorTypes; + +export const setFillingColorspaceOrUndefined = (color: Color) => + color.type === Separation ? setFillingColorspace(color.name) : undefined; // prettier-ignore export const setFillingColor = (color: Color) => - color.type === Grayscale ? setFillingGrayscaleColor(color.gray) - : color.type === RGB ? setFillingRgbColor(color.red, color.green, color.blue) - : color.type === CMYK ? setFillingCmykColor(color.cyan, color.magenta, color.yellow, color.key) + color.type === Grayscale ? setFillingGrayscaleColor(color.gray) + : color.type === RGB ? setFillingRgbColor(color.red, color.green, color.blue) + : color.type === CMYK ? setFillingCmykColor(color.cyan, color.magenta, color.yellow, color.key) + : color.type === Separation ? setFillingSpecialColor(color.tint) : error(`Invalid color: ${JSON.stringify(color)}`); +export const setStrokingColorspaceOrUndefined = (color: Color) => + color.type === Separation ? setStrokingColorspace(color.name) : undefined; + // prettier-ignore export const setStrokingColor = (color: Color) => - color.type === Grayscale ? setStrokingGrayscaleColor(color.gray) - : color.type === RGB ? setStrokingRgbColor(color.red, color.green, color.blue) - : color.type === CMYK ? setStrokingCmykColor(color.cyan, color.magenta, color.yellow, color.key) + color.type === Grayscale ? setStrokingGrayscaleColor(color.gray) + : color.type === RGB ? setStrokingRgbColor(color.red, color.green, color.blue) + : color.type === CMYK ? setStrokingCmykColor(color.cyan, color.magenta, color.yellow, color.key) + : color.type === Separation ? setStrokingSpecialColor(color.tint) : error(`Invalid color: ${JSON.stringify(color)}`); // prettier-ignore diff --git a/src/api/form/appearances.ts b/src/api/form/appearances.ts index 4047a9142..37a7dcba1 100644 --- a/src/api/form/appearances.ts +++ b/src/api/form/appearances.ts @@ -23,6 +23,7 @@ import { grayscale, cmyk, Color, + setFillingColorspaceOrUndefined, } from 'src/api/colors'; import { reduceRotation, adjustDimsForRotation } from 'src/api/rotations'; import { @@ -157,9 +158,12 @@ const updateDefaultAppearance = ( fontSize: number = 0, ) => { const da = [ + setFillingColorspaceOrUndefined(color)?.toString(), setFillingColor(color).toString(), setFontAndSize(font?.name ?? 'dummy__noop', fontSize).toString(), - ].join('\n'); + ] + .filter(Boolean) + .join('\n'); field.setDefaultAppearance(da); }; diff --git a/src/api/index.ts b/src/api/index.ts index 677cd389d..e05cec977 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -14,6 +14,7 @@ export * from 'src/api/StandardFonts'; export { default as PDFDocument } from 'src/api/PDFDocument'; export { default as PDFFont } from 'src/api/PDFFont'; export { default as PDFImage } from 'src/api/PDFImage'; +export { default as PDFSeparation } from 'src/api/PDFSeparation'; export { default as PDFPage } from 'src/api/PDFPage'; export { default as PDFEmbeddedPage } from 'src/api/PDFEmbeddedPage'; export { default as PDFJavaScript } from 'src/api/PDFJavaScript'; diff --git a/src/api/operations.ts b/src/api/operations.ts index 0e46e6fdf..abeba9e2e 100644 --- a/src/api/operations.ts +++ b/src/api/operations.ts @@ -1,4 +1,10 @@ -import { Color, setFillingColor, setStrokingColor } from 'src/api/colors'; +import { + Color, + setFillingColor, + setStrokingColor, + setFillingColorspaceOrUndefined, + setStrokingColorspaceOrUndefined, +} from 'src/api/colors'; import { beginText, closePath, @@ -57,6 +63,7 @@ export const drawText = ( pushGraphicsState(), options.graphicsState && setGraphicsState(options.graphicsState), beginText(), + setFillingColorspaceOrUndefined(options.color), setFillingColor(options.color), setFontAndSize(options.font, options.size), rotateAndSkewTextRadiansAndTranslate( @@ -83,6 +90,7 @@ export const drawLinesOfText = ( pushGraphicsState(), options.graphicsState && setGraphicsState(options.graphicsState), beginText(), + setFillingColorspaceOrUndefined(options.color), setFillingColor(options.color), setFontAndSize(options.font, options.size), setLineHeight(options.lineHeight), @@ -164,6 +172,7 @@ export const drawLine = (options: { [ pushGraphicsState(), options.graphicsState && setGraphicsState(options.graphicsState), + options.color && setStrokingColorspaceOrUndefined(options.color), options.color && setStrokingColor(options.color), setLineWidth(options.thickness), setDashPattern(options.dashArray ?? [], options.dashPhase ?? 0), @@ -194,7 +203,10 @@ export const drawRectangle = (options: { [ pushGraphicsState(), options.graphicsState && setGraphicsState(options.graphicsState), + options.color && setFillingColorspaceOrUndefined(options.color), options.color && setFillingColor(options.color), + options.borderColor && + setStrokingColorspaceOrUndefined(options.borderColor), options.borderColor && setStrokingColor(options.borderColor), setLineWidth(options.borderWidth), options.borderLineCap && setLineCap(options.borderLineCap), @@ -302,7 +314,10 @@ export const drawEllipse = (options: { [ pushGraphicsState(), options.graphicsState && setGraphicsState(options.graphicsState), + options.color && setFillingColorspaceOrUndefined(options.color), options.color && setFillingColor(options.color), + options.borderColor && + setStrokingColorspaceOrUndefined(options.borderColor), options.borderColor && setStrokingColor(options.borderColor), setLineWidth(options.borderWidth), options.borderLineCap && setLineCap(options.borderLineCap), @@ -360,7 +375,10 @@ export const drawSvgPath = ( // SVG path Y axis is opposite pdf-lib's options.scale ? scale(options.scale, -options.scale) : scale(1, -1), + options.color && setFillingColorspaceOrUndefined(options.color), options.color && setFillingColor(options.color), + options.borderColor && + setStrokingColorspaceOrUndefined(options.borderColor), options.borderColor && setStrokingColor(options.borderColor), options.borderWidth && setLineWidth(options.borderWidth), options.borderLineCap && setLineCap(options.borderLineCap), @@ -423,6 +441,7 @@ export const drawCheckMark = (options: { return [ pushGraphicsState(), + options.color && setStrokingColorspaceOrUndefined(options.color), options.color && setStrokingColor(options.color), setLineWidth(options.thickness), @@ -602,9 +621,10 @@ export const drawTextLines = ( ): PDFOperator[] => { const operators = [ beginText(), + setFillingColorspaceOrUndefined(options.color), setFillingColor(options.color), setFontAndSize(options.font, options.size), - ]; + ].filter(Boolean) as PDFOperator[]; for (let idx = 0, len = lines.length; idx < len; idx++) { const { encoded, x, y } = lines[idx]; diff --git a/src/api/operators.ts b/src/api/operators.ts index 62b5e3a24..f456518bf 100644 --- a/src/api/operators.ts +++ b/src/api/operators.ts @@ -352,6 +352,19 @@ export const setStrokingCmykColor = ( asPDFNumber(key), ]); +export const setFillingColorspace = (name: string | PDFName) => + PDFOperator.of(Ops.NonStrokingColorspace, [asPDFName(name)]); + +export const setFillingSpecialColor = (...components: (number | PDFNumber)[]) => + PDFOperator.of(Ops.NonStrokingColorN, [...components.map(asPDFNumber)]); + +export const setStrokingColorspace = (name: string | PDFName) => + PDFOperator.of(Ops.StrokingColorspace, [asPDFName(name)]); + +export const setStrokingSpecialColor = ( + ...components: (number | PDFNumber)[] +) => PDFOperator.of(Ops.StrokingColorN, [...components.map(asPDFNumber)]); + /* ==================== Marked Content Operators ==================== */ export const beginMarkedContent = (tag: string | PDFName) => diff --git a/src/core/embedders/SeparationEmbedder.ts b/src/core/embedders/SeparationEmbedder.ts new file mode 100644 index 000000000..00ee98b21 --- /dev/null +++ b/src/core/embedders/SeparationEmbedder.ts @@ -0,0 +1,55 @@ +import PDFRef from 'src/core/objects/PDFRef'; +import PDFContext from 'src/core/PDFContext'; + +class SeparationEmbedder { + static for( + name: string, + alternateColorSpace: string, + alternateColorComponents: number[], + ) { + return new SeparationEmbedder( + name, + alternateColorSpace, + alternateColorComponents, + ); + } + + readonly separationName: string; + private readonly alternateColorSpace: string; + private readonly alternateColorComponents: number[]; + + private constructor( + separationName: string, + alternateColorSpace: string, + alternateColorComponents: number[], + ) { + this.separationName = separationName; + this.alternateColorSpace = alternateColorSpace; + this.alternateColorComponents = alternateColorComponents; + } + + embedIntoContext(context: PDFContext, ref?: PDFRef): PDFRef { + const components = this.alternateColorComponents; + const colorSpace = context.obj([ + 'Separation', + this.separationName, + this.alternateColorSpace, + { + FunctionType: 2, + Domain: [0, 1], + C0: components.map(() => 0), + C1: components, + N: 1, + }, + ]); + + if (ref) { + context.assign(ref, colorSpace); + return ref; + } else { + return context.register(colorSpace); + } + } +} + +export default SeparationEmbedder; diff --git a/src/core/index.ts b/src/core/index.ts index 8e2d38616..aa9511fe9 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -18,6 +18,7 @@ export { default as FileEmbedder, AFRelationship, } from 'src/core/embedders/FileEmbedder'; +export { default as SeparationEmbedder } from 'src/core/embedders/SeparationEmbedder'; export { default as JpegEmbedder } from 'src/core/embedders/JpegEmbedder'; export { default as PngEmbedder } from 'src/core/embedders/PngEmbedder'; export { diff --git a/src/core/objects/PDFName.ts b/src/core/objects/PDFName.ts index 86a04bc34..3f09c7efa 100644 --- a/src/core/objects/PDFName.ts +++ b/src/core/objects/PDFName.ts @@ -40,6 +40,7 @@ class PDFName extends PDFObject { static readonly Font = PDFName.of('Font'); static readonly XObject = PDFName.of('XObject'); static readonly ExtGState = PDFName.of('ExtGState'); + static readonly ColorSpace = PDFName.of('ColorSpace'); static readonly Contents = PDFName.of('Contents'); static readonly Type = PDFName.of('Type'); static readonly Parent = PDFName.of('Parent'); diff --git a/src/core/structures/PDFPageLeaf.ts b/src/core/structures/PDFPageLeaf.ts index 0015c5965..dc1a2b340 100644 --- a/src/core/structures/PDFPageLeaf.ts +++ b/src/core/structures/PDFPageLeaf.ts @@ -193,6 +193,26 @@ class PDFPageLeaf extends PDFDict { return key; } + setColorSpace(name: PDFName, colorSpaceRef: PDFRef): void { + const { ColorSpace } = this.normalizedEntries(); + ColorSpace.set(name, colorSpaceRef); + } + + newColorSpaceKey(tag: string): PDFName { + const { ColorSpace } = this.normalizedEntries(); + return ColorSpace.uniqueKey(tag); + } + + newColorSpace(tag: string, colorSpaceRef: PDFRef): PDFName { + const { ColorSpace } = this.normalizedEntries(); + for (const [key, value] of ColorSpace.entries()) { + if (value === colorSpaceRef) return key; + } + const newKey = this.newColorSpaceKey(tag); + this.setColorSpace(newKey, colorSpaceRef); + return newKey; + } + ascend(visitor: (node: PDFPageTree | PDFPageLeaf) => any): void { visitor(this); const Parent = this.Parent(); @@ -238,6 +258,11 @@ class PDFPageLeaf extends PDFDict { Resources.lookupMaybe(PDFName.ExtGState, PDFDict) || context.obj({}); Resources.set(PDFName.ExtGState, ExtGState); + // TODO: Clone `ColorSpace` if it is inherited + const ColorSpace = + Resources.lookupMaybe(PDFName.ColorSpace, PDFDict) || context.obj({}); + Resources.set(PDFName.ColorSpace, ColorSpace); + const Annots = this.Annots() || context.obj([]); this.set(PDFName.Annots, Annots); @@ -256,6 +281,7 @@ class PDFPageLeaf extends PDFDict { Font: Resources.lookup(PDFName.Font, PDFDict), XObject: Resources.lookup(PDFName.XObject, PDFDict), ExtGState: Resources.lookup(PDFName.ExtGState, PDFDict), + ColorSpace: Resources.lookup(PDFName.ColorSpace, PDFDict), }; } } diff --git a/tests/api/PDFDocument.spec.ts b/tests/api/PDFDocument.spec.ts index d1cacbae4..ed7091955 100644 --- a/tests/api/PDFDocument.spec.ts +++ b/tests/api/PDFDocument.spec.ts @@ -14,6 +14,7 @@ import { PrintScaling, ReadingDirection, ViewerPreferences, + rgb, } from 'src/index'; const examplePngImage = @@ -163,6 +164,21 @@ describe(`PDFDocument`, () => { }); }); + describe(`embedSeparation() method`, () => { + it(`serializes the same value on every save`, async () => { + const pdfDoc1 = await PDFDocument.create({ updateMetadata: false }); + const pdfDoc2 = await PDFDocument.create({ updateMetadata: false }); + + pdfDoc1.embedSeparation('PANTONE 123 C', rgb(1, 0, 0)); + pdfDoc2.embedSeparation('PANTONE 123 C', rgb(1, 0, 0)); + + const savedDoc1 = await pdfDoc1.save(); + const savedDoc2 = await pdfDoc2.save(); + + expect(savedDoc1).toEqual(savedDoc2); + }); + }); + describe(`setLanguage() method`, () => { it(`sets the language of the document`, async () => { const pdfDoc = await PDFDocument.create(); diff --git a/tests/api/PDFPage.spec.ts b/tests/api/PDFPage.spec.ts index 3a5f0a949..c91f75574 100644 --- a/tests/api/PDFPage.spec.ts +++ b/tests/api/PDFPage.spec.ts @@ -1,5 +1,13 @@ import fs from 'fs'; -import { PDFArray, PDFDocument, PDFName, StandardFonts } from 'src/index'; +import { + cmyk, + PDFArray, + PDFDocument, + PDFName, + StandardFonts, + Separation, + ColorTypes, +} from 'src/index'; const birdPng = fs.readFileSync('assets/images/greyscale_bird.png'); @@ -148,4 +156,50 @@ describe(`PDFDocument`, () => { expect(key1).not.toEqual(key2); expect(page2.node.normalizedEntries().Font.keys()).toEqual([key1, key2]); }); + + describe('getSeparationColor', () => { + it('returns a new separation color', async () => { + const pdfDoc = await PDFDocument.create(); + const page = await pdfDoc.addPage(); + const pdfSeparation = await pdfDoc.embedSeparation( + 'PANTONE 123 C', + cmyk(0, 0.22, 0.83, 0), + ); + const color = page.getSeparationColor(pdfSeparation, 0.5); + expect(color.type).toBe(ColorTypes.Separation); + expect((color as Separation).tint).toEqual(0.5); + }); + + it('adds the separation color to the page resources', async () => { + const pdfDoc = await PDFDocument.create(); + const page = await pdfDoc.addPage(); + const pdfSeparation = await pdfDoc.embedSeparation( + 'PANTONE 123 C', + cmyk(0, 0.22, 0.83, 0), + ); + const color = page.getSeparationColor(pdfSeparation, 0.5); + const { ColorSpace } = page.node.normalizedEntries(); + expect(ColorSpace.keys()).toEqual([(color as Separation).name]); + expect(ColorSpace.get((color as Separation).name)).toEqual( + pdfSeparation.ref, + ); + }); + + it('does not add the same separation color twice', async () => { + const pdfDoc = await PDFDocument.create(); + const page = await pdfDoc.addPage(); + const pdfSeparation = await pdfDoc.embedSeparation( + 'PANTONE 123 C', + cmyk(0, 0.22, 0.83, 0), + ); + const color1 = page.getSeparationColor(pdfSeparation, 0.5); + const color2 = page.getSeparationColor(pdfSeparation, 0.5); + expect(color1).toEqual(color2); + const { ColorSpace } = page.node.normalizedEntries(); + expect(ColorSpace.keys()).toEqual([(color1 as Separation).name]); + expect(ColorSpace.get((color1 as Separation).name)).toEqual( + pdfSeparation.ref, + ); + }); + }); }); diff --git a/tests/api/PDFSeparation.spec.ts b/tests/api/PDFSeparation.spec.ts new file mode 100644 index 000000000..5beeae569 --- /dev/null +++ b/tests/api/PDFSeparation.spec.ts @@ -0,0 +1,54 @@ +import { PDFDocument, PDFSeparation } from 'src/api'; +import { SeparationEmbedder } from 'src/core'; + +describe(`PDFSeparation`, () => { + describe(`embed() method`, () => { + it(`gets name from the embedder`, async () => { + const pdfDoc = await PDFDocument.create(); + + const embedder = SeparationEmbedder.for('PANTONE 123 C', 'DeviceCMYK', [ + 0, + 0.22, + 0.83, + ]); + const ref = pdfDoc.context.nextRef(); + const pdfSeparation = PDFSeparation.of(ref, pdfDoc, embedder); + expect(pdfSeparation.name).toBe('PANTONE 123 C'); + }); + + it(`may be called multiple times without causing an error`, async () => { + const pdfDoc = await PDFDocument.create(); + + const embedder = SeparationEmbedder.for('PANTONE 123 C', 'DeviceCMYK', [ + 0, + 0.22, + 0.83, + ]); + const ref = pdfDoc.context.nextRef(); + const pdfSeparation = PDFSeparation.of(ref, pdfDoc, embedder); + + await expect(pdfSeparation.embed()).resolves.not.toThrowError(); + await expect(pdfSeparation.embed()).resolves.not.toThrowError(); + }); + + it(`may be called in parallel without causing an error`, async () => { + const pdfDoc = await PDFDocument.create(); + + const embedder = SeparationEmbedder.for('PANTONE 123 C', 'DeviceCMYK', [ + 0, + 0.22, + 0.83, + ]); + jest.spyOn(embedder, 'embedIntoContext'); + const ref = pdfDoc.context.nextRef(); + const pdfSeparation = PDFSeparation.of(ref, pdfDoc, embedder); + + const task1 = pdfSeparation.embed(); + const task2 = pdfSeparation.embed(); + + await Promise.all([task1, task2]); + + expect(embedder.embedIntoContext).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/tests/core/embedders/SeparationEmbedder.spec.ts b/tests/core/embedders/SeparationEmbedder.spec.ts new file mode 100644 index 000000000..7ede27792 --- /dev/null +++ b/tests/core/embedders/SeparationEmbedder.spec.ts @@ -0,0 +1,42 @@ +import { PDFArray, PDFContext, PDFRef, SeparationEmbedder } from 'src/index'; + +describe(`SeparationEmbedder`, () => { + it(`can be constructed with SeparationEmbedder.for(...)`, () => { + const embedder = SeparationEmbedder.for('PANTONE 123 C', 'DeviceCMYK', [ + 0, + 0.22, + 0.83, + ]); + expect(embedder).toBeInstanceOf(SeparationEmbedder); + }); + + it(`can embed separations into PDFContexts without a predefined ref`, async () => { + const context = PDFContext.create(); + const embedder = SeparationEmbedder.for('PANTONE 123 C', 'DeviceCMYK', [ + 0, + 0.22, + 0.83, + ]); + + expect(context.enumerateIndirectObjects().length).toBe(0); + const ref = embedder.embedIntoContext(context); + expect(context.enumerateIndirectObjects().length).toBe(1); + expect(context.lookup(ref)).toBeInstanceOf(PDFArray); + }); + + fit(`can embed separations into PDFContexts with a predefined ref`, async () => { + const context = PDFContext.create(); + const predefinedRef = PDFRef.of(9999); + const embedder = SeparationEmbedder.for('PANTONE 123 C', 'DeviceCMYK', [ + 0, + 0.22, + 0.83, + ]); + + expect(context.enumerateIndirectObjects().length).toBe(0); + const ref = embedder.embedIntoContext(context, predefinedRef); + expect(context.enumerateIndirectObjects().length).toBe(1); + expect(context.lookup(predefinedRef)).toBeInstanceOf(PDFArray); + expect(ref).toBe(predefinedRef); + }); +}); diff --git a/tests/core/structures/PDFPageLeaf.spec.ts b/tests/core/structures/PDFPageLeaf.spec.ts index 6c1305149..e98437168 100644 --- a/tests/core/structures/PDFPageLeaf.spec.ts +++ b/tests/core/structures/PDFPageLeaf.spec.ts @@ -287,6 +287,18 @@ describe(`PDFPageLeaf`, () => { ); }); + it(`can set ColorSpace refs`, () => { + const context = PDFContext.create(); + const parentRef = PDFRef.of(1); + const pageTree = PDFPageLeaf.withContextAndParent(context, parentRef); + + const ColorSpace = PDFName.of('ColorSpace'); + pageTree.setColorSpace(PDFName.of('Foo'), PDFRef.of(21)); + expect(pageTree.Resources()!.get(ColorSpace)!.toString()).toBe( + '<<\n/Foo 21 0 R\n>>', + ); + }); + it(`can be ascended`, () => { const context = PDFContext.create(); @@ -325,7 +337,7 @@ describe(`PDFPageLeaf`, () => { expect(pageTree.Contents()!.toString()).toBe('[ 21 0 R ]'); expect(pageTree.Resources()!.toString()).toBe( - '<<\n/Font <<\n>>\n/XObject <<\n>>\n/ExtGState <<\n>>\n>>', + '<<\n/Font <<\n>>\n/XObject <<\n>>\n/ExtGState <<\n>>\n/ColorSpace <<\n>>\n>>', ); }); @@ -349,7 +361,7 @@ describe(`PDFPageLeaf`, () => { `[ ${pushRef} 21 0 R ${popRef} ]`, ); expect(pageTree.Resources()!.toString()).toBe( - '<<\n/Font <<\n>>\n/XObject <<\n>>\n/ExtGState <<\n>>\n>>', + '<<\n/Font <<\n>>\n/XObject <<\n>>\n/ExtGState <<\n>>\n/ColorSpace <<\n>>\n>>', ); }); });