Skip to content

Commit 23a0801

Browse files
author
Philippe Plantier
committed
Add support for Separation colors
1 parent 93dd36e commit 23a0801

18 files changed

+484
-15
lines changed

rollup.config.js

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const IgnoredWarnings = [
99
// Mac & Linux
1010
'Circular dependency: es/api/PDFDocument.js -> es/api/PDFFont.js -> es/api/PDFDocument.js',
1111
'Circular dependency: es/api/PDFDocument.js -> es/api/PDFImage.js -> es/api/PDFDocument.js',
12+
'Circular dependency: es/api/PDFDocument.js -> es/api/PDFSeparation.js -> es/api/PDFDocument.js',
1213
'Circular dependency: es/api/PDFDocument.js -> es/api/PDFPage.js -> es/api/PDFDocument.js',
1314
'Circular dependency: es/api/PDFPage.js -> es/api/PDFDocument.js -> es/api/PDFPage.js',
1415
'Circular dependency: es/api/PDFDocument.js -> es/api/PDFEmbeddedPage.js -> es/api/PDFDocument.js',
@@ -25,6 +26,7 @@ const IgnoredWarnings = [
2526
// Windows
2627
'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFFont.js -> es\\api\\PDFDocument.js',
2728
'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFImage.js -> es\\api\\PDFDocument.js',
29+
'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFSeparation.js -> es\\api\\PDFDocument.js',
2830
'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFPage.js -> es\\api\\PDFDocument.js',
2931
'Circular dependency: es\\api\\PDFPage.js -> es\\api\\PDFDocument.js -> es\\api\\PDFPage.js',
3032
'Circular dependency: es\\api\\PDFDocument.js -> es\\api\\PDFEmbeddedPage.js -> es\\api\\PDFDocument.js',

src/api/PDFDocument.ts

+40
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import PDFFont from 'src/api/PDFFont';
1010
import PDFImage from 'src/api/PDFImage';
1111
import PDFPage from 'src/api/PDFPage';
1212
import PDFForm from 'src/api/form/PDFForm';
13+
import PDFSeparation from 'src/api/PDFSeparation';
1314
import { PageSizes } from 'src/api/sizes';
1415
import { StandardFonts } from 'src/api/StandardFonts';
1516
import {
@@ -33,6 +34,7 @@ import {
3334
PDFWriter,
3435
PngEmbedder,
3536
StandardFontEmbedder,
37+
SeparationEmbedder,
3638
UnexpectedObjectTypeError,
3739
} from 'src/core';
3840
import {
@@ -61,11 +63,13 @@ import {
6163
pluckIndices,
6264
range,
6365
toUint8Array,
66+
error,
6467
} from 'src/utils';
6568
import FileEmbedder, { AFRelationship } from 'src/core/embedders/FileEmbedder';
6669
import PDFEmbeddedFile from 'src/api/PDFEmbeddedFile';
6770
import PDFJavaScript from 'src/api/PDFJavaScript';
6871
import JavaScriptEmbedder from 'src/core/embedders/JavaScriptEmbedder';
72+
import { Color, ColorTypes, colorToComponents } from './colors';
6973

7074
/**
7175
* Represents a PDF document.
@@ -185,6 +189,7 @@ export default class PDFDocument {
185189
private readonly formCache: Cache<PDFForm>;
186190
private readonly fonts: PDFFont[];
187191
private readonly images: PDFImage[];
192+
private readonly separationColorSpaces: PDFSeparation[];
188193
private readonly embeddedPages: PDFEmbeddedPage[];
189194
private readonly embeddedFiles: PDFEmbeddedFile[];
190195
private readonly javaScripts: PDFJavaScript[];
@@ -206,6 +211,7 @@ export default class PDFDocument {
206211
this.formCache = Cache.populatedBy(this.getOrCreateForm);
207212
this.fonts = [];
208213
this.images = [];
214+
this.separationColorSpaces = [];
209215
this.embeddedPages = [];
210216
this.embeddedFiles = [];
211217
this.javaScripts = [];
@@ -997,6 +1003,32 @@ export default class PDFDocument {
9971003
return pdfFont;
9981004
}
9991005

1006+
/**
1007+
* Embed a separation color space into this document.
1008+
* For example:
1009+
* ```js
1010+
* import { rgb } from 'pdf-lib'
1011+
* const separation = pdfDoc.embedSeparation('PANTONE 123 C', rgb(1, 0, 0))
1012+
* ```
1013+
*
1014+
* @param name The name of the separation color space.
1015+
* @param alternate An alternate color to be used to approximate the intended
1016+
* color.
1017+
*/
1018+
embedSeparation(name: string, alternate: Color): PDFSeparation {
1019+
const ref = this.context.nextRef();
1020+
const alternateColorSpace = getColorSpace(alternate);
1021+
const alternateColorComponents = colorToComponents(alternate);
1022+
const embedder = SeparationEmbedder.for(
1023+
name,
1024+
alternateColorSpace,
1025+
alternateColorComponents,
1026+
);
1027+
const separation = PDFSeparation.of(ref, this, embedder);
1028+
this.separationColorSpaces.push(separation);
1029+
return separation;
1030+
}
1031+
10001032
/**
10011033
* Embed a JPEG image into this document. The input data can be provided in
10021034
* multiple formats:
@@ -1243,6 +1275,7 @@ export default class PDFDocument {
12431275
async flush(): Promise<void> {
12441276
await this.embedAll(this.fonts);
12451277
await this.embedAll(this.images);
1278+
await this.embedAll(this.separationColorSpaces);
12461279
await this.embedAll(this.embeddedPages);
12471280
await this.embedAll(this.embeddedFiles);
12481281
await this.embedAll(this.javaScripts);
@@ -1393,3 +1426,10 @@ function assertIsLiteralOrHexString(
13931426
throw new UnexpectedObjectTypeError([PDFHexString, PDFString], pdfObject);
13941427
}
13951428
}
1429+
1430+
// prettier-ignore
1431+
const getColorSpace = (color: Color) =>
1432+
color.type === ColorTypes.Grayscale ? 'DeviceGray'
1433+
: color.type === ColorTypes.RGB ? 'DeviceRGB'
1434+
: color.type === ColorTypes.CMYK ? 'DeviceCMYK'
1435+
: error(`Invalid alternate color: ${JSON.stringify(color)}`);

src/api/PDFPage.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Color, rgb } from 'src/api/colors';
1+
import { Color, rgb, separation } from 'src/api/colors';
22
import {
33
drawImage,
44
drawLine,
@@ -55,6 +55,7 @@ import {
5555
assertRangeOrUndefined,
5656
assertIsOneOfOrUndefined,
5757
} from 'src/utils';
58+
import PDFSeparation from './PDFSeparation';
5859

5960
/**
6061
* Represents a single page of a [[PDFDocument]].
@@ -763,6 +764,30 @@ export default class PDFPage {
763764
this.lineHeight = lineHeight;
764765
}
765766

767+
/**
768+
* Creates a local Separation color for this page. The color can then be
769+
* used to draw text or fill shapes. For example:
770+
* ```js
771+
* const pdfSeparation = await pdfDoc.embedSeparation(
772+
* 'PANTONE 123 C',
773+
* cmyk(0, 0.22, 0.83, 0),
774+
* );
775+
* const color = page.getSeparationColor(pdfSeparation, 0.5);
776+
* page.drawText('This text will be printed using a spot color', { color });
777+
* ```
778+
*
779+
* @param pdfSeparation A PDFSeparation object that was embedded into the
780+
* document.
781+
* @param tint The tint (intensity) value to use for the color.
782+
*
783+
* @returns The name of the color space in the page's resources.
784+
*/
785+
getSeparationColor(pdfSeparation: PDFSeparation, tint: number): Color {
786+
const name = pdfSeparation.name;
787+
const ref = pdfSeparation.ref;
788+
return separation(this.node.newColorSpace(name, ref), tint);
789+
}
790+
766791
/**
767792
* Get the default position of this page. For example:
768793
* ```js

src/api/PDFSeparation.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import Embeddable from 'src/api/Embeddable';
2+
import PDFDocument from 'src/api/PDFDocument';
3+
import { PDFRef } from 'src/core';
4+
import SeparationEmbedder from 'src/core/embedders/SeparationEmbedder';
5+
import { assertIs } from 'src/utils';
6+
7+
/**
8+
* Represents a file that has been embedded in a [[PDFDocument]].
9+
*/
10+
export default class PDFSeparation implements Embeddable {
11+
/**
12+
* > **NOTE:** You probably don't want to call this method directly. Instead,
13+
* > consider using the [[PDFDocument.embedSeparation]] method which will
14+
* > create instances of [[PDFSeparation]] for you.
15+
*
16+
* Create an instance of [[PDFSeparation]] from an existing ref and embedder
17+
*
18+
* @param ref The unique reference for this file.
19+
* @param doc The document to which the file will belong.
20+
* @param embedder The embedder that will be used to embed the file.
21+
*/
22+
static of = (ref: PDFRef, doc: PDFDocument, embedder: SeparationEmbedder) =>
23+
new PDFSeparation(ref, doc, embedder);
24+
25+
/** The unique reference assigned to this separation within the document. */
26+
readonly ref: PDFRef;
27+
28+
/** The document to which this separation belongs. */
29+
readonly doc: PDFDocument;
30+
31+
/** The name of this separation. */
32+
readonly name: string;
33+
34+
private alreadyEmbedded = false;
35+
private readonly embedder: SeparationEmbedder;
36+
37+
private constructor(
38+
ref: PDFRef,
39+
doc: PDFDocument,
40+
embedder: SeparationEmbedder,
41+
) {
42+
assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]);
43+
assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]);
44+
assertIs(embedder, 'embedder', [
45+
[SeparationEmbedder, 'SeparationEmbedder'],
46+
]);
47+
this.ref = ref;
48+
this.doc = doc;
49+
this.name = embedder.separationName;
50+
51+
this.embedder = embedder;
52+
}
53+
54+
/**
55+
* > **NOTE:** You probably don't need to call this method directly. The
56+
* > [[PDFDocument.save]] and [[PDFDocument.saveAsBase64]] methods will
57+
* > automatically ensure all separations get embedded.
58+
*
59+
* Embed this separation in its document.
60+
*
61+
* @returns Resolves when the embedding is complete.
62+
*/
63+
async embed(): Promise<void> {
64+
if (!this.embedder) return;
65+
66+
// The separation should only be embedded once. If there's a pending embed
67+
// operation then wait on it. Otherwise we need to start the embed.
68+
if (this.alreadyEmbedded) return;
69+
this.alreadyEmbedded = true;
70+
71+
await this.embedder.embedIntoContext(this.doc.context, this.ref);
72+
}
73+
}

src/api/colors.ts

+32-8
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@ import {
22
setFillingCmykColor,
33
setFillingGrayscaleColor,
44
setFillingRgbColor,
5+
setFillingSpecialColor,
56
setStrokingCmykColor,
67
setStrokingGrayscaleColor,
78
setStrokingRgbColor,
9+
setStrokingSpecialColor,
10+
setFillingColorspace,
811
} from 'src/api/operators';
912
import { assertRange, error } from 'src/utils';
13+
import { PDFName } from 'src/core';
1014

1115
export enum ColorTypes {
1216
Grayscale = 'Grayscale',
1317
RGB = 'RGB',
1418
CMYK = 'CMYK',
19+
Separation = 'Separation',
1520
}
1621

1722
export interface Grayscale {
@@ -34,7 +39,13 @@ export interface CMYK {
3439
key: number;
3540
}
3641

37-
export type Color = Grayscale | RGB | CMYK;
42+
export interface Separation {
43+
type: ColorTypes.Separation;
44+
name: PDFName;
45+
tint: number;
46+
}
47+
48+
export type Color = Grayscale | RGB | CMYK | Separation;
3849

3950
export const grayscale = (gray: number): Grayscale => {
4051
assertRange(gray, 'gray', 0.0, 1.0);
@@ -61,20 +72,33 @@ export const cmyk = (
6172
return { type: ColorTypes.CMYK, cyan, magenta, yellow, key };
6273
};
6374

64-
const { Grayscale, RGB, CMYK } = ColorTypes;
75+
export const separation = (name: PDFName, tint: number): Separation => {
76+
assertRange(tint, 'tint', 0, 1);
77+
return { type: ColorTypes.Separation, name, tint };
78+
};
79+
80+
const { Grayscale, RGB, CMYK, Separation } = ColorTypes;
81+
82+
export const setFillingColorspaceOrUndefined = (color: Color) =>
83+
color.type === Separation ? setFillingColorspace(color.name) : undefined;
6584

6685
// prettier-ignore
6786
export const setFillingColor = (color: Color) =>
68-
color.type === Grayscale ? setFillingGrayscaleColor(color.gray)
69-
: color.type === RGB ? setFillingRgbColor(color.red, color.green, color.blue)
70-
: color.type === CMYK ? setFillingCmykColor(color.cyan, color.magenta, color.yellow, color.key)
87+
color.type === Grayscale ? setFillingGrayscaleColor(color.gray)
88+
: color.type === RGB ? setFillingRgbColor(color.red, color.green, color.blue)
89+
: color.type === CMYK ? setFillingCmykColor(color.cyan, color.magenta, color.yellow, color.key)
90+
: color.type === Separation ? setFillingSpecialColor(color.tint)
7191
: error(`Invalid color: ${JSON.stringify(color)}`);
7292

93+
export const setStrokingColorspaceOrUndefined = (color: Color) =>
94+
color.type === Separation ? setFillingColorspace(color.name) : undefined;
95+
7396
// prettier-ignore
7497
export const setStrokingColor = (color: Color) =>
75-
color.type === Grayscale ? setStrokingGrayscaleColor(color.gray)
76-
: color.type === RGB ? setStrokingRgbColor(color.red, color.green, color.blue)
77-
: color.type === CMYK ? setStrokingCmykColor(color.cyan, color.magenta, color.yellow, color.key)
98+
color.type === Grayscale ? setStrokingGrayscaleColor(color.gray)
99+
: color.type === RGB ? setStrokingRgbColor(color.red, color.green, color.blue)
100+
: color.type === CMYK ? setStrokingCmykColor(color.cyan, color.magenta, color.yellow, color.key)
101+
: color.type === Separation ? setStrokingSpecialColor(color.tint)
78102
: error(`Invalid color: ${JSON.stringify(color)}`);
79103

80104
// prettier-ignore

src/api/form/appearances.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
grayscale,
2424
cmyk,
2525
Color,
26+
setFillingColorspaceOrUndefined,
2627
} from 'src/api/colors';
2728
import { reduceRotation, adjustDimsForRotation } from 'src/api/rotations';
2829
import {
@@ -157,9 +158,12 @@ const updateDefaultAppearance = (
157158
fontSize: number = 0,
158159
) => {
159160
const da = [
161+
setFillingColorspaceOrUndefined(color)?.toString(),
160162
setFillingColor(color).toString(),
161163
setFontAndSize(font?.name ?? 'dummy__noop', fontSize).toString(),
162-
].join('\n');
164+
]
165+
.filter(Boolean)
166+
.join('\n');
163167
field.setDefaultAppearance(da);
164168
};
165169

src/api/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from 'src/api/StandardFonts';
1414
export { default as PDFDocument } from 'src/api/PDFDocument';
1515
export { default as PDFFont } from 'src/api/PDFFont';
1616
export { default as PDFImage } from 'src/api/PDFImage';
17+
export { default as PDFSeparation } from 'src/api/PDFSeparation';
1718
export { default as PDFPage } from 'src/api/PDFPage';
1819
export { default as PDFEmbeddedPage } from 'src/api/PDFEmbeddedPage';
1920
export { default as PDFJavaScript } from 'src/api/PDFJavaScript';

0 commit comments

Comments
 (0)