Skip to content

Commit

Permalink
Add support for Separation colors
Browse files Browse the repository at this point in the history
  • Loading branch information
Philippe Plantier committed Oct 12, 2022
1 parent 93dd36e commit c52af89
Show file tree
Hide file tree
Showing 18 changed files with 478 additions and 15 deletions.
2 changes: 2 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
40 changes: 40 additions & 0 deletions src/api/PDFDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -33,6 +34,7 @@ import {
PDFWriter,
PngEmbedder,
StandardFontEmbedder,
SeparationEmbedder,
UnexpectedObjectTypeError,
} from 'src/core';
import {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -185,6 +189,7 @@ export default class PDFDocument {
private readonly formCache: Cache<PDFForm>;
private readonly fonts: PDFFont[];
private readonly images: PDFImage[];
private readonly separationColorSpaces: PDFSeparation[];
private readonly embeddedPages: PDFEmbeddedPage[];
private readonly embeddedFiles: PDFEmbeddedFile[];
private readonly javaScripts: PDFJavaScript[];
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1243,6 +1275,7 @@ export default class PDFDocument {
async flush(): Promise<void> {
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);
Expand Down Expand Up @@ -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)}`);
27 changes: 26 additions & 1 deletion src/api/PDFPage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Color, rgb } from 'src/api/colors';
import { Color, rgb, separation } from 'src/api/colors';
import {
drawImage,
drawLine,
Expand Down Expand Up @@ -55,6 +55,7 @@ import {
assertRangeOrUndefined,
assertIsOneOfOrUndefined,
} from 'src/utils';
import PDFSeparation from './PDFSeparation';

/**
* Represents a single page of a [[PDFDocument]].
Expand Down Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions src/api/PDFSeparation.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
}
40 changes: 32 additions & 8 deletions src/api/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import {
setFillingCmykColor,
setFillingGrayscaleColor,
setFillingRgbColor,
setFillingSpecialColor,
setStrokingCmykColor,
setStrokingGrayscaleColor,
setStrokingRgbColor,
setStrokingSpecialColor,
setFillingColorspace,
} 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 {
Expand All @@ -34,7 +39,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);
Expand All @@ -61,20 +72,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 ? setFillingColorspace(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
Expand Down
6 changes: 5 additions & 1 deletion src/api/form/appearances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
grayscale,
cmyk,
Color,
setFillingColorspaceOrUndefined,
} from 'src/api/colors';
import { reduceRotation, adjustDimsForRotation } from 'src/api/rotations';
import {
Expand Down Expand Up @@ -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);
};

Expand Down
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit c52af89

Please sign in to comment.